diff --git a/.gitignore b/.gitignore index d7bb907eb..91fa7cd04 100644 --- a/.gitignore +++ b/.gitignore @@ -221,3 +221,4 @@ package-lock.json /.proxy-dash.pid.lock .vscode +cerberus-dashboard/.eslintcache diff --git a/cerberus-core/src/main/java/com/nike/cerberus/error/AuthTokenTooLongException.java b/cerberus-core/src/main/java/com/nike/cerberus/error/AuthTokenTooLongException.java new file mode 100644 index 000000000..fa7ce366c --- /dev/null +++ b/cerberus-core/src/main/java/com/nike/cerberus/error/AuthTokenTooLongException.java @@ -0,0 +1,12 @@ +package com.nike.cerberus.error; + +public class AuthTokenTooLongException extends Exception { + + private AuthTokenTooLongException() { + super(); + } + + public AuthTokenTooLongException(String message) { + super(message); + } +} diff --git a/cerberus-core/src/main/java/com/nike/cerberus/error/DefaultApiError.java b/cerberus-core/src/main/java/com/nike/cerberus/error/DefaultApiError.java index 83f1f44f0..c16cfcb75 100644 --- a/cerberus-core/src/main/java/com/nike/cerberus/error/DefaultApiError.java +++ b/cerberus-core/src/main/java/com/nike/cerberus/error/DefaultApiError.java @@ -54,6 +54,10 @@ public enum DefaultApiError implements ApiError { /** Supplied credentials are invalid. */ AUTH_BAD_CREDENTIALS(99106, "Invalid credentials", SC_UNAUTHORIZED), + /** User belongs to too many groups, so the jwt token would make header too large */ + AUTH_TOKEN_TOO_LONG( + 99107, "X-Cerberus-Token header would be too long.", SC_INTERNAL_SERVER_ERROR), + /** Category display name is blank. */ CATEGORY_DISPLAY_NAME_BLANK(99200, "Display name may not be blank.", SC_BAD_REQUEST), diff --git a/cerberus-core/src/main/java/com/nike/cerberus/security/CerberusPrincipal.java b/cerberus-core/src/main/java/com/nike/cerberus/security/CerberusPrincipal.java index 62f59fd79..5c62f9c88 100644 --- a/cerberus-core/src/main/java/com/nike/cerberus/security/CerberusPrincipal.java +++ b/cerberus-core/src/main/java/com/nike/cerberus/security/CerberusPrincipal.java @@ -87,6 +87,10 @@ public String getToken() { return cerberusAuthToken.getToken(); } + public String getTokenId() { + return cerberusAuthToken.getId(); + } + public Set getUserGroups() { if (cerberusAuthToken.getGroups() == null) { return new HashSet<>(); diff --git a/cerberus-core/src/test/java/com/nike/cerberus/event/AuditableEventContextTest.java b/cerberus-core/src/test/java/com/nike/cerberus/event/AuditableEventContextTest.java index a38949e0f..74bf8f5b9 100644 --- a/cerberus-core/src/test/java/com/nike/cerberus/event/AuditableEventContextTest.java +++ b/cerberus-core/src/test/java/com/nike/cerberus/event/AuditableEventContextTest.java @@ -26,7 +26,7 @@ public void testCheckAuthTokenIsEmptyIfPrincipleIsNotInstanceOfCerberusAuthToken @Test public void testCheckAuthTokenIsEmptyIfPrincipleIsInstanceOfCerberusAuthToken() { - CerberusAuthToken cerberusAuthToken = new CerberusAuthToken(); + CerberusAuthToken cerberusAuthToken = CerberusAuthToken.Builder.create().build(); auditableEventContext.setPrincipal(cerberusAuthToken); Optional principalAsCerberusPrincipal = auditableEventContext.getPrincipalAsCerberusPrincipal(); @@ -36,9 +36,9 @@ public void testCheckAuthTokenIsEmptyIfPrincipleIsInstanceOfCerberusAuthToken() @Test public void testGetPrincipalNameIfPrincipleIsInstanceOfCerberusAuthToken() { - CerberusAuthToken cerberusAuthToken = new CerberusAuthToken(); String cerberusPrinciple = "cerberusPrinciple"; - cerberusAuthToken.setPrincipal(cerberusPrinciple); + CerberusAuthToken cerberusAuthToken = + CerberusAuthToken.Builder.create().withPrincipal(cerberusPrinciple).build(); auditableEventContext.setPrincipal(cerberusAuthToken); String principalName = auditableEventContext.getPrincipalName(); Assert.assertEquals(cerberusPrinciple, principalName); diff --git a/cerberus-dashboard/.eslintcache b/cerberus-dashboard/.eslintcache deleted file mode 100644 index 5e6e5ac97..000000000 --- a/cerberus-dashboard/.eslintcache +++ /dev/null @@ -1 +0,0 @@ -[{"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/index.js":"1","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/store/configureStore.js":"2","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/utils/logger.js":"3","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/LandingView/LandingView.js":"4","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/App/App.js":"5","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/NotFound/NotFound.js":"6","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/SDBMetadataList/SDBMetadataList.js":"7","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/ManageSafeDepositBox/ManageSafeDepositBox.js":"8","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/actions/authenticationActions.js":"9","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/reducers/rootReducer.js":"10","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/actions/metadataActions.js":"11","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/service/EnvironmentService.js":"12","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/Login/Login.js":"13","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/Modal/Modal.js":"14","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/Messenger/Messenger.js":"15","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/Footer/Footer.js":"16","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/actions/appActions.js":"17","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/Header/Header.js":"18","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/SideBar/SideBar.js":"19","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/SDBMetadata/SDBMetadata.js":"20","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/actions/versionHistoryBrowserActions.js":"21","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/SecureDataBrowser/SecureDataBrowser.js":"22","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/SafeDepositBoxSettings/SafeDepositBoxSettings.js":"23","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/actions/messengerActions.js":"24","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/actions/headerActions.js":"25","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/utils/cmsUtils.js":"26","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/actions/modalActions.js":"27","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/constants/cms.js":"28","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/ApiError/ApiError.js":"29","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/ConfirmationBox/ConfirmationBox.js":"30","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/SecureDataVersionsBrowser/SecureDataVersionsBrowser.js":"31","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/constants/actions.js":"32","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/actions/manageSafetyDepositBoxActions.js":"33","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/reducers/headerStateReducer.js":"34","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/reducers/appReducer.js":"35","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/reducers/createSDBoxReducer.js":"36","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/reducers/messengerReducer.js":"37","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/reducers/metadataReducer.js":"38","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/reducers/modalReducer.js":"39","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/reducers/authenticationReducer.js":"40","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/reducers/versionHistoryBrowserReducer.js":"41","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/actions/createSDBoxActions.js":"42","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/LoginUserForm/LoginUserForm.js":"43","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/Loader/Loader.js":"44","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/LoginMfaForm/LoginMfaForm.js":"45","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/reducers/manageSafetyDepositBoxReducer.js":"46","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/CreateSDBoxForm/CreateSDBoxForm.js":"47","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/ViewTokenModal/ViewTokenModal.js":"48","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/AddButton/AddButton.js":"49","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/FileButton/FileButton.js":"50","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/SecureData/SecureData.js":"51","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/SecureFile/SecureFile.js":"52","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/DeleteSafeDepositBoxForm/DeleteSafeDepositBoxForm.js":"53","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/EditSDBoxForm/EditSDBoxForm.js":"54","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/SecureDataForm/SecureDataForm.js":"55","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/SecureFileForm/SecureFileForm.js":"56","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/SDBDescriptionField/SDBDescriptionField.js":"57","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/GroupSelect/GroupsSelect.js":"58","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/IamPrincipalPermissionsFieldSet/IamPrincipalPermissionsFieldSet.js":"59","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/UserGroupPermissionsFieldSet/UserGroupPermissionsFieldSet.js":"60","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/Buttons/Buttons.js":"61","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/RoleSelect/RoleSelect.js":"62","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/DeleteSafeDepositBoxForm/validator.js":"63","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/CategorySelect/CategorySelect.js":"64","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/CreateSDBoxForm/validator.js":"65","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/MfaDeviceSelect/MfaDeviceSelect.js":"66","/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/utils/index.js":"67"},{"size":4045,"mtime":1601579886279,"results":"68","hashOfConfig":"69"},{"size":1933,"mtime":1597944654537,"results":"70","hashOfConfig":"69"},{"size":960,"mtime":1597944654537,"results":"71","hashOfConfig":"69"},{"size":2701,"mtime":1597944654522,"results":"72","hashOfConfig":"69"},{"size":3359,"mtime":1597944654517,"results":"73","hashOfConfig":"69"},{"size":859,"mtime":1597944654526,"results":"74","hashOfConfig":"69"},{"size":4857,"mtime":1597944654527,"results":"75","hashOfConfig":"69"},{"size":7111,"mtime":1597944654524,"results":"76","hashOfConfig":"69"},{"size":18601,"mtime":1602021414126,"results":"77","hashOfConfig":"69"},{"size":1372,"mtime":1597944654536,"results":"78","hashOfConfig":"69"},{"size":2511,"mtime":1597944654486,"results":"79","hashOfConfig":"69"},{"size":1703,"mtime":1597944654536,"results":"80","hashOfConfig":"69"},{"size":2527,"mtime":1601930126516,"results":"81","hashOfConfig":"69"},{"size":1253,"mtime":1597944654525,"results":"82","hashOfConfig":"69"},{"size":1761,"mtime":1597944654525,"results":"83","hashOfConfig":"69"},{"size":885,"mtime":1597944654521,"results":"84","hashOfConfig":"69"},{"size":6994,"mtime":1597944654485,"results":"85","hashOfConfig":"69"},{"size":3842,"mtime":1610138382736,"results":"86","hashOfConfig":"69"},{"size":4705,"mtime":1597944654532,"results":"87","hashOfConfig":"69"},{"size":4362,"mtime":1597944654527,"results":"88","hashOfConfig":"69"},{"size":5747,"mtime":1597944654487,"results":"89","hashOfConfig":"69"},{"size":8174,"mtime":1597944654529,"results":"90","hashOfConfig":"69"},{"size":6413,"mtime":1597944654528,"results":"91","hashOfConfig":"69"},{"size":1717,"mtime":1597944654486,"results":"92","hashOfConfig":"69"},{"size":1041,"mtime":1597944654486,"results":"93","hashOfConfig":"69"},{"size":1086,"mtime":1601923292880,"results":"94","hashOfConfig":"69"},{"size":999,"mtime":1597944654486,"results":"95","hashOfConfig":"69"},{"size":1174,"mtime":1597944654534,"results":"96","hashOfConfig":"69"},{"size":2162,"mtime":1597944654516,"results":"97","hashOfConfig":"69"},{"size":1578,"mtime":1597944654518,"results":"98","hashOfConfig":"69"},{"size":12150,"mtime":1597944654531,"results":"99","hashOfConfig":"69"},{"size":5202,"mtime":1602021414126,"results":"100","hashOfConfig":"69"},{"size":20616,"mtime":1597944654486,"results":"101","hashOfConfig":"69"},{"size":1299,"mtime":1597944654535,"results":"102","hashOfConfig":"69"},{"size":2326,"mtime":1597944654534,"results":"103","hashOfConfig":"69"},{"size":1505,"mtime":1597944654535,"results":"104","hashOfConfig":"69"},{"size":1893,"mtime":1597944654535,"results":"105","hashOfConfig":"69"},{"size":1301,"mtime":1597944654536,"results":"106","hashOfConfig":"69"},{"size":1478,"mtime":1597944654536,"results":"107","hashOfConfig":"69"},{"size":4307,"mtime":1601930134837,"results":"108","hashOfConfig":"69"},{"size":2950,"mtime":1597944654536,"results":"109","hashOfConfig":"69"},{"size":3006,"mtime":1597944654485,"results":"110","hashOfConfig":"69"},{"size":4989,"mtime":1597944654524,"results":"111","hashOfConfig":"69"},{"size":1342,"mtime":1597944654523,"results":"112","hashOfConfig":"69"},{"size":7607,"mtime":1601923292877,"results":"113","hashOfConfig":"69"},{"size":12571,"mtime":1597944654535,"results":"114","hashOfConfig":"69"},{"size":6825,"mtime":1597944654518,"results":"115","hashOfConfig":"69"},{"size":5486,"mtime":1597944654533,"results":"116","hashOfConfig":"69"},{"size":1576,"mtime":1597944654516,"results":"117","hashOfConfig":"69"},{"size":1536,"mtime":1597944654520,"results":"118","hashOfConfig":"69"},{"size":4360,"mtime":1597944654529,"results":"119","hashOfConfig":"69"},{"size":3256,"mtime":1597944654531,"results":"120","hashOfConfig":"69"},{"size":4128,"mtime":1597944654519,"results":"121","hashOfConfig":"69"},{"size":4944,"mtime":1597944654520,"results":"122","hashOfConfig":"69"},{"size":8748,"mtime":1597944654530,"results":"123","hashOfConfig":"69"},{"size":10836,"mtime":1597944654532,"results":"124","hashOfConfig":"69"},{"size":1795,"mtime":1597944654527,"results":"125","hashOfConfig":"69"},{"size":2550,"mtime":1597944654521,"results":"126","hashOfConfig":"69"},{"size":4045,"mtime":1597944654522,"results":"127","hashOfConfig":"69"},{"size":3551,"mtime":1597944654533,"results":"128","hashOfConfig":"69"},{"size":1565,"mtime":1597944654517,"results":"129","hashOfConfig":"69"},{"size":1618,"mtime":1597944654526,"results":"130","hashOfConfig":"69"},{"size":930,"mtime":1597944654520,"results":"131","hashOfConfig":"69"},{"size":1731,"mtime":1597944654518,"results":"132","hashOfConfig":"69"},{"size":3282,"mtime":1597944654519,"results":"133","hashOfConfig":"69"},{"size":2209,"mtime":1597944654525,"results":"134","hashOfConfig":"69"},{"size":821,"mtime":1597944654537,"results":"135","hashOfConfig":"69"},{"filePath":"136","messages":"137","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},"1245nyw",{"filePath":"139","messages":"140","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"141","messages":"142","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"143","messages":"144","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"145","messages":"146","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"147","messages":"148","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"149","messages":"150","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"151","messages":"152","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"153","messages":"154","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"155","messages":"156","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"157","messages":"158","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"159","messages":"160","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"161","messages":"162","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"163","messages":"164","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"165","messages":"166","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"167","messages":"168","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"169","messages":"170","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"171","messages":"172","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"173","messages":"174","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"175","messages":"176","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"177","messages":"178","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"179","messages":"180","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"181","messages":"182","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"183","messages":"184","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"185","messages":"186","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"187","messages":"188","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"189","messages":"190","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"191","messages":"192","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"193","messages":"194","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"195","messages":"196","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"197","messages":"198","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"199","messages":"200","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"201","messages":"202","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"203","messages":"204","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"205","messages":"206","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"207","messages":"208","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"209","messages":"210","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"211","messages":"212","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"213","messages":"214","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"215","messages":"216","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"217","messages":"218","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"219","messages":"220","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"221","messages":"222","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"223","messages":"224","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"225","messages":"226","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"227","messages":"228","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"229","messages":"230","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"231","messages":"232","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"233","messages":"234","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"235","messages":"236","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"237","messages":"238","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"239","messages":"240","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"241","messages":"242","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"243","messages":"244","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"245","messages":"246","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"247","messages":"248","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"249","messages":"250","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"251","messages":"252","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"253","messages":"254","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"255","messages":"256","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"257","messages":"258","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"259"},{"filePath":"260","messages":"261","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"262","messages":"263","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"264","messages":"265","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"266","messages":"267","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"268","messages":"269","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},{"filePath":"270","messages":"271","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"138"},"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/index.js",[],[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/store/configureStore.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/utils/logger.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/LandingView/LandingView.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/App/App.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/NotFound/NotFound.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/SDBMetadataList/SDBMetadataList.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/ManageSafeDepositBox/ManageSafeDepositBox.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/actions/authenticationActions.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/reducers/rootReducer.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/actions/metadataActions.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/service/EnvironmentService.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/Login/Login.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/Modal/Modal.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/Messenger/Messenger.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/Footer/Footer.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/actions/appActions.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/Header/Header.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/SideBar/SideBar.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/SDBMetadata/SDBMetadata.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/actions/versionHistoryBrowserActions.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/SecureDataBrowser/SecureDataBrowser.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/SafeDepositBoxSettings/SafeDepositBoxSettings.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/actions/messengerActions.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/actions/headerActions.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/utils/cmsUtils.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/actions/modalActions.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/constants/cms.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/ApiError/ApiError.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/ConfirmationBox/ConfirmationBox.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/SecureDataVersionsBrowser/SecureDataVersionsBrowser.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/constants/actions.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/actions/manageSafetyDepositBoxActions.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/reducers/headerStateReducer.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/reducers/appReducer.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/reducers/createSDBoxReducer.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/reducers/messengerReducer.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/reducers/metadataReducer.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/reducers/modalReducer.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/reducers/authenticationReducer.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/reducers/versionHistoryBrowserReducer.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/actions/createSDBoxActions.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/LoginUserForm/LoginUserForm.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/Loader/Loader.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/LoginMfaForm/LoginMfaForm.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/reducers/manageSafetyDepositBoxReducer.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/CreateSDBoxForm/CreateSDBoxForm.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/ViewTokenModal/ViewTokenModal.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/AddButton/AddButton.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/FileButton/FileButton.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/SecureData/SecureData.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/SecureFile/SecureFile.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/DeleteSafeDepositBoxForm/DeleteSafeDepositBoxForm.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/EditSDBoxForm/EditSDBoxForm.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/SecureDataForm/SecureDataForm.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/SecureFileForm/SecureFileForm.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/SDBDescriptionField/SDBDescriptionField.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/GroupSelect/GroupsSelect.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/IamPrincipalPermissionsFieldSet/IamPrincipalPermissionsFieldSet.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/UserGroupPermissionsFieldSet/UserGroupPermissionsFieldSet.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/Buttons/Buttons.js",[],[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/RoleSelect/RoleSelect.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/DeleteSafeDepositBoxForm/validator.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/CategorySelect/CategorySelect.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/CreateSDBoxForm/validator.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/components/MfaDeviceSelect/MfaDeviceSelect.js",[],"/Users/TUnde1/workspace/OSS/cerberus/cerberus-dashboard/src/utils/index.js",[]] \ No newline at end of file diff --git a/cerberus-dashboard/build.gradle b/cerberus-dashboard/build.gradle index 4ed89d2bc..f5911f220 100644 --- a/cerberus-dashboard/build.gradle +++ b/cerberus-dashboard/build.gradle @@ -19,7 +19,7 @@ plugins { } node { - version = '10.16.3' + version = '14.16.0' download = true } diff --git a/cerberus-dashboard/package-lock.json b/cerberus-dashboard/package-lock.json index 7e7603c7d..00498d933 100644 --- a/cerberus-dashboard/package-lock.json +++ b/cerberus-dashboard/package-lock.json @@ -13775,9 +13775,9 @@ "dev": true }, "nan": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", - "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" + "version": "2.14.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", + "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==" }, "nanoid": { "version": "3.1.20", diff --git a/cerberus-dashboard/package.json b/cerberus-dashboard/package.json index 14188bd92..00c827d8c 100644 --- a/cerberus-dashboard/package.json +++ b/cerberus-dashboard/package.json @@ -21,6 +21,7 @@ "axios": "^0.21.1", "cookie": "0.4.1", "downloadjs": "1.4.7", + "handlebars": "^4.7.7", "humps": "2.0.1", "lodash": "^4.17.21", "loglevel": "1.7.1", @@ -28,7 +29,6 @@ "prop-types": "^15.7.2", "react": "15.7", "react-addons-create-fragment": "15.6.2", - "react-transition-group": "2.9.0", "react-addons-shallow-compare": "15.6.3", "react-copy-to-clipboard": "5.0.3", "react-dom": "15.7.0", @@ -39,6 +39,7 @@ "react-router-redux": "4.0.8", "react-select": "1.3.0", "react-simple-file-input": "2.1.0", + "react-transition-group": "2.9.0", "redux": "3.7.2", "redux-form": "5.3.6", "redux-logger": "3.0.6", @@ -52,7 +53,7 @@ "eslint-loader": "4.0.2", "eslint-plugin-react": "7.22.0", "estraverse-fb": "1.3.2", - "react-scripts": "4.0.1", + "react-scripts": "4.0.3", "redux-devtools": "3.7.0", "bufferutil": "^4.0.3", "utf-8-validate": "^5.0.4", diff --git a/cerberus-dashboard/src/components/ViewTokenModal/ViewTokenModal.scss b/cerberus-dashboard/src/components/ViewTokenModal/ViewTokenModal.scss index bce02545e..326be5cac 100644 --- a/cerberus-dashboard/src/components/ViewTokenModal/ViewTokenModal.scss +++ b/cerberus-dashboard/src/components/ViewTokenModal/ViewTokenModal.scss @@ -100,14 +100,28 @@ font-size: 16px; font-weight: bold; } - .view-token-modal-data-token-value { + height: 100px; + width: 600px; + overflow-y: scroll; + overflow-wrap: break-word; padding-left: 5px; margin-left: 140px; margin-top: 3px; } - .view-token-modal-data-date-value { + .view-token-modal-data-token-value::-webkit-scrollbar { + width: 7px; + background-color: $snkrs_medium_grey; + } + + .view-token-modal-data-token-value::-webkit-scrollbar-thumb { + height: 18px; + border-radius: 5px; + background-color: $snkrs_dark_grey; + } + + .view-token-modal-data-date-value { padding-left: 5px; margin-left: 17px; margin-top: 3px; diff --git a/cerberus-domain/src/main/java/com/nike/cerberus/domain/AuthTokenAcceptType.java b/cerberus-domain/src/main/java/com/nike/cerberus/domain/AuthTokenAcceptType.java new file mode 100644 index 000000000..895447cae --- /dev/null +++ b/cerberus-domain/src/main/java/com/nike/cerberus/domain/AuthTokenAcceptType.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2021 Nike, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.nike.cerberus.domain; + +/** Enum used to distinguish between JWT and session token */ +public enum AuthTokenAcceptType { + JWT, + SESSION, + ALL +} diff --git a/cerberus-domain/src/main/java/com/nike/cerberus/domain/AuthTokenInfo.java b/cerberus-domain/src/main/java/com/nike/cerberus/domain/AuthTokenInfo.java new file mode 100644 index 000000000..b2d2b3985 --- /dev/null +++ b/cerberus-domain/src/main/java/com/nike/cerberus/domain/AuthTokenInfo.java @@ -0,0 +1,37 @@ +package com.nike.cerberus.domain; + +import java.time.OffsetDateTime; + +public interface AuthTokenInfo { + String getId(); + + AuthTokenInfo setId(String id); + + OffsetDateTime getCreatedTs(); + + AuthTokenInfo setCreatedTs(OffsetDateTime createdTs); + + OffsetDateTime getExpiresTs(); + + AuthTokenInfo setExpiresTs(OffsetDateTime expiresTs); + + String getPrincipal(); + + AuthTokenInfo setPrincipal(String principal); + + String getPrincipalType(); + + AuthTokenInfo setPrincipalType(String principalType); + + Boolean getIsAdmin(); + + AuthTokenInfo setIsAdmin(Boolean admin); + + String getGroups(); + + AuthTokenInfo setGroups(String groups); + + Integer getRefreshCount(); + + AuthTokenInfo setRefreshCount(Integer refreshCount); +} diff --git a/cerberus-domain/src/main/java/com/nike/cerberus/domain/AuthTokenIssueType.java b/cerberus-domain/src/main/java/com/nike/cerberus/domain/AuthTokenIssueType.java new file mode 100644 index 000000000..7e6a19c77 --- /dev/null +++ b/cerberus-domain/src/main/java/com/nike/cerberus/domain/AuthTokenIssueType.java @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2021 Nike, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.nike.cerberus.domain; + +/** Enum used to distinguish between JWT and session token */ +public enum AuthTokenIssueType { + JWT, + SESSION +} diff --git a/cerberus-domain/src/main/java/com/nike/cerberus/domain/CerberusAuthToken.java b/cerberus-domain/src/main/java/com/nike/cerberus/domain/CerberusAuthToken.java index 8c9c24736..de69d69e3 100644 --- a/cerberus-domain/src/main/java/com/nike/cerberus/domain/CerberusAuthToken.java +++ b/cerberus-domain/src/main/java/com/nike/cerberus/domain/CerberusAuthToken.java @@ -1,11 +1,11 @@ /* - * Copyright (c) 2020 Nike, inc. + * Copyright (c) 2017 Nike, Inc. * - * Licensed under the Apache License, Version 2.0 (the "License") + * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -19,24 +19,96 @@ import com.nike.cerberus.PrincipalType; import java.io.Serializable; import java.time.OffsetDateTime; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -@Data -@Builder -@AllArgsConstructor -@NoArgsConstructor +import lombok.Getter; + public class CerberusAuthToken implements Serializable { + private static final long serialVersionUID = 703097175899198451L; - private String token; - private OffsetDateTime created; - private OffsetDateTime expires; - private String principal; - private PrincipalType principalType; - private boolean isAdmin; - private String groups; - private int refreshCount; + @Getter private String token; + @Getter private OffsetDateTime created; + @Getter private OffsetDateTime expires; + @Getter private String principal; + @Getter private PrincipalType principalType; + @Getter private boolean isAdmin; + @Getter private String groups; + @Getter private int refreshCount; + @Getter private String id; + + public static final class Builder { + private String token; + private OffsetDateTime created; + private OffsetDateTime expires; + private String principal; + private PrincipalType principalType; + private boolean isAdmin; + private String groups; + private int refreshCount; + private String id; + + private Builder() {} + + public static Builder create() { + return new Builder(); + } + + public Builder withToken(String token) { + this.token = token; + return this; + } + + public Builder withCreated(OffsetDateTime created) { + this.created = created; + return this; + } + + public Builder withExpires(OffsetDateTime expires) { + this.expires = expires; + return this; + } + + public Builder withPrincipal(String principal) { + this.principal = principal; + return this; + } + + public Builder withPrincipalType(PrincipalType principalType) { + this.principalType = principalType; + return this; + } + + public Builder withIsAdmin(boolean isAdmin) { + this.isAdmin = isAdmin; + return this; + } + + public Builder withGroups(String groups) { + this.groups = groups; + return this; + } + + public Builder withRefreshCount(int refreshCount) { + this.refreshCount = refreshCount; + return this; + } + + public Builder withId(String id) { + this.id = id; + return this; + } + + public CerberusAuthToken build() { + CerberusAuthToken generateTokenResult = new CerberusAuthToken(); + generateTokenResult.refreshCount = this.refreshCount; + generateTokenResult.principal = this.principal; + generateTokenResult.token = this.token; + generateTokenResult.isAdmin = this.isAdmin; + generateTokenResult.expires = this.expires; + generateTokenResult.groups = this.groups; + generateTokenResult.principalType = this.principalType; + generateTokenResult.created = this.created; + generateTokenResult.id = this.id; + return generateTokenResult; + } + } } diff --git a/cerberus-domain/src/test/java/com/nike/cerberus/domain/DomainPojoTest.java b/cerberus-domain/src/test/java/com/nike/cerberus/domain/DomainPojoTest.java index 698627361..33585da85 100644 --- a/cerberus-domain/src/test/java/com/nike/cerberus/domain/DomainPojoTest.java +++ b/cerberus-domain/src/test/java/com/nike/cerberus/domain/DomainPojoTest.java @@ -36,8 +36,7 @@ public void test_pojo_structure_and_behavior() { List pojoClasses = PojoClassFactory.getPojoClasses("com.nike.cerberus.domain"); pojoClasses.remove(PojoClassFactory.getPojoClass(CerberusAuthToken.class)); - pojoClasses.remove( - PojoClassFactory.getPojoClass(CerberusAuthToken.CerberusAuthTokenBuilder.class)); + pojoClasses.remove(PojoClassFactory.getPojoClass(CerberusAuthToken.Builder.class)); pojoClasses.remove(PojoClassFactory.getPojoClass(VaultStyleErrorResponse.Builder.class)); pojoClasses.remove(PojoClassFactory.getPojoClass(IamPrincipalPermission.Builder.class)); pojoClasses.remove(PojoClassFactory.getPojoClass(UserGroupPermission.Builder.class)); diff --git a/cerberus-web/build.gradle b/cerberus-web/build.gradle index e8a0a2f4b..30f7911ea 100644 --- a/cerberus-web/build.gradle +++ b/cerberus-web/build.gradle @@ -64,9 +64,17 @@ dependencies { implementation "com.amazonaws:aws-java-sdk-core:${versions.awsSdkVersion}" implementation "com.amazonaws:aws-java-sdk-kms:${versions.awsSdkVersion}" implementation "com.amazonaws:aws-java-sdk-sts:${versions.awsSdkVersion}" + implementation "com.amazonaws:aws-java-sdk-s3:${versions.awsSdkVersion}" implementation "com.amazonaws:aws-java-sdk-secretsmanager:${versions.awsSdkVersion}" implementation 'com.amazonaws:aws-encryption-sdk-java:1.6.1' + + // JWT + implementation "io.jsonwebtoken:jjwt-api:0.10.8" + implementation "io.jsonwebtoken:jjwt-impl:0.10.8" + implementation "io.jsonwebtoken:jjwt-jackson:0.10.8" + + //dist tracing implementation 'com.nike.wingtips:wingtips-spring-boot:0.23.1' diff --git a/cerberus-web/src/main/java/com/nike/cerberus/controller/authentication/RevokeAuthenticationController.java b/cerberus-web/src/main/java/com/nike/cerberus/controller/authentication/RevokeAuthenticationController.java index 685ddd489..4e6ac88c1 100644 --- a/cerberus-web/src/main/java/com/nike/cerberus/controller/authentication/RevokeAuthenticationController.java +++ b/cerberus-web/src/main/java/com/nike/cerberus/controller/authentication/RevokeAuthenticationController.java @@ -40,6 +40,6 @@ public RevokeAuthenticationController(AuthenticationService authenticationServic @RequestMapping(method = DELETE) public void revokeAuthentication(Authentication authentication) { var cerberusPrincipal = (CerberusPrincipal) authentication; - authenticationService.revoke(cerberusPrincipal.getToken()); + authenticationService.revoke(cerberusPrincipal, cerberusPrincipal.getTokenExpires()); } } diff --git a/cerberus-web/src/main/java/com/nike/cerberus/dao/AuthTokenDao.java b/cerberus-web/src/main/java/com/nike/cerberus/dao/AuthTokenDao.java index f2014d3db..c4acb7c60 100644 --- a/cerberus-web/src/main/java/com/nike/cerberus/dao/AuthTokenDao.java +++ b/cerberus-web/src/main/java/com/nike/cerberus/dao/AuthTokenDao.java @@ -41,7 +41,12 @@ public int createAuthToken(AuthTokenRecord record) { } public Optional getAuthTokenFromHash(String hash) { - return Optional.ofNullable(authTokenMapper.getAuthTokenFromHash(hash)); + Optional authTokenRecord = + Optional.ofNullable(authTokenMapper.getAuthTokenFromHash(hash)); + if (authTokenRecord.isEmpty()) { + logger.warn("Failed to get auth token from hash"); + } + return authTokenRecord; } public void deleteAuthTokenFromHash(String hash) { diff --git a/cerberus-web/src/main/java/com/nike/cerberus/dao/JwtBlocklistDao.java b/cerberus-web/src/main/java/com/nike/cerberus/dao/JwtBlocklistDao.java new file mode 100644 index 000000000..64c14f057 --- /dev/null +++ b/cerberus-web/src/main/java/com/nike/cerberus/dao/JwtBlocklistDao.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2021 Nike, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.nike.cerberus.dao; + +import com.nike.cerberus.mapper.JwtBlocklistMapper; +import com.nike.cerberus.record.JwtBlocklistRecord; +import java.util.HashSet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class JwtBlocklistDao { + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + private final JwtBlocklistMapper jwtBlocklistMapper; + + @Autowired + public JwtBlocklistDao(JwtBlocklistMapper jwtBlocklistMapper) { + this.jwtBlocklistMapper = jwtBlocklistMapper; + } + + public HashSet getBlocklist() { + return jwtBlocklistMapper.getBlocklist(); + } + + public int addToBlocklist(JwtBlocklistRecord jwtBlocklistRecord) { + return jwtBlocklistMapper.addToBlocklist(jwtBlocklistRecord); + } + + public int deleteExpiredTokens() { + return jwtBlocklistMapper.deleteExpiredTokens(); + } +} diff --git a/cerberus-web/src/main/java/com/nike/cerberus/jobs/JwtBlocklistCleanUpJob.java b/cerberus-web/src/main/java/com/nike/cerberus/jobs/JwtBlocklistCleanUpJob.java new file mode 100644 index 000000000..7c2a02e0d --- /dev/null +++ b/cerberus-web/src/main/java/com/nike/cerberus/jobs/JwtBlocklistCleanUpJob.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2021 Nike, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.nike.cerberus.jobs; + +import com.nike.cerberus.service.JwtService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +/** Periodically clean up JWT blocklist. */ +@Slf4j +@ConditionalOnProperty("cerberus.jobs.jwtBlocklistCleanUpJob.enabled") +@Component +public class JwtBlocklistCleanUpJob extends LockingJob { + + private final JwtService jwtService; + + @Autowired + public JwtBlocklistCleanUpJob(JwtService jwtService) { + this.jwtService = jwtService; + } + + @Override + @Scheduled(cron = "${cerberus.jobs.jwtBlocklistCleanUpJob.cronExpression}") + public void execute() { + super.execute(); + } + + @Override + protected void executeLockableCode() { + int numberOfDeletedTokens = jwtService.deleteExpiredTokens(); + log.info("Deleted {} JWT blocklist entries", numberOfDeletedTokens); + } +} diff --git a/cerberus-web/src/main/java/com/nike/cerberus/jobs/JwtBlocklistRefreshJob.java b/cerberus-web/src/main/java/com/nike/cerberus/jobs/JwtBlocklistRefreshJob.java new file mode 100644 index 000000000..e6f7a4b88 --- /dev/null +++ b/cerberus-web/src/main/java/com/nike/cerberus/jobs/JwtBlocklistRefreshJob.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2021 Nike, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.nike.cerberus.jobs; + +import com.nike.cerberus.service.JwtService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +/** Periodically refresh JWT blocklist. */ +@Slf4j +@ConditionalOnProperty("cerberus.jobs.jwtBlocklistRefreshJob.enabled") +@Component +public class JwtBlocklistRefreshJob { + + private final JwtService jwtService; + + @Autowired + public JwtBlocklistRefreshJob(JwtService jwtService) { + this.jwtService = jwtService; + } + + @Scheduled(cron = "${cerberus.jobs.jwtBlocklistRefreshJob.cronExpression}") + public void execute() { + // This would be too spammy as an info message + log.debug("Running JWT blocklist refresh job"); + jwtService.refreshBlocklist(); + } +} diff --git a/cerberus-web/src/main/java/com/nike/cerberus/jobs/JwtSecretRefreshJob.java b/cerberus-web/src/main/java/com/nike/cerberus/jobs/JwtSecretRefreshJob.java new file mode 100644 index 000000000..77aa5ee0e --- /dev/null +++ b/cerberus-web/src/main/java/com/nike/cerberus/jobs/JwtSecretRefreshJob.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2021 Nike, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package com.nike.cerberus.jobs; + +import com.nike.cerberus.service.JwtService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +/** Periodically refresh JWT signing keys. */ +@Slf4j +@ConditionalOnProperty("cerberus.jobs.jwtSecretRefreshJob.enabled") +@Component +public class JwtSecretRefreshJob { + + private final JwtService jwtService; + + @Autowired + public JwtSecretRefreshJob(JwtService jwtService) { + this.jwtService = jwtService; + } + + @Scheduled(cron = "${cerberus.jobs.jwtSecretRefreshJob.cronExpression}") + public void execute() { + log.debug("Running JWT secret refresh job"); + jwtService.refreshKeys(); + } +} diff --git a/cerberus-web/src/main/java/com/nike/cerberus/jwt/CerberusJwtClaims.java b/cerberus-web/src/main/java/com/nike/cerberus/jwt/CerberusJwtClaims.java new file mode 100644 index 000000000..f6d64129f --- /dev/null +++ b/cerberus-web/src/main/java/com/nike/cerberus/jwt/CerberusJwtClaims.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2021 Nike, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.nike.cerberus.jwt; + +import com.nike.cerberus.domain.AuthTokenInfo; +import java.time.OffsetDateTime; +import lombok.Getter; + +public class CerberusJwtClaims implements AuthTokenInfo { + + @Getter private String id; + @Getter private OffsetDateTime createdTs; + @Getter private OffsetDateTime expiresTs; + @Getter private String principal; + @Getter private String principalType; + @Getter private Boolean isAdmin; + @Getter private String groups; + @Getter private Integer refreshCount; + + public CerberusJwtClaims setId(String id) { + this.id = id; + return this; + } + + public CerberusJwtClaims setCreatedTs(OffsetDateTime createdTs) { + this.createdTs = createdTs; + return this; + } + + public CerberusJwtClaims setExpiresTs(OffsetDateTime expiresTs) { + this.expiresTs = expiresTs; + return this; + } + + public CerberusJwtClaims setPrincipal(String principal) { + this.principal = principal; + return this; + } + + public CerberusJwtClaims setPrincipalType(String principalType) { + this.principalType = principalType; + return this; + } + + public CerberusJwtClaims setIsAdmin(Boolean admin) { + isAdmin = admin; + return this; + } + + public CerberusJwtClaims setGroups(String groups) { + this.groups = groups; + return this; + } + + public CerberusJwtClaims setRefreshCount(Integer refreshCount) { + this.refreshCount = refreshCount; + return this; + } +} diff --git a/cerberus-web/src/main/java/com/nike/cerberus/jwt/CerberusJwtKeySpec.java b/cerberus-web/src/main/java/com/nike/cerberus/jwt/CerberusJwtKeySpec.java new file mode 100644 index 000000000..40a14c37b --- /dev/null +++ b/cerberus-web/src/main/java/com/nike/cerberus/jwt/CerberusJwtKeySpec.java @@ -0,0 +1,38 @@ +package com.nike.cerberus.jwt; + +import java.util.Objects; +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; + +public class CerberusJwtKeySpec extends SecretKeySpec { + // todo maybe implement destroyable + private String kid; + + public CerberusJwtKeySpec(byte[] key, String algorithm, String kid) { + super(key, algorithm); + this.kid = kid; + } + + public CerberusJwtKeySpec(SecretKey secretKey, String kid) { + super(secretKey.getEncoded(), secretKey.getAlgorithm()); + this.kid = kid; + } + + public String getKid() { + return kid; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + if (!super.equals(o)) return false; + CerberusJwtKeySpec keySpec = (CerberusJwtKeySpec) o; + return kid.equals(keySpec.kid); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), kid); + } +} diff --git a/cerberus-web/src/main/java/com/nike/cerberus/jwt/CerberusSigningKeyResolver.java b/cerberus-web/src/main/java/com/nike/cerberus/jwt/CerberusSigningKeyResolver.java new file mode 100644 index 000000000..83108d8e0 --- /dev/null +++ b/cerberus-web/src/main/java/com/nike/cerberus/jwt/CerberusSigningKeyResolver.java @@ -0,0 +1,300 @@ +package com.nike.cerberus.jwt; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.nike.cerberus.service.ConfigService; +import com.nike.cerberus.util.UuidSupplier; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwsHeader; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.SigningKeyResolverAdapter; +import io.jsonwebtoken.security.Keys; +import java.io.IOException; +import java.security.Key; +import java.util.Base64; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; +import javax.crypto.SecretKey; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +/** + * A subclass of {@link SigningKeyResolverAdapter} that resolves the key used for JWT signing and + * signature validation + */ +@Component +public class CerberusSigningKeyResolver extends SigningKeyResolverAdapter { + + private ConfigService configService; + private final ObjectMapper objectMapper; + private CerberusJwtKeySpec signingKey; + private Map keyMap; + private boolean checkKeyRotation; + private long nextRotationTs; + private String nextKeyId; + + // Hardcoding these for now + private static final String DEFAULT_ALGORITHM = "HmacSHA512"; + private static final String DEFAULT_JWT_ALG_HEADER = "HS512"; + private static final int DEFAULT_MINIMUM_KEY_LENGTH_IN_BYTES = 512 / 8; + + protected final Logger log = LoggerFactory.getLogger(this.getClass()); + + @Autowired + public CerberusSigningKeyResolver( + JwtServiceOptionalPropertyHolder jwtServiceOptionalPropertyHolder, + ObjectMapper objectMapper, + Optional configService, + @Value("${cerberus.auth.jwt.secret.local.autoGenerate}") boolean autoGenerate, + @Value("${cerberus.auth.jwt.secret.local.enabled}") boolean jwtLocalEnabled, + UuidSupplier uuidSupplier) { + this.configService = configService.orElse(null); + this.objectMapper = objectMapper; + + // Override key with properties, useful for local development + if (jwtLocalEnabled) { + if (autoGenerate) { + log.info("Auto generating JWT secret for local development"); + SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.forName(DEFAULT_JWT_ALG_HEADER)); + this.signingKey = new CerberusJwtKeySpec(key, uuidSupplier.get()); + } else { + log.info("Using JWT secret from properties"); + if (!StringUtils.isBlank(jwtServiceOptionalPropertyHolder.jwtSecretLocalMaterial) + && !StringUtils.isBlank(jwtServiceOptionalPropertyHolder.jwtSecretLocalKeyId)) { + byte[] key = + Base64.getDecoder().decode(jwtServiceOptionalPropertyHolder.jwtSecretLocalMaterial); + this.signingKey = + new CerberusJwtKeySpec( + key, DEFAULT_ALGORITHM, jwtServiceOptionalPropertyHolder.jwtSecretLocalKeyId); + } else { + throw new IllegalArgumentException( + "Invalid JWT config. To resolve, either set " + + "cms.auth.jwt.secret.local.autoGenerate=true or provide both cms.auth.jwt.secret.local.material" + + " and cms.auth.jwt.secret.local.kid"); + } + } + rotateKeyMap(signingKey); + } else { + log.info("Initializing JWT key resolver using Jwt Secret from S3 bucket"); + refresh(); + } + } + + /** + * This 'holder' class allows optional injection of Cerberus JWT-specific properties that are only + * necessary for local development. + */ + @Component + static class JwtServiceOptionalPropertyHolder { + @Value("${cms.auth.jwt.secret.local.material: #{null}}") + String jwtSecretLocalMaterial; + + @Value("${cms.auth.jwt.secret.local.kid: #{null}}") + String jwtSecretLocalKeyId; + } + + @Override + public Key resolveSigningKey(JwsHeader jwsHeader, Claims claims) { + // Rejects non HS512 token + if (!StringUtils.equals(DEFAULT_JWT_ALG_HEADER, jwsHeader.getAlgorithm())) { + throw new IllegalArgumentException("Algorithm not supported"); + } + String keyId = jwsHeader.getKeyId(); + Key key = lookupVerificationKey(keyId); + + return key; + } + + /** + * Return the signing key that should be used to sign JWT. The signing key is defined as the + * "newest active key" i.e. key with the biggest effectiveTs value and effectiveTs before now. + * + * @return The signing key + */ + public CerberusJwtKeySpec resolveSigningKey() { + if (checkKeyRotation) { + rotateSigningKey(); + return signingKey; + } else { + return signingKey; + } + } + + /** Poll for JWT config and update key map with new data */ + public void refresh() { + JwtSecretData jwtSecretData = getJwtSecretData(); + + rotateKeyMap(jwtSecretData); + setSigningKey(jwtSecretData); + } + + /** + * Poll for JWT config and validate new data + * + * @return JWT config + */ + protected JwtSecretData getJwtSecretData() { + String jwtSecretsString = configService.getJwtSecrets(); + try { + JwtSecretData jwtSecretData = objectMapper.readValue(jwtSecretsString, JwtSecretData.class); + validateJwtSecretData(jwtSecretData); + return jwtSecretData; + } catch (IOException e) { + log.error("IOException encountered during deserialization of jwt secret data"); + throw new RuntimeException(e); + } + } + + /** + * Validate {@link JwtSecretData}. Validates required fields and rejects weak keys. + * + * @param jwtSecretData JWT config + */ + protected void validateJwtSecretData(JwtSecretData jwtSecretData) { + if (jwtSecretData == null || jwtSecretData.getJwtSecrets() == null) { + throw new IllegalArgumentException("JWT secret data cannot be null"); + } + if (jwtSecretData.getJwtSecrets().isEmpty()) { + throw new IllegalArgumentException("JWT secret data cannot be empty"); + } + + long minEffectiveTs = 0; + + for (JwtSecret jwtSecret : jwtSecretData.getJwtSecrets()) { + if (jwtSecret.getSecret() == null) { + throw new IllegalArgumentException("JWT secret cannot be null"); + } + if (Base64.getDecoder().decode(jwtSecret.getSecret()).length + < DEFAULT_MINIMUM_KEY_LENGTH_IN_BYTES) { + throw new IllegalArgumentException( + "JWT secret does NOT meet minimum length requirement of " + + DEFAULT_MINIMUM_KEY_LENGTH_IN_BYTES); + } + if (StringUtils.isBlank(jwtSecret.getId())) { + throw new IllegalArgumentException("JWT secret key ID cannot be empty"); + } + minEffectiveTs = Math.min(minEffectiveTs, jwtSecret.getEffectiveTs()); + } + + long now = System.currentTimeMillis(); + if (now < minEffectiveTs) { + // Prevents rotation or start up if no key is active + throw new IllegalArgumentException("Requires at least 1 active JWT secret"); + } + } + + /** + * Set the signing key that should be used to sign JWT and the next signing key in line. The + * signing key is defined as the "newest active key" i.e. key with the biggest effectiveTs value + * and effectiveTs before now. + * + * @param jwtSecretData JWT config + */ + protected void setSigningKey(JwtSecretData jwtSecretData) { + // Find the active key + long now = System.currentTimeMillis(); + String currentKeyId = getSigningKeyId(jwtSecretData, now); + signingKey = keyMap.get(currentKeyId); + + // Find the next key + List futureJwtSecrets = getFutureJwtSecrets(jwtSecretData, now); + + // Set up rotation + if (!futureJwtSecrets.isEmpty()) { + JwtSecret jwtSecret = futureJwtSecrets.get(0); + checkKeyRotation = true; + nextRotationTs = jwtSecret.getEffectiveTs(); + nextKeyId = jwtSecret.getId(); + } else { + checkKeyRotation = false; + } + } + + /** + * Get future signing keys i.e. keys with effectiveTs after now. + * + * @param jwtSecretData JWT config + * @param now Timestamp of now + * @return Future signing keys + */ + protected List getFutureJwtSecrets(JwtSecretData jwtSecretData, long now) { + return jwtSecretData.getJwtSecrets().stream() + .filter(secretData -> secretData.getEffectiveTs() > now) + .sorted( + (secretData1, secretData2) -> + secretData1.getEffectiveTs() - secretData2.getEffectiveTs() < 0 ? -1 : 1) + // this puts older keys in the front of the list + .collect(Collectors.toList()); + } + + /** + * Get the ID of signing key that should be used to sign JWT. The signing key is defined as the + * "newest active key" i.e. key with the biggest effectiveTs value and effectiveTs before now. + * + * @param jwtSecretData JWT config + * @param now Timestamp of now in millisecond + * @return ID of the signing key + */ + protected String getSigningKeyId(JwtSecretData jwtSecretData, long now) { + List sortedJwtSecrets = + jwtSecretData.getJwtSecrets().stream() + .filter(secretData -> secretData.getEffectiveTs() <= now) + .sorted( + (secretData1, secretData2) -> + secretData1.getEffectiveTs() - secretData2.getEffectiveTs() > 0 + ? -1 + : 1) // this puts newer keys in the front of the list + .collect(Collectors.toList()); + String currentKeyId = sortedJwtSecrets.get(0).getId(); + return currentKeyId; + } + + private void rotateKeyMap(JwtSecretData jwtSecretData) { + ConcurrentHashMap keyMap = new ConcurrentHashMap<>(); + for (JwtSecret jwtSecret : jwtSecretData.getJwtSecrets()) { + CerberusJwtKeySpec keySpec = + new CerberusJwtKeySpec( + Base64.getDecoder().decode(jwtSecret.getSecret()), + DEFAULT_ALGORITHM, + jwtSecret.getId()); + keyMap.put(jwtSecret.getId(), keySpec); + } + this.keyMap = keyMap; + } + + private void rotateKeyMap(CerberusJwtKeySpec cerberusJwtKeySpec) { + ConcurrentHashMap keyMap = new ConcurrentHashMap<>(); + keyMap.put(cerberusJwtKeySpec.getKid(), cerberusJwtKeySpec); + this.keyMap = keyMap; + } + + private Key lookupVerificationKey(String keyId) { + if (StringUtils.isBlank(keyId)) { + throw new IllegalArgumentException("Key ID cannot be empty"); + } + try { + CerberusJwtKeySpec keySpec = keyMap.get(keyId); + if (keySpec == null) { + throw new IllegalArgumentException("The key ID " + keyId + " is invalid or expired"); + } + + return keySpec; + } catch (NullPointerException e) { + throw new IllegalArgumentException("The key ID " + keyId + " is either invalid or expired"); + } + } + + private void rotateSigningKey() { + long now = System.currentTimeMillis(); + if (now >= nextRotationTs) { + this.signingKey = keyMap.get(nextKeyId); + } + checkKeyRotation = false; + } +} diff --git a/cerberus-web/src/main/java/com/nike/cerberus/jwt/JwtSecret.java b/cerberus-web/src/main/java/com/nike/cerberus/jwt/JwtSecret.java new file mode 100644 index 000000000..cc45f1d18 --- /dev/null +++ b/cerberus-web/src/main/java/com/nike/cerberus/jwt/JwtSecret.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2021 Nike, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.nike.cerberus.jwt; + +public class JwtSecret { + private String id; + + private String secret; + + private String algorithm; + + private long effectiveTs; + + private long createdTs; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getSecret() { + return secret; + } + + public void setSecret(String secret) { + this.secret = secret; + } + + public long getEffectiveTs() { + return effectiveTs; + } + + public void setEffectiveTs(long effectiveTs) { + this.effectiveTs = effectiveTs; + } + + public long getCreatedTs() { + return createdTs; + } + + public void setCreatedTs(long createdTs) { + this.createdTs = createdTs; + } + + public String getAlgorithm() { + return algorithm; + } + + public void setAlgorithm(String algorithm) { + this.algorithm = algorithm; + } +} diff --git a/cerberus-web/src/main/java/com/nike/cerberus/jwt/JwtSecretData.java b/cerberus-web/src/main/java/com/nike/cerberus/jwt/JwtSecretData.java new file mode 100644 index 000000000..6e36191cf --- /dev/null +++ b/cerberus-web/src/main/java/com/nike/cerberus/jwt/JwtSecretData.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2021 Nike, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.nike.cerberus.jwt; + +import java.util.LinkedList; +import lombok.Data; + +/** A POJO that represents the JWT config */ +@Data +public class JwtSecretData { + private LinkedList jwtSecrets = new LinkedList<>(); +} diff --git a/cerberus-web/src/main/java/com/nike/cerberus/mapper/JwtBlocklistMapper.java b/cerberus-web/src/main/java/com/nike/cerberus/mapper/JwtBlocklistMapper.java new file mode 100644 index 000000000..b8683437b --- /dev/null +++ b/cerberus-web/src/main/java/com/nike/cerberus/mapper/JwtBlocklistMapper.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2021 Nike, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.nike.cerberus.mapper; + +import com.nike.cerberus.record.JwtBlocklistRecord; +import java.util.HashSet; +import org.apache.ibatis.annotations.Param; + +public interface JwtBlocklistMapper { + + HashSet getBlocklist(); + + int addToBlocklist(@Param("record") JwtBlocklistRecord record); + + int deleteExpiredTokens(); +} diff --git a/cerberus-web/src/main/java/com/nike/cerberus/record/AuthTokenRecord.java b/cerberus-web/src/main/java/com/nike/cerberus/record/AuthTokenRecord.java index b42844e29..cbe2a7662 100644 --- a/cerberus-web/src/main/java/com/nike/cerberus/record/AuthTokenRecord.java +++ b/cerberus-web/src/main/java/com/nike/cerberus/record/AuthTokenRecord.java @@ -16,9 +16,10 @@ package com.nike.cerberus.record; +import com.nike.cerberus.domain.AuthTokenInfo; import java.time.OffsetDateTime; -public class AuthTokenRecord { +public class AuthTokenRecord implements AuthTokenInfo { private String id; diff --git a/cerberus-web/src/main/java/com/nike/cerberus/record/JwtBlocklistRecord.java b/cerberus-web/src/main/java/com/nike/cerberus/record/JwtBlocklistRecord.java new file mode 100644 index 000000000..e35274979 --- /dev/null +++ b/cerberus-web/src/main/java/com/nike/cerberus/record/JwtBlocklistRecord.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2021 Nike, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.nike.cerberus.record; + +import java.time.OffsetDateTime; +import lombok.Getter; + +public class JwtBlocklistRecord { + + @Getter private String id; + @Getter private OffsetDateTime expiresTs; + + public JwtBlocklistRecord setId(String id) { + this.id = id; + return this; + } + + public JwtBlocklistRecord setExpiresTs(OffsetDateTime expiresTs) { + this.expiresTs = expiresTs; + return this; + } +} diff --git a/cerberus-web/src/main/java/com/nike/cerberus/security/JwtTokenFilter.java b/cerberus-web/src/main/java/com/nike/cerberus/security/JwtTokenFilter.java new file mode 100644 index 000000000..03844d2f6 --- /dev/null +++ b/cerberus-web/src/main/java/com/nike/cerberus/security/JwtTokenFilter.java @@ -0,0 +1,28 @@ +package com.nike.cerberus.security; + +import static com.nike.cerberus.security.WebSecurityConfiguration.HEADER_X_CERBERUS_TOKEN; +import static com.nike.cerberus.security.WebSecurityConfiguration.LEGACY_AUTH_TOKN_HEADER; + +import com.nike.cerberus.service.AuthTokenService; +import java.util.Optional; +import javax.servlet.http.HttpServletRequest; +import org.springframework.security.web.util.matcher.RequestMatcher; + +public class JwtTokenFilter extends CerberusAuthenticationFilter { + + private final AuthTokenService authTokenService; + + public JwtTokenFilter( + RequestMatcher requiresAuthenticationRequestMatcher, AuthTokenService authTokenService) { + super(requiresAuthenticationRequestMatcher); + this.authTokenService = authTokenService; + } + + @Override + Optional extractCerberusPrincipalFromRequest(HttpServletRequest request) { + return Optional.ofNullable(request.getHeader(HEADER_X_CERBERUS_TOKEN)) + .or(() -> Optional.ofNullable(request.getHeader(LEGACY_AUTH_TOKN_HEADER))) + // If the token is present then use the auth service to map it to a Cerberus Principal + .flatMap(token -> authTokenService.getCerberusAuthToken(token).map(CerberusPrincipal::new)); + } +} diff --git a/cerberus-web/src/main/java/com/nike/cerberus/security/WebSecurityConfiguration.java b/cerberus-web/src/main/java/com/nike/cerberus/security/WebSecurityConfiguration.java index 51b59813c..c5aee2c65 100644 --- a/cerberus-web/src/main/java/com/nike/cerberus/security/WebSecurityConfiguration.java +++ b/cerberus-web/src/main/java/com/nike/cerberus/security/WebSecurityConfiguration.java @@ -98,6 +98,8 @@ protected void configure(HttpSecurity http) throws Exception { new DatabaseTokenAuthenticationProcessingFilter( authTokenService, requestDoesNotRequireAuthMatcher); + var jwtFilter = new JwtTokenFilter(requestDoesNotRequireAuthMatcher, authTokenService); + // Disable CSRF (cross site request forgery) http.csrf().disable(); @@ -117,5 +119,7 @@ protected void configure(HttpSecurity http) throws Exception { // Add the auth filters http.addFilterBefore(dbTokenFilter, UsernamePasswordAuthenticationFilter.class); + + http.addFilterBefore(jwtFilter, dbTokenFilter.getClass()); } } diff --git a/cerberus-web/src/main/java/com/nike/cerberus/service/AuthTokenService.java b/cerberus-web/src/main/java/com/nike/cerberus/service/AuthTokenService.java index 16d2cbf10..255210293 100644 --- a/cerberus-web/src/main/java/com/nike/cerberus/service/AuthTokenService.java +++ b/cerberus-web/src/main/java/com/nike/cerberus/service/AuthTokenService.java @@ -19,23 +19,42 @@ import static com.google.common.base.Preconditions.checkArgument; import static org.springframework.transaction.annotation.Isolation.READ_UNCOMMITTED; +import com.nike.backstopper.exception.ApiException; import com.nike.cerberus.PrincipalType; import com.nike.cerberus.dao.AuthTokenDao; +import com.nike.cerberus.domain.AuthTokenAcceptType; +import com.nike.cerberus.domain.AuthTokenInfo; +import com.nike.cerberus.domain.AuthTokenIssueType; import com.nike.cerberus.domain.CerberusAuthToken; +import com.nike.cerberus.error.AuthTokenTooLongException; +import com.nike.cerberus.error.DefaultApiError; +import com.nike.cerberus.jwt.CerberusJwtClaims; import com.nike.cerberus.record.AuthTokenRecord; +import com.nike.cerberus.security.CerberusPrincipal; import com.nike.cerberus.util.AuthTokenGenerator; +import com.nike.cerberus.util.CustomApiError; import com.nike.cerberus.util.DateTimeSupplier; import com.nike.cerberus.util.TokenHasher; import com.nike.cerberus.util.UuidSupplier; import java.time.OffsetDateTime; import java.util.Optional; +import lombok.Data; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; +@Data +@Component +@ConfigurationProperties("cerberus.auth.token") +class JwtFeatureFlags { + private AuthTokenIssueType issueType; + private AuthTokenAcceptType acceptType; +} + /** Service for handling authentication tokens. */ @Component public class AuthTokenService { @@ -47,6 +66,9 @@ public class AuthTokenService { private final AuthTokenGenerator authTokenGenerator; private final AuthTokenDao authTokenDao; private final DateTimeSupplier dateTimeSupplier; + private final JwtService jwtService; + + private final JwtFeatureFlags tokenFlag; @Autowired public AuthTokenService( @@ -54,13 +76,17 @@ public AuthTokenService( TokenHasher tokenHasher, AuthTokenGenerator authTokenGenerator, AuthTokenDao authTokenDao, - DateTimeSupplier dateTimeSupplier) { + DateTimeSupplier dateTimeSupplier, + JwtService jwtService, + JwtFeatureFlags tokenFlag) { this.uuidSupplier = uuidSupplier; this.tokenHasher = tokenHasher; this.authTokenGenerator = authTokenGenerator; this.authTokenDao = authTokenDao; this.dateTimeSupplier = dateTimeSupplier; + this.jwtService = jwtService; + this.tokenFlag = tokenFlag; } @Transactional @@ -75,13 +101,54 @@ public CerberusAuthToken generateToken( checkArgument(StringUtils.isNotBlank(principal), "The principal must be set and not empty"); String id = uuidSupplier.get(); - String token = authTokenGenerator.generateSecureToken(); OffsetDateTime now = dateTimeSupplier.get(); - AuthTokenRecord tokenRecord = - new AuthTokenRecord() + switch (tokenFlag.getIssueType()) { + case JWT: + try { + return getCerberusAuthTokenFromJwt( + principal, principalType, isAdmin, groups, ttlInMinutes, refreshCount, id, now); + } catch (AuthTokenTooLongException e) { + final String msg = e.getMessage(); + logger.info(msg); + + if (tokenFlag.getAcceptType() == AuthTokenAcceptType.ALL) { + return getCerberusAuthTokenFromSession( + principal, principalType, isAdmin, groups, ttlInMinutes, refreshCount, id, now); + } + throw ApiException.newBuilder() + .withApiErrors( + CustomApiError.createCustomApiError(DefaultApiError.AUTH_TOKEN_TOO_LONG, msg)) + .withExceptionMessage(msg) + .build(); + } + case SESSION: + return getCerberusAuthTokenFromSession( + principal, principalType, isAdmin, groups, ttlInMinutes, refreshCount, id, now); + default: + throw ApiException.newBuilder() + .withApiErrors(DefaultApiError.INTERNAL_SERVER_ERROR) + .build(); + } + } + + private CerberusAuthToken getCerberusAuthTokenFromJwt( + String principal, + PrincipalType principalType, + boolean isAdmin, + String groups, + long ttlInMinutes, + int refreshCount, + String id, + OffsetDateTime now) + throws AuthTokenTooLongException { + + AuthTokenInfo authTokenInfo; + String token; + + authTokenInfo = + new CerberusJwtClaims() .setId(id) - .setTokenHash(tokenHasher.hashToken(token)) .setCreatedTs(now) .setExpiresTs(now.plusMinutes(ttlInMinutes)) .setPrincipal(principal) @@ -89,29 +156,67 @@ public CerberusAuthToken generateToken( .setIsAdmin(isAdmin) .setGroups(groups) .setRefreshCount(refreshCount); + token = jwtService.generateJwtToken((CerberusJwtClaims) authTokenInfo); + return getCerberusAuthTokenFromRecord(token, authTokenInfo); + } - authTokenDao.createAuthToken(tokenRecord); + private CerberusAuthToken getCerberusAuthTokenFromSession( + String principal, + PrincipalType principalType, + boolean isAdmin, + String groups, + long ttlInMinutes, + int refreshCount, + String id, + OffsetDateTime now) { - return getCerberusAuthTokenFromRecord(token, tokenRecord); + String token = authTokenGenerator.generateSecureToken(); + AuthTokenRecord authTokenRecord = + new AuthTokenRecord() + .setId(id) + .setTokenHash(tokenHasher.hashToken(token)) + .setCreatedTs(now) + .setExpiresTs(now.plusMinutes(ttlInMinutes)) + .setPrincipal(principal) + .setPrincipalType(principalType.getName()) + .setIsAdmin(isAdmin) + .setGroups(groups) + .setRefreshCount(refreshCount); + authTokenDao.createAuthToken(authTokenRecord); + return getCerberusAuthTokenFromRecord(token, authTokenRecord); } private CerberusAuthToken getCerberusAuthTokenFromRecord( - String token, AuthTokenRecord tokenRecord) { - return CerberusAuthToken.builder() - .token(token) - .created(tokenRecord.getCreatedTs()) - .expires(tokenRecord.getExpiresTs()) - .principal(tokenRecord.getPrincipal()) - .principalType(PrincipalType.fromName(tokenRecord.getPrincipalType())) - .isAdmin(tokenRecord.getIsAdmin()) - .groups(tokenRecord.getGroups()) - .refreshCount(tokenRecord.getRefreshCount()) + String token, AuthTokenInfo authTokenInfo) { + return CerberusAuthToken.Builder.create() + .withToken(token) + .withCreated(authTokenInfo.getCreatedTs()) + .withExpires(authTokenInfo.getExpiresTs()) + .withPrincipal(authTokenInfo.getPrincipal()) + .withPrincipalType(PrincipalType.fromName(authTokenInfo.getPrincipalType())) + .withIsAdmin(authTokenInfo.getIsAdmin()) + .withGroups(authTokenInfo.getGroups()) + .withRefreshCount(authTokenInfo.getRefreshCount()) + .withId(authTokenInfo.getId()) .build(); } public Optional getCerberusAuthToken(String token) { - Optional tokenRecord = - authTokenDao.getAuthTokenFromHash(tokenHasher.hashToken(token)); + Optional tokenRecord = Optional.empty(); + AuthTokenAcceptType acceptType = tokenFlag.getAcceptType(); + boolean isJwt = jwtService.isJwt(token); + if (isJwt && (acceptType != AuthTokenAcceptType.SESSION)) { + tokenRecord = jwtService.parseAndValidateToken(token); + } else if (acceptType != AuthTokenAcceptType.JWT) { + tokenRecord = authTokenDao.getAuthTokenFromHash(tokenHasher.hashToken(token)); + } else { + String tokenType = isJwt ? "JWT" : "Session"; + logger.warn( + "Returning empty optional, because token type is {} and only {} are accepted", + tokenType, + acceptType.toString()); + return Optional.empty(); + } OffsetDateTime now = OffsetDateTime.now(); if (tokenRecord.isPresent() && tokenRecord.get().getExpiresTs().isBefore(now)) { @@ -127,9 +232,14 @@ public Optional getCerberusAuthToken(String token) { } @Transactional - public void revokeToken(String token) { - String hash = tokenHasher.hashToken(token); - authTokenDao.deleteAuthTokenFromHash(hash); + public void revokeToken(CerberusPrincipal cerberusPrincipal, OffsetDateTime tokenExpires) { + if (jwtService.isJwt(cerberusPrincipal.getToken())) { + logger.info("Revoking token ID: {}", cerberusPrincipal); + jwtService.revokeToken(cerberusPrincipal.getTokenId(), tokenExpires); + } else { + String hash = tokenHasher.hashToken(cerberusPrincipal.getToken()); + authTokenDao.deleteAuthTokenFromHash(hash); + } } @Transactional( diff --git a/cerberus-web/src/main/java/com/nike/cerberus/service/AuthenticationService.java b/cerberus-web/src/main/java/com/nike/cerberus/service/AuthenticationService.java index 98ae8b496..2186da333 100644 --- a/cerberus-web/src/main/java/com/nike/cerberus/service/AuthenticationService.java +++ b/cerberus-web/src/main/java/com/nike/cerberus/service/AuthenticationService.java @@ -487,7 +487,7 @@ public AuthResponse refreshUserToken(final CerberusPrincipal authPrincipal) { .build(); } - revoke(authPrincipal.getToken()); + revoke(authPrincipal, authPrincipal.getTokenExpires()); final AuthResponse authResponse = new AuthResponse(); authResponse.setStatus(AuthStatus.SUCCESS); @@ -501,9 +501,12 @@ public AuthResponse refreshUserToken(final CerberusPrincipal authPrincipal) { return authResponse; } - /** @param authToken Auth Token to be revoked */ - public void revoke(final String authToken) { - authTokenService.revokeToken(authToken); + /** + * @param cerberusPrincipal Auth principal to be revoked + * @param tokenExpires Token expire timestamp + */ + public void revoke(final CerberusPrincipal cerberusPrincipal, OffsetDateTime tokenExpires) { + authTokenService.revokeToken(cerberusPrincipal, tokenExpires); } /** diff --git a/cerberus-web/src/main/java/com/nike/cerberus/service/ConfigService.java b/cerberus-web/src/main/java/com/nike/cerberus/service/ConfigService.java new file mode 100644 index 000000000..81b3c5fa1 --- /dev/null +++ b/cerberus-web/src/main/java/com/nike/cerberus/service/ConfigService.java @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2021 Nike, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.nike.cerberus.service; + +import static com.nike.cerberus.service.EncryptionService.decrypt; + +import com.amazonaws.AmazonServiceException; +import com.amazonaws.encryptionsdk.AwsCrypto; +import com.amazonaws.regions.Region; +import com.amazonaws.regions.Regions; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.GetObjectRequest; +import com.amazonaws.services.s3.model.S3Object; +import com.amazonaws.util.IOUtils; +import com.nike.cerberus.util.CiphertextUtils; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; + +@ConditionalOnProperty(name = "cerberus.auth.jwt.secret.local.enabled", havingValue = "false") +@Component +public class ConfigService { + + private static final String JWT_SECRETS_PATH = "cms/jwt-secrets.json"; + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private final AmazonS3 s3Client; + + private final String bucketName; + + private final AwsCrypto awsCrypto; + + private final Region currentRegion; + + @Autowired + public ConfigService( + @Value("${cerberus.auth.jwt.secret.bucket}") final String bucketName, + final String region, + AwsCrypto awsCrypto) { + + currentRegion = Region.getRegion(Regions.fromName(region)); + this.s3Client = AmazonS3Client.builder().withRegion(region).build(); + + this.bucketName = bucketName; + this.awsCrypto = awsCrypto; + } + + public String getJwtSecrets() { + return getPlainText(JWT_SECRETS_PATH); + } + + private String getPlainText(String path) { + try { + return decrypt(CiphertextUtils.parse(getCipherText(path)), awsCrypto, currentRegion); + } catch (Exception e) { + throw new IllegalStateException( + "Failed to download and decrypt environment specific properties from s3", e); + } + } + + private String getCipherText(String path) { + final GetObjectRequest request = new GetObjectRequest(bucketName, path); + + try { + S3Object s3Object = s3Client.getObject(request); + InputStream object = s3Object.getObjectContent(); + return IOUtils.toString(object); + } catch (AmazonServiceException ase) { + if (StringUtils.equalsIgnoreCase(ase.getErrorCode(), "NoSuchKey")) { + final String errorMessage = + String.format( + "The S3 object doesn't exist. Bucket: %s, Key: %s", bucketName, request.getKey()); + logger.debug(errorMessage); + throw new IllegalStateException(errorMessage); + } else { + logger.error("Unexpected error communicating with AWS.", ase); + throw ase; + } + } catch (IOException e) { + String errorMessage = + String.format( + "Unable to read contents of S3 object. Bucket: %s, Key: %s, Expected Encoding: %s", + bucketName, request.getKey(), Charset.defaultCharset()); + logger.error(errorMessage); + throw new IllegalStateException(errorMessage, e); + } + } +} diff --git a/cerberus-web/src/main/java/com/nike/cerberus/service/JwtService.java b/cerberus-web/src/main/java/com/nike/cerberus/service/JwtService.java new file mode 100644 index 000000000..89b25a9d5 --- /dev/null +++ b/cerberus-web/src/main/java/com/nike/cerberus/service/JwtService.java @@ -0,0 +1,207 @@ +/* + * Copyright (c) 2021 Nike, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.nike.cerberus.service; + +import static io.jsonwebtoken.JwtParser.SEPARATOR_CHAR; +import static org.springframework.transaction.annotation.Isolation.READ_UNCOMMITTED; + +import com.nike.cerberus.dao.JwtBlocklistDao; +import com.nike.cerberus.error.AuthTokenTooLongException; +import com.nike.cerberus.jwt.CerberusJwtClaims; +import com.nike.cerberus.jwt.CerberusJwtKeySpec; +import com.nike.cerberus.jwt.CerberusSigningKeyResolver; +import com.nike.cerberus.record.JwtBlocklistRecord; +import io.jsonwebtoken.*; +import java.time.OffsetDateTime; +import java.time.ZoneId; +import java.util.Date; +import java.util.HashSet; +import java.util.Optional; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +/** Service for generating, parsing, and validating JWT tokens. */ +@Component +@ComponentScan(basePackages = {"com.nike.cerberus.jwt", "com.nike.cerberus.dao"}) +public class JwtService { + + protected final Logger log = LoggerFactory.getLogger(getClass()); + private static final String PRINCIPAL_TYPE_CLAIM_NAME = "principalType"; + private static final String GROUP_CLAIM_NAME = "groups"; + private static final String IS_ADMIN_CLAIM_NAME = "isAdmin"; + private static final String REFRESH_COUNT_CLAIM_NAME = "refreshCount"; + private static final int NUM_SEPARATORS_IN_JWT_TOKEN = 2; + + // Max header line length for an AWS ALB is 16k, so it needs to be less + @Value("${cerberus.auth.jwt.maxTokenLength:#{16000}}") + private int maxTokenLength; + + private final CerberusSigningKeyResolver signingKeyResolver; + private final String environmentName; + private final JwtBlocklistDao jwtBlocklistDao; + + private HashSet blocklist; + + @Autowired + public JwtService( + CerberusSigningKeyResolver signingKeyResolver, + @Value("cerberus.environmentName") String environmentName, + JwtBlocklistDao jwtBlocklistDao) { + this.signingKeyResolver = signingKeyResolver; + this.environmentName = environmentName; + this.jwtBlocklistDao = jwtBlocklistDao; + refreshBlocklist(); + } + + /** + * Generate JWT token + * + * @param cerberusJwtClaims Cerberus JWT claims + * @return JWT token + */ + public String generateJwtToken(CerberusJwtClaims cerberusJwtClaims) + throws AuthTokenTooLongException { + CerberusJwtKeySpec cerberusJwtKeySpec = signingKeyResolver.resolveSigningKey(); + String principal = cerberusJwtClaims.getPrincipal(); + + String jwtToken = + Jwts.builder() + .setHeaderParam(JwsHeader.KEY_ID, cerberusJwtKeySpec.getKid()) + .setId(cerberusJwtClaims.getId()) + .setIssuer(environmentName) + .setSubject(principal) + .claim(PRINCIPAL_TYPE_CLAIM_NAME, cerberusJwtClaims.getPrincipalType()) + .claim(GROUP_CLAIM_NAME, cerberusJwtClaims.getGroups()) + .claim(IS_ADMIN_CLAIM_NAME, cerberusJwtClaims.getIsAdmin()) + .claim(REFRESH_COUNT_CLAIM_NAME, cerberusJwtClaims.getRefreshCount()) + .setExpiration(Date.from(cerberusJwtClaims.getExpiresTs().toInstant())) + .setIssuedAt(Date.from(cerberusJwtClaims.getCreatedTs().toInstant())) + .signWith(cerberusJwtKeySpec) + .compressWith(CompressionCodecs.GZIP) + .compact(); + + int tokenLength = jwtToken.length(); + log.info("{}: JWT length: {}", principal, tokenLength); + if (tokenLength > maxTokenLength) { + String msg = + String.format( + "Token for %s is %d characters long. The max is %d bytes.", + principal, tokenLength, maxTokenLength); + throw new AuthTokenTooLongException(msg); + } + return jwtToken; + } + + /** + * Parse and validate JWT token + * + * @param token JWT token + * @return Cerberus JWT claims + */ + public Optional parseAndValidateToken(String token) { + Jws claimsJws; + try { + claimsJws = + Jwts.parser() + .requireIssuer(environmentName) + .setSigningKeyResolver(signingKeyResolver) + .parseClaimsJws(token); + } catch (InvalidClaimException e) { + log.warn("Invalid claim when parsing token: {}", token, e); + return Optional.empty(); + } catch (JwtException e) { + log.warn("Error parsing JWT token: {}", token, e); + return Optional.empty(); + } catch (IllegalArgumentException e) { + log.warn("Error parsing JWT token: {}", token, e); + return Optional.empty(); + } + Claims claims = claimsJws.getBody(); + if (blocklist.contains(claims.getId())) { + log.warn("This JWT token is blocklisted. ID: {}", claims.getId()); + return Optional.empty(); + } + String subject = claims.getSubject(); + CerberusJwtClaims cerberusJwtClaims = + new CerberusJwtClaims() + .setId(claims.getId()) + .setPrincipal(subject) + .setExpiresTs( + OffsetDateTime.ofInstant( + claims.getExpiration().toInstant(), ZoneId.systemDefault())) + .setCreatedTs( + OffsetDateTime.ofInstant(claims.getIssuedAt().toInstant(), ZoneId.systemDefault())) + .setPrincipalType(claims.get(PRINCIPAL_TYPE_CLAIM_NAME, String.class)) + .setGroups(claims.get(GROUP_CLAIM_NAME, String.class)) + .setIsAdmin(claims.get(IS_ADMIN_CLAIM_NAME, Boolean.class)) + .setRefreshCount(claims.get(REFRESH_COUNT_CLAIM_NAME, Integer.class)); + + return Optional.of(cerberusJwtClaims); + } + + /** Refresh signing keys in {@link CerberusSigningKeyResolver} */ + public void refreshKeys() { + signingKeyResolver.refresh(); + } + + /** Refresh JWT blocklist */ + public void refreshBlocklist() { + blocklist = jwtBlocklistDao.getBlocklist(); + } + + /** + * Revoke JWT + * + * @param id JWT ID + * @param tokenExpires Expiration timestamp of the JWT + */ + public void revokeToken(String id, OffsetDateTime tokenExpires) { + blocklist.add(id); + JwtBlocklistRecord jwtBlocklistRecord = + new JwtBlocklistRecord().setId(id).setExpiresTs(tokenExpires); + jwtBlocklistDao.addToBlocklist(jwtBlocklistRecord); + } + + /** + * Delete JWT blocklist entries that have expired + * + * @return + */ + @Transactional( + isolation = READ_UNCOMMITTED // allow dirty reads so we don't block other threads + ) + public int deleteExpiredTokens() { + return jwtBlocklistDao.deleteExpiredTokens(); + } + + /** + * Return if the token looks like a JWT. Technically a JWT can have one dot but we don't allow it + * here. + * + * @param token The token to examine + * @return Does the token look like a JWT + */ + public boolean isJwt(String token) { + return StringUtils.countMatches(token, SEPARATOR_CHAR) == NUM_SEPARATORS_IN_JWT_TOKEN; + } +} diff --git a/cerberus-web/src/main/resources/cerberus.yaml b/cerberus-web/src/main/resources/cerberus.yaml index 5ef6d1928..14e31818c 100644 --- a/cerberus-web/src/main/resources/cerberus.yaml +++ b/cerberus-web/src/main/resources/cerberus.yaml @@ -138,6 +138,14 @@ cerberus: # clientId: yourClientId # clientSecret: yourClientSecret # subdomain: yourSubDomain + auth: + token: + issue-type: session # Can be session or jwt + accept-type: session # Can be session, jwt or all (which is both) +# auth.jwt: +# secret.local.autoGenerate: false # generate jwt secret for local development +# secret.local.enabled: false # set this to true when doing local development +# secret.bucket: jwt-secret-bucket # s3 bucket containing secret material # With Cerberus 4.+ (Phoenix) We now have officially deprecated and turned off by default KMS Auth @@ -232,6 +240,21 @@ cerberus: # Every hour cronExpression: "0 0 * ? * *" + # JWT + jwtSecretRefreshJob: + enabled: false + # Every minute + cronExpression: "0 * * ? * *" + + jwtBlocklistCleanUpJob: + enabled: false + # Every 5 minutes + cronExpression: "30 0/5 * ? * *" + + jwtBlocklistRefreshJob: + enabled: false + # Every 10 seconds + cronExpression: "0/10 * * ? * *" ################################################################################################ # This Job require auth.iam.kms.rootUserArn,adminRoleArn,cmsRoleArn to be configured diff --git a/cerberus-web/src/main/resources/com/nike/cerberus/mapper/JwtBlocklistMapper.xml b/cerberus-web/src/main/resources/com/nike/cerberus/mapper/JwtBlocklistMapper.xml new file mode 100644 index 000000000..c85f975fd --- /dev/null +++ b/cerberus-web/src/main/resources/com/nike/cerberus/mapper/JwtBlocklistMapper.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + INSERT INTO JWT_BLOCKLIST ( + ID, + EXPIRES_TS + ) + VALUES ( + #{record.id}, + #{record.expiresTs} + ) + + + + DELETE FROM JWT_BLOCKLIST WHERE EXPIRES_TS < CURRENT_TIME + + \ No newline at end of file diff --git a/cerberus-web/src/main/resources/com/nike/cerberus/migration/V1.7.0.0__jwt_blocklist.sql b/cerberus-web/src/main/resources/com/nike/cerberus/migration/V1.7.0.0__jwt_blocklist.sql new file mode 100644 index 000000000..f4f160566 --- /dev/null +++ b/cerberus-web/src/main/resources/com/nike/cerberus/migration/V1.7.0.0__jwt_blocklist.sql @@ -0,0 +1,15 @@ +### +# +# Create Table for JWT Blocklist +# +### + +CREATE TABLE JWT_BLOCKLIST( + ID CHAR(36) NOT NULL, + EXPIRES_TS DATETIME NOT NULL, + PRIMARY KEY (ID) +) + ENGINE = InnoDB DEFAULT CHARSET = utf8; + +ALTER TABLE JWT_BLOCKLIST + ADD INDEX `IX_JWT_BLOCKLIST_EXPIRES_TS` (EXPIRES_TS); \ No newline at end of file diff --git a/cerberus-web/src/test/java/com/nike/cerberus/controller/CategoryControllerTest.java b/cerberus-web/src/test/java/com/nike/cerberus/controller/CategoryControllerTest.java new file mode 100644 index 000000000..b49e63aaf --- /dev/null +++ b/cerberus-web/src/test/java/com/nike/cerberus/controller/CategoryControllerTest.java @@ -0,0 +1,90 @@ +package com.nike.cerberus.controller; + +import com.nike.cerberus.domain.Category; +import com.nike.cerberus.service.CategoryService; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.util.UriComponentsBuilder; + +public class CategoryControllerTest { + + @Mock private CategoryService categoryService; + + @InjectMocks private CategoryController categoryController; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + categoryController = Mockito.spy(categoryController); + } + + @Test + public void testCreateCategory() { + Category category = Mockito.mock(Category.class); + Authentication authentication = Mockito.mock(Authentication.class); + Mockito.when(authentication.getName()).thenReturn("name"); + SecurityContextHolder.getContext().setAuthentication(authentication); + Mockito.when(categoryService.createCategory(Mockito.eq(category), Mockito.anyString())) + .thenReturn("value"); + UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.newInstance(); + ResponseEntity response = + categoryController.createCategory(category, uriComponentsBuilder); + HttpHeaders responseHeaders = response.getHeaders(); + Assert.assertEquals("/v1/category/value", responseHeaders.get("Location").get(0)); + } + + @Test + public void testDeleteCategoryWhenDeletionIsSuccessful() { + Mockito.when(categoryService.deleteCategory("categoryId")).thenReturn(true); + ResponseEntity categoryIdResponse = categoryController.deleteCategory("categoryId"); + Assert.assertEquals(HttpStatus.NO_CONTENT, categoryIdResponse.getStatusCode()); + } + + @Test + public void testDeleteCategoryWhenDeletionIsFailed() { + Mockito.when(categoryService.deleteCategory("categoryId")).thenReturn(false); + ResponseEntity categoryIdResponse = categoryController.deleteCategory("categoryId"); + Assert.assertEquals(HttpStatus.NOT_FOUND, categoryIdResponse.getStatusCode()); + } + + @Test + public void testGetCategory() { + Category category = Mockito.mock(Category.class); + Mockito.when(categoryService.getCategory("categoryId")).thenReturn(Optional.of(category)); + ResponseEntity categoryResponseEntity = categoryController.getCategory("categoryId"); + Assert.assertEquals(HttpStatus.OK, categoryResponseEntity.getStatusCode()); + Assert.assertEquals(category, categoryResponseEntity.getBody()); + } + + @Test + public void testGetCategoryWhenCategoryIsNotPresent() { + Mockito.when(categoryService.getCategory("categoryId")).thenReturn(Optional.empty()); + ResponseEntity categoryResponseEntity = categoryController.getCategory("categoryId"); + Assert.assertEquals(HttpStatus.NOT_FOUND, categoryResponseEntity.getStatusCode()); + } + + @Test + public void testListCategories() { + Category category = Mockito.mock(Category.class); + List categoryList = new ArrayList<>(); + categoryList.add(category); + Mockito.when(categoryService.getAllCategories()).thenReturn(categoryList); + List categories = categoryController.listCategories(); + Assert.assertSame(categoryList, categories); + Assert.assertEquals(categoryList.size(), categories.size()); + Assert.assertSame(categoryList.get(0), categories.get(0)); + } +} diff --git a/cerberus-web/src/test/java/com/nike/cerberus/controller/DashboardControllerTest.java b/cerberus-web/src/test/java/com/nike/cerberus/controller/DashboardControllerTest.java new file mode 100644 index 000000000..1fc224861 --- /dev/null +++ b/cerberus-web/src/test/java/com/nike/cerberus/controller/DashboardControllerTest.java @@ -0,0 +1,13 @@ +package com.nike.cerberus.controller; + +import org.junit.Assert; +import org.junit.Test; + +public class DashboardControllerTest { + + @Test + public void testRoot() { + String root = new DashboardController().root(); + Assert.assertEquals("redirect:/dashboard/index.html", root); + } +} diff --git a/cerberus-web/src/test/java/com/nike/cerberus/controller/GetSecretVersionPathsForSdbControllerTest.java b/cerberus-web/src/test/java/com/nike/cerberus/controller/GetSecretVersionPathsForSdbControllerTest.java new file mode 100644 index 000000000..f412bab48 --- /dev/null +++ b/cerberus-web/src/test/java/com/nike/cerberus/controller/GetSecretVersionPathsForSdbControllerTest.java @@ -0,0 +1,35 @@ +package com.nike.cerberus.controller; + +import com.nike.cerberus.service.SafeDepositBoxService; +import java.util.HashSet; +import java.util.Set; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +public class GetSecretVersionPathsForSdbControllerTest { + + @Mock private SafeDepositBoxService safeDepositBoxService; + + @InjectMocks private GetSecretVersionPathsForSdbController getSecretVersionPathsForSdbController; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testGetVersionPathsForSdb() { + Set paths = new HashSet<>(); + paths.add("path1"); + paths.add("path2"); + Mockito.when(safeDepositBoxService.getSecureDataVersionPathsForSdb("sdbId")).thenReturn(paths); + Set actualPaths = getSecretVersionPathsForSdbController.getVersionPathsForSdb("sdbId"); + Assert.assertSame(paths, actualPaths); + Assert.assertEquals(paths, actualPaths); + } +} diff --git a/cerberus-web/src/test/java/com/nike/cerberus/controller/GetSecureDataVersionsControllerTest.java b/cerberus-web/src/test/java/com/nike/cerberus/controller/GetSecureDataVersionsControllerTest.java new file mode 100644 index 000000000..fd7183719 --- /dev/null +++ b/cerberus-web/src/test/java/com/nike/cerberus/controller/GetSecureDataVersionsControllerTest.java @@ -0,0 +1,76 @@ +package com.nike.cerberus.controller; + +import com.nike.backstopper.apierror.ApiError; +import com.nike.backstopper.exception.ApiException; +import com.nike.cerberus.domain.SecureDataVersionSummary; +import com.nike.cerberus.domain.SecureDataVersionsResult; +import com.nike.cerberus.error.DefaultApiError; +import com.nike.cerberus.event.filter.AuditLoggingFilterDetails; +import com.nike.cerberus.service.SecureDataVersionService; +import com.nike.cerberus.util.SdbAccessRequest; +import java.util.ArrayList; +import java.util.List; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +public class GetSecureDataVersionsControllerTest { + @Mock private SecureDataVersionService secureDataVersionService; + @Mock private SdbAccessRequest sdbAccessRequest; + @Mock private AuditLoggingFilterDetails auditLoggingFilterDetails; + + @InjectMocks private GetSecureDataVersionsController getSecureDataVersionsController; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testGetVersionPathsForSdbWhenSecureDataVersionsAreNotPresent() { + SecureDataVersionsResult secureDataVersionsResult = + Mockito.mock(SecureDataVersionsResult.class); + Mockito.when(sdbAccessRequest.getSdbId()).thenReturn("sdbId"); + Mockito.when(sdbAccessRequest.getPath()).thenReturn("path"); + Mockito.when(sdbAccessRequest.getCategory()).thenReturn("category"); + Mockito.when( + secureDataVersionService.getSecureDataVersionSummariesByPath( + "sdbId", "path", "category", 10, 10)) + .thenReturn(secureDataVersionsResult); + ApiError apiError = null; + try { + getSecureDataVersionsController.getVersionPathsForSdb(10, 10); + } catch (ApiException apiException) { + apiError = apiException.getApiErrors().get(0); + } + Assert.assertEquals(DefaultApiError.GENERIC_BAD_REQUEST, apiError); + Mockito.verify(auditLoggingFilterDetails) + .setAction("Failed to find versions for secret with path: " + sdbAccessRequest.getPath()); + } + + @Test + public void testGetVersionPaths() { + SecureDataVersionsResult secureDataVersionsResult = + Mockito.mock(SecureDataVersionsResult.class); + Mockito.when(sdbAccessRequest.getSdbId()).thenReturn("sdbId"); + Mockito.when(sdbAccessRequest.getPath()).thenReturn("path"); + Mockito.when(sdbAccessRequest.getCategory()).thenReturn("category"); + List secureDataVersionSummaries = new ArrayList<>(); + SecureDataVersionSummary secureDataVersionSummary = + Mockito.mock(SecureDataVersionSummary.class); + secureDataVersionSummaries.add(secureDataVersionSummary); + Mockito.when(secureDataVersionsResult.getSecureDataVersionSummaries()) + .thenReturn(secureDataVersionSummaries); + Mockito.when( + secureDataVersionService.getSecureDataVersionSummariesByPath( + "sdbId", "path", "category", 10, 10)) + .thenReturn(secureDataVersionsResult); + SecureDataVersionsResult actualSecureDataVersionsResult = + getSecureDataVersionsController.getVersionPathsForSdb(10, 10); + Assert.assertSame(secureDataVersionsResult, actualSecureDataVersionsResult); + } +} diff --git a/cerberus-web/src/test/java/com/nike/cerberus/controller/RoleControllerTest.java b/cerberus-web/src/test/java/com/nike/cerberus/controller/RoleControllerTest.java new file mode 100644 index 000000000..6cbc12889 --- /dev/null +++ b/cerberus-web/src/test/java/com/nike/cerberus/controller/RoleControllerTest.java @@ -0,0 +1,55 @@ +package com.nike.cerberus.controller; + +import com.nike.cerberus.domain.Role; +import com.nike.cerberus.service.RoleService; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +public class RoleControllerTest { + + @Mock private RoleService roleService; + + @InjectMocks private RoleController roleController; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testGetCategoryWhenRoleIsNotPresentById() { + Mockito.when(roleService.getRoleById("id")).thenReturn(Optional.empty()); + ResponseEntity categoryResponseEntity = roleController.getCategory("id"); + Assert.assertEquals(HttpStatus.NOT_FOUND, categoryResponseEntity.getStatusCode()); + } + + @Test + public void testGetCategory() { + Role role = Mockito.mock(Role.class); + Mockito.when(roleService.getRoleById("id")).thenReturn(Optional.of(role)); + ResponseEntity categoryResponseEntity = roleController.getCategory("id"); + Assert.assertEquals(HttpStatus.OK, categoryResponseEntity.getStatusCode()); + Assert.assertSame(role, categoryResponseEntity.getBody()); + } + + @Test + public void testListRoles() { + List roles = new ArrayList<>(); + Role role = Mockito.mock(Role.class); + roles.add(role); + Mockito.when(roleService.getAllRoles()).thenReturn(roles); + List actualRoles = roleController.listRoles(); + Assert.assertSame(roles, actualRoles); + Assert.assertEquals(roles, actualRoles); + } +} diff --git a/cerberus-web/src/test/java/com/nike/cerberus/controller/SafeDepositBoxControllerV2Test.java b/cerberus-web/src/test/java/com/nike/cerberus/controller/SafeDepositBoxControllerV2Test.java new file mode 100644 index 000000000..dfe72a71c --- /dev/null +++ b/cerberus-web/src/test/java/com/nike/cerberus/controller/SafeDepositBoxControllerV2Test.java @@ -0,0 +1,91 @@ +package com.nike.cerberus.controller; + +import com.nike.cerberus.domain.SafeDepositBoxSummary; +import com.nike.cerberus.domain.SafeDepositBoxV2; +import com.nike.cerberus.security.CerberusPrincipal; +import com.nike.cerberus.service.SafeDepositBoxService; +import java.util.ArrayList; +import java.util.List; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.web.util.UriComponentsBuilder; + +public class SafeDepositBoxControllerV2Test { + + @Mock private SafeDepositBoxService safeDepositBoxService; + + @InjectMocks private SafeDepositBoxControllerV2 safeDepositBoxControllerV2; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testCreateSafeDepositBox() { + SafeDepositBoxV2 safeDepositBoxV2 = Mockito.mock(SafeDepositBoxV2.class); + Mockito.when(safeDepositBoxV2.getId()).thenReturn("id"); + Authentication authentication = Mockito.mock(Authentication.class); + Mockito.when(authentication.getName()).thenReturn("name"); + Mockito.when(safeDepositBoxService.createSafeDepositBoxV2(safeDepositBoxV2, "name")) + .thenReturn(safeDepositBoxV2); + UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.newInstance(); + ResponseEntity safeDepositBoxResponseEntity = + safeDepositBoxControllerV2.createSafeDepositBox( + safeDepositBoxV2, authentication, uriComponentsBuilder); + Assert.assertSame(safeDepositBoxV2, safeDepositBoxResponseEntity.getBody()); + HttpHeaders headers = safeDepositBoxResponseEntity.getHeaders(); + Assert.assertEquals("/V2/safe-deposit-box/id", headers.get("Location").get(0)); + } + + @Test + public void testGetSafeDepositBox() { + SafeDepositBoxV2 safeDepositBoxV2 = Mockito.mock(SafeDepositBoxV2.class); + Mockito.when(safeDepositBoxService.getSDBAndValidatePrincipalAssociationV2("sdbId")) + .thenReturn(safeDepositBoxV2); + SafeDepositBoxV2 actualSafeDepositBox = safeDepositBoxControllerV2.getSafeDepositBox("sdbId"); + Assert.assertSame(safeDepositBoxV2, actualSafeDepositBox); + } + + @Test + public void testUpdateSafeDepositBoxV2() { + SafeDepositBoxV2 safeDepositBoxV2 = Mockito.mock(SafeDepositBoxV2.class); + CerberusPrincipal cerberusPrincipal = Mockito.mock(CerberusPrincipal.class); + Mockito.when( + safeDepositBoxService.updateSafeDepositBoxV2( + safeDepositBoxV2, cerberusPrincipal, "sdbId")) + .thenReturn(safeDepositBoxV2); + SafeDepositBoxV2 actualSafeDepositBoxV2 = + safeDepositBoxControllerV2.updateSafeDepositBox( + "sdbId", safeDepositBoxV2, cerberusPrincipal); + Assert.assertSame(safeDepositBoxV2, actualSafeDepositBoxV2); + } + + @Test + public void testDeleteSafeDepositBox() { + safeDepositBoxControllerV2.deleteSafeDepositBox("sdbId"); + Mockito.verify(safeDepositBoxService).deleteSafeDepositBox("sdbId"); + } + + @Test + public void testGetSafeDepositBoxes() { + CerberusPrincipal cerberusPrincipal = Mockito.mock(CerberusPrincipal.class); + SafeDepositBoxSummary safeDepositBoxSummary = Mockito.mock(SafeDepositBoxSummary.class); + List safeDepositBoxSummaries = new ArrayList<>(); + safeDepositBoxSummaries.add(safeDepositBoxSummary); + Mockito.when(safeDepositBoxService.getAssociatedSafeDepositBoxes(cerberusPrincipal)) + .thenReturn(safeDepositBoxSummaries); + List actualSafeDepositBoSummaries = + safeDepositBoxControllerV2.getSafeDepositBoxes(cerberusPrincipal); + Assert.assertSame(safeDepositBoxSummaries, actualSafeDepositBoSummaries); + Assert.assertEquals(safeDepositBoxSummaries, actualSafeDepositBoSummaries); + } +} diff --git a/cerberus-web/src/test/java/com/nike/cerberus/controller/SecureDataControllerTest.java b/cerberus-web/src/test/java/com/nike/cerberus/controller/SecureDataControllerTest.java new file mode 100644 index 000000000..5853ff63a --- /dev/null +++ b/cerberus-web/src/test/java/com/nike/cerberus/controller/SecureDataControllerTest.java @@ -0,0 +1,165 @@ +package com.nike.cerberus.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.nike.cerberus.domain.SecureData; +import com.nike.cerberus.domain.SecureDataResponse; +import com.nike.cerberus.domain.SecureDataType; +import com.nike.cerberus.domain.SecureDataVersion; +import com.nike.cerberus.security.CerberusPrincipal; +import com.nike.cerberus.service.SecureDataService; +import com.nike.cerberus.service.SecureDataVersionService; +import com.nike.cerberus.util.SdbAccessRequest; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.util.*; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +public class SecureDataControllerTest { + + @Mock private SecureDataService secureDataService; + @Mock private SecureDataVersionService secureDataVersionService; + @Mock private SdbAccessRequest sdbAccessRequest; + @InjectMocks private SecureDataController secureDataController; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testReadSecureDataWhenSecureDataIsNotPresent() { + Mockito.when(sdbAccessRequest.getSdbId()).thenReturn("sdbId"); + Mockito.when(sdbAccessRequest.getPath()).thenReturn("path"); + Mockito.when(secureDataService.readSecret("sdbId", "path")).thenReturn(Optional.empty()); + ResponseEntity responseEntity = secureDataController.readSecureData(); + Assert.assertEquals(HttpStatus.NOT_FOUND, responseEntity.getStatusCode()); + } + + @Test + @SuppressFBWarnings + public void testReadSecureData() { + Mockito.when(sdbAccessRequest.getSdbId()).thenReturn("sdbId"); + Mockito.when(sdbAccessRequest.getPath()).thenReturn("path"); + SecureData secureData = Mockito.mock(SecureData.class); + ObjectNode objectNode = new ObjectMapper().createObjectNode(); + objectNode.put("key", "value"); + Mockito.when(secureData.getData()).thenReturn(objectNode.toString()); + Mockito.when(secureDataService.readSecret("sdbId", "path")).thenReturn(Optional.of(secureData)); + Map metadata = new HashMap<>(); + Mockito.when(secureDataService.parseSecretMetadata(secureData)).thenReturn(metadata); + ResponseEntity responseEntity = secureDataController.readSecureData(); + Assert.assertEquals(HttpStatus.OK, responseEntity.getStatusCode()); + SecureDataResponse secureDataResponse = (SecureDataResponse) responseEntity.getBody(); + Assert.assertSame(metadata, secureDataResponse.getMetadata()); + Assert.assertNotNull(secureDataResponse.getRequestId()); + Assert.assertEquals(objectNode.toString(), secureDataResponse.getData().toString()); + } + + @Test + @SuppressFBWarnings + public void testListKeys() { + Mockito.when(sdbAccessRequest.getSdbId()).thenReturn("sdbId"); + Mockito.when(sdbAccessRequest.getPath()).thenReturn("path"); + Set keys = new HashSet<>(); + keys.add("key"); + Mockito.when(secureDataService.listKeys("sdbId", "path")).thenReturn(keys); + ResponseEntity responseEntity = secureDataController.listKeys("true"); + Assert.assertEquals(HttpStatus.OK, responseEntity.getStatusCode()); + SecureDataResponse secureDataResponse = (SecureDataResponse) responseEntity.getBody(); + Map> data = (Map>) secureDataResponse.getData(); + Assert.assertSame(keys, data.get("keys")); + Assert.assertEquals(keys, data.get("keys")); + } + + @Test + public void testListKeysWhenFalse() { + Mockito.when(sdbAccessRequest.getSdbId()).thenReturn("sdbId"); + Mockito.when(sdbAccessRequest.getPath()).thenReturn("path"); + Mockito.when(secureDataService.readSecret("sdbId", "path")).thenReturn(Optional.empty()); + ResponseEntity responseEntity = secureDataController.listKeys("false"); + Assert.assertEquals(HttpStatus.NOT_FOUND, responseEntity.getStatusCode()); + } + + @Test + public void testReadSecureDataVersionWhenDataIsNotPresentByVersionId() { + Mockito.when(sdbAccessRequest.getSdbId()).thenReturn("sdbId"); + Mockito.when(sdbAccessRequest.getPath()).thenReturn("path"); + Mockito.when(sdbAccessRequest.getCategory()).thenReturn("category"); + Mockito.when( + secureDataVersionService.getSecureDataVersionById( + "sdbId", "versionId", "category", "path")) + .thenReturn(Optional.empty()); + ResponseEntity responseEntity = secureDataController.readSecureDataVersion("versionId"); + Assert.assertEquals(HttpStatus.NOT_FOUND, responseEntity.getStatusCode()); + } + + @Test + @SuppressFBWarnings + public void testReadSecureDataVersion() { + Mockito.when(sdbAccessRequest.getSdbId()).thenReturn("sdbId"); + Mockito.when(sdbAccessRequest.getPath()).thenReturn("path"); + Mockito.when(sdbAccessRequest.getCategory()).thenReturn("category"); + SecureDataVersion secureDataVersion = Mockito.mock(SecureDataVersion.class); + ObjectNode objectNode = new ObjectMapper().createObjectNode(); + objectNode.put("key", "value"); + Mockito.when(secureDataVersion.getData()).thenReturn(objectNode.toString()); + Map metadata = new HashMap<>(); + Mockito.when(secureDataVersionService.parseVersionMetadata(secureDataVersion)) + .thenReturn(metadata); + Mockito.when( + secureDataVersionService.getSecureDataVersionById( + "sdbId", "versionId", "category", "path")) + .thenReturn(Optional.of(secureDataVersion)); + ResponseEntity responseEntity = secureDataController.readSecureDataVersion("versionId"); + Assert.assertEquals(HttpStatus.OK, responseEntity.getStatusCode()); + SecureDataResponse secureDataResponse = (SecureDataResponse) responseEntity.getBody(); + Assert.assertSame(metadata, secureDataResponse.getMetadata()); + Assert.assertNotNull(secureDataResponse.getRequestId()); + Assert.assertEquals(objectNode.toString(), secureDataResponse.getData().toString()); + } + + @Test + public void testWriteSecureDataWhenBodyIsNullInHttpEntity() { + HttpEntity httpEntity = new HttpEntity<>(new HttpHeaders()); + String exceptionMessage = ""; + try { + secureDataController.writeSecureData(httpEntity); + } catch (RuntimeException e) { + exceptionMessage = e.getMessage(); + } + Assert.assertEquals("The body must not be null", exceptionMessage); + } + + @Test + public void testWriteSecureData() { + Mockito.when(sdbAccessRequest.getSdbId()).thenReturn("sdbId"); + Mockito.when(sdbAccessRequest.getPath()).thenReturn("path"); + CerberusPrincipal cerberusPrincipal = Mockito.mock(CerberusPrincipal.class); + Mockito.when(sdbAccessRequest.getPrincipal()).thenReturn(cerberusPrincipal); + Mockito.when(cerberusPrincipal.getName()).thenReturn("name"); + HttpEntity httpEntity = new HttpEntity<>("body"); + secureDataController.writeSecureData(httpEntity); + Mockito.verify(secureDataService).writeSecret("sdbId", "path", "body", "name"); + } + + @Test + public void testDeleteSecureData() { + Mockito.when(sdbAccessRequest.getSdbId()).thenReturn("sdbId"); + Mockito.when(sdbAccessRequest.getPath()).thenReturn("path"); + CerberusPrincipal cerberusPrincipal = Mockito.mock(CerberusPrincipal.class); + Mockito.when(sdbAccessRequest.getPrincipal()).thenReturn(cerberusPrincipal); + Mockito.when(cerberusPrincipal.getName()).thenReturn("name"); + secureDataController.deleteSecureData(); + Mockito.verify(secureDataService).deleteSecret("sdbId", "path", SecureDataType.OBJECT, "name"); + } +} diff --git a/cerberus-web/src/test/java/com/nike/cerberus/controller/SecureFileControllerTest.java b/cerberus-web/src/test/java/com/nike/cerberus/controller/SecureFileControllerTest.java new file mode 100644 index 000000000..e5159cb8c --- /dev/null +++ b/cerberus-web/src/test/java/com/nike/cerberus/controller/SecureFileControllerTest.java @@ -0,0 +1,190 @@ +package com.nike.cerberus.controller; + +import com.nike.backstopper.apierror.ApiError; +import com.nike.backstopper.exception.ApiException; +import com.nike.cerberus.domain.SecureDataType; +import com.nike.cerberus.domain.SecureFileCurrent; +import com.nike.cerberus.domain.SecureFileSummary; +import com.nike.cerberus.domain.SecureFileVersion; +import com.nike.cerberus.error.DefaultApiError; +import com.nike.cerberus.security.CerberusPrincipal; +import com.nike.cerberus.service.SecureDataService; +import com.nike.cerberus.service.SecureDataVersionService; +import com.nike.cerberus.util.SdbAccessRequest; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Optional; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.multipart.MultipartFile; + +public class SecureFileControllerTest { + + @Mock private SecureDataService secureDataService; + @Mock private SecureDataVersionService secureDataVersionService; + @Mock private SdbAccessRequest sdbAccessRequest; + + @InjectMocks private SecureFileController secureFileController; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testHeadSecureFileWhenFileMetadataIsNotPresent() { + Mockito.when(sdbAccessRequest.getSdbId()).thenReturn("sdbId"); + Mockito.when(sdbAccessRequest.getPath()).thenReturn("path"); + Mockito.when(secureDataService.readFileMetadataOnly("sdbId", "path")) + .thenReturn(Optional.empty()); + ApiError apiError = null; + try { + secureFileController.headSecureFile(); + } catch (ApiException apiException) { + apiError = apiException.getApiErrors().get(0); + } + Assert.assertEquals(DefaultApiError.ENTITY_NOT_FOUND, apiError); + } + + @Test + @SuppressFBWarnings + public void testHeadSecureFile() { + Mockito.when(sdbAccessRequest.getSdbId()).thenReturn("sdbId"); + Mockito.when(sdbAccessRequest.getPath()).thenReturn("path"); + SecureFileSummary secureFileSummary = Mockito.mock(SecureFileSummary.class); + Mockito.when(secureFileSummary.getName()).thenReturn("sample.txt"); + Mockito.when(secureDataService.readFileMetadataOnly("sdbId", "path")) + .thenReturn(Optional.of(secureFileSummary)); + ResponseEntity voidResponseEntity = secureFileController.headSecureFile(); + Assert.assertEquals(HttpStatus.OK, voidResponseEntity.getStatusCode()); + HttpHeaders httpHeaders = voidResponseEntity.getHeaders(); + Assert.assertEquals("text/plain", httpHeaders.get("Content-Type").get(0)); + Assert.assertEquals("0", httpHeaders.get("Content-Length").get(0)); + Assert.assertEquals( + "attachment; filename=\"sample.txt\"", httpHeaders.get("Content-Disposition").get(0)); + } + + @Test + public void testGetSecureFileWhenFileIsNotPresent() { + Mockito.when(sdbAccessRequest.getSdbId()).thenReturn("sdbId"); + Mockito.when(sdbAccessRequest.getPath()).thenReturn("path"); + Mockito.when(secureDataService.readFile("sdbId", "path")).thenReturn(Optional.empty()); + ApiError apiError = null; + try { + secureFileController.getSecureFile(); + } catch (ApiException apiException) { + apiError = apiException.getApiErrors().get(0); + } + Assert.assertEquals(DefaultApiError.ENTITY_NOT_FOUND, apiError); + } + + @Test + @SuppressFBWarnings + public void testGetSecureFile() { + Mockito.when(sdbAccessRequest.getSdbId()).thenReturn("sdbId"); + Mockito.when(sdbAccessRequest.getPath()).thenReturn("path"); + SecureFileCurrent secureFileCurrent = Mockito.mock(SecureFileCurrent.class); + Mockito.when(secureFileCurrent.getName()).thenReturn("sample.txt"); + Mockito.when(secureFileCurrent.getData()).thenReturn("data".getBytes(StandardCharsets.UTF_8)); + Mockito.when(secureDataService.readFile("sdbId", "path")) + .thenReturn(Optional.of(secureFileCurrent)); + ResponseEntity responseEntity = secureFileController.getSecureFile(); + Assert.assertEquals(HttpStatus.OK, responseEntity.getStatusCode()); + HttpHeaders httpHeaders = responseEntity.getHeaders(); + Assert.assertEquals("text/plain", httpHeaders.get("Content-Type").get(0)); + Assert.assertEquals( + "attachment; filename=\"sample.txt\"", httpHeaders.get("Content-Disposition").get(0)); + } + + @Test + public void testGetSecureFileVersionWhenFileIsNotFound() { + Mockito.when(sdbAccessRequest.getSdbId()).thenReturn("sdbId"); + Mockito.when(sdbAccessRequest.getPath()).thenReturn("path"); + Mockito.when(sdbAccessRequest.getCategory()).thenReturn("category"); + Mockito.when( + secureDataVersionService.getSecureFileVersionById( + "sdbId", "versionId", "category", "path")) + .thenReturn(Optional.empty()); + ApiError apiError = null; + try { + secureFileController.getSecureFileVersion("versionId"); + } catch (ApiException apiException) { + apiError = apiException.getApiErrors().get(0); + } + Assert.assertEquals(DefaultApiError.ENTITY_NOT_FOUND, apiError); + } + + @Test + @SuppressFBWarnings + public void testGetSecureFileVersion() { + Mockito.when(sdbAccessRequest.getSdbId()).thenReturn("sdbId"); + Mockito.when(sdbAccessRequest.getPath()).thenReturn("path"); + Mockito.when(sdbAccessRequest.getCategory()).thenReturn("category"); + SecureFileVersion secureFileVersion = Mockito.mock(SecureFileVersion.class); + Mockito.when(secureFileVersion.getName()).thenReturn("sample.txt"); + Mockito.when(secureFileVersion.getData()).thenReturn("data".getBytes(StandardCharsets.UTF_8)); + Mockito.when( + secureDataVersionService.getSecureFileVersionById( + "sdbId", "versionId", "category", "path")) + .thenReturn(Optional.of(secureFileVersion)); + + ResponseEntity responseEntity = secureFileController.getSecureFileVersion("versionId"); + Assert.assertEquals(HttpStatus.OK, responseEntity.getStatusCode()); + HttpHeaders httpHeaders = responseEntity.getHeaders(); + Assert.assertEquals("text/plain", httpHeaders.get("Content-Type").get(0)); + Assert.assertEquals( + "attachment; filename=\"sample.txt\"", httpHeaders.get("Content-Disposition").get(0)); + } + + @Test + public void testDeleteSecureFile() { + Mockito.when(sdbAccessRequest.getSdbId()).thenReturn("sdbId"); + Mockito.when(sdbAccessRequest.getPath()).thenReturn("path"); + CerberusPrincipal cerberusPrincipal = Mockito.mock(CerberusPrincipal.class); + Mockito.when(sdbAccessRequest.getPrincipal()).thenReturn(cerberusPrincipal); + Mockito.when(cerberusPrincipal.getName()).thenReturn("name"); + secureFileController.deleteSecureFile(); + Mockito.verify(secureDataService).deleteSecret("sdbId", "path", SecureDataType.FILE, "name"); + } + + @Test + public void testWriteSecureFile() throws IOException { + Mockito.when(sdbAccessRequest.getSdbId()).thenReturn("sdbId"); + Mockito.when(sdbAccessRequest.getPath()).thenReturn("path"); + CerberusPrincipal cerberusPrincipal = Mockito.mock(CerberusPrincipal.class); + Mockito.when(sdbAccessRequest.getPrincipal()).thenReturn(cerberusPrincipal); + Mockito.when(cerberusPrincipal.getName()).thenReturn("name"); + MultipartFile multipartFile = Mockito.mock(MultipartFile.class); + byte[] data = "data".getBytes(StandardCharsets.UTF_8); + Mockito.when(multipartFile.getBytes()).thenReturn(data); + secureFileController.writeSecureFile(multipartFile); + Mockito.verify(secureDataService).writeSecureFile("sdbId", "path", data, data.length, "name"); + } + + @Test + @SuppressFBWarnings + public void testWriteSecureFileThrowsException() throws IOException { + + MultipartFile multipartFile = Mockito.mock(MultipartFile.class); + String message = "no data"; + Mockito.when(multipartFile.getBytes()).thenThrow(new IOException(message)); + ApiException apiException = null; + + try { + secureFileController.writeSecureFile(multipartFile); + } catch (ApiException e) { + + apiException = e; + } + Assert.assertEquals("Failed to get contents from multipart file", apiException.getMessage()); + } +} diff --git a/cerberus-web/src/test/java/com/nike/cerberus/controller/SecureFilesSummaryControllerTest.java b/cerberus-web/src/test/java/com/nike/cerberus/controller/SecureFilesSummaryControllerTest.java new file mode 100644 index 000000000..11ebcca2f --- /dev/null +++ b/cerberus-web/src/test/java/com/nike/cerberus/controller/SecureFilesSummaryControllerTest.java @@ -0,0 +1,36 @@ +package com.nike.cerberus.controller; + +import com.nike.cerberus.domain.SecureFileSummaryResult; +import com.nike.cerberus.service.SecureDataService; +import com.nike.cerberus.util.SdbAccessRequest; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +public class SecureFilesSummaryControllerTest { + + @Mock private SecureDataService secureDataService; + @Mock private SdbAccessRequest sdbAccessRequest; + @InjectMocks private SecureFilesSummaryController secureFilesSummaryController; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testListSecureFiles() { + SecureFileSummaryResult secureFileSummaryResult = Mockito.mock(SecureFileSummaryResult.class); + Mockito.when(sdbAccessRequest.getSdbId()).thenReturn("sdbId"); + Mockito.when(sdbAccessRequest.getPath()).thenReturn("path"); + Mockito.when(secureDataService.listSecureFilesSummaries("sdbId", "path", 10, 10)) + .thenReturn(secureFileSummaryResult); + SecureFileSummaryResult actualSecureFileSummaryResult = + secureFilesSummaryController.listSecureFiles(10, 10); + Assert.assertSame(secureFileSummaryResult, actualSecureFileSummaryResult); + } +} diff --git a/cerberus-web/src/test/java/com/nike/cerberus/controller/admin/AdminActionsControllerTest.java b/cerberus-web/src/test/java/com/nike/cerberus/controller/admin/AdminActionsControllerTest.java new file mode 100644 index 000000000..0d3f200ee --- /dev/null +++ b/cerberus-web/src/test/java/com/nike/cerberus/controller/admin/AdminActionsControllerTest.java @@ -0,0 +1,71 @@ +package com.nike.cerberus.controller.admin; + +import com.nike.cerberus.domain.AuthKmsKeyMetadata; +import com.nike.cerberus.domain.AuthKmsKeyMetadataResult; +import com.nike.cerberus.domain.SDBMetadata; +import com.nike.cerberus.service.KmsService; +import com.nike.cerberus.service.SafeDepositBoxService; +import java.util.ArrayList; +import java.util.List; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.springframework.security.core.Authentication; + +public class AdminActionsControllerTest { + + @Mock private KmsService kmsService; + @Mock private SafeDepositBoxService safeDepositBoxService; + + @InjectMocks private AdminActionsController adminActionsController; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testGetAuthKmsKeyMetadataWhenKmsServiceReturnsEmptyMetadata() { + AuthKmsKeyMetadataResult authKmsKeyMetadata = adminActionsController.getAuthKmsKeyMetadata(); + Assert.assertNotNull(authKmsKeyMetadata); + Assert.assertEquals(0, authKmsKeyMetadata.getAuthenticationKmsKeyMetadata().size()); + } + + @Test + public void testGetAuthKmsKeyMetadataWhenKmsServiceReturnsNull() { + Mockito.when(kmsService.getAuthenticationKmsMetadata()).thenReturn(null); + AuthKmsKeyMetadataResult authKmsKeyMetadata = adminActionsController.getAuthKmsKeyMetadata(); + Assert.assertNotNull(authKmsKeyMetadata); + Assert.assertNull(authKmsKeyMetadata.getAuthenticationKmsKeyMetadata()); + } + + @Test + public void testGetAuthKmsKeyMetadataWhenKmsServiceReturnsListWithAuthenticationKmsData() { + AuthKmsKeyMetadata authKmsKeyMetadata = new AuthKmsKeyMetadata(); + List authKmsKeyMetadataList = new ArrayList<>(); + authKmsKeyMetadataList.add(authKmsKeyMetadata); + Mockito.when(kmsService.getAuthenticationKmsMetadata()).thenReturn(authKmsKeyMetadataList); + AuthKmsKeyMetadataResult authKmsKeyMetadataResult = + adminActionsController.getAuthKmsKeyMetadata(); + Assert.assertNotNull(authKmsKeyMetadataResult); + Assert.assertSame( + authKmsKeyMetadataList, authKmsKeyMetadataResult.getAuthenticationKmsKeyMetadata()); + Assert.assertSame( + authKmsKeyMetadata, authKmsKeyMetadataResult.getAuthenticationKmsKeyMetadata().get(0)); + } + + @Test + public void testOverrideOwner() { + SDBMetadata sdbMetadata = Mockito.mock(SDBMetadata.class); + Authentication authentication = Mockito.mock(Authentication.class); + Mockito.when(sdbMetadata.getName()).thenReturn("name"); + Mockito.when(sdbMetadata.getOwner()).thenReturn("owner"); + Mockito.when(authentication.getName()).thenReturn("name"); + adminActionsController.overrideSdbOwner(sdbMetadata, authentication); + Mockito.verify(safeDepositBoxService).overrideOwner("name", "owner", "name"); + } +} diff --git a/cerberus-web/src/test/java/com/nike/cerberus/controller/admin/SdbMetadataControllerTest.java b/cerberus-web/src/test/java/com/nike/cerberus/controller/admin/SdbMetadataControllerTest.java new file mode 100644 index 000000000..5553a11cd --- /dev/null +++ b/cerberus-web/src/test/java/com/nike/cerberus/controller/admin/SdbMetadataControllerTest.java @@ -0,0 +1,65 @@ +package com.nike.cerberus.controller.admin; + +import com.nike.cerberus.domain.SDBMetadata; +import com.nike.cerberus.domain.SDBMetadataResult; +import com.nike.cerberus.service.MetadataService; +import com.nike.cerberus.service.SafeDepositBoxService; +import java.util.Optional; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.springframework.security.core.Authentication; + +public class SdbMetadataControllerTest { + + @Mock private MetadataService metadataService; + + @Mock private SafeDepositBoxService safeDepositBoxService; + + @InjectMocks private SdbMetadataController sdbMetadataController; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testGetMetadata() { + SDBMetadataResult sdbMetadataResultMock = Mockito.mock(SDBMetadataResult.class); + Mockito.when(metadataService.getSDBMetadata(1, 2, "sdbNameFilter")) + .thenReturn(sdbMetadataResultMock); + SDBMetadataResult sdbMetadataResult = sdbMetadataController.getMetadata(1, 2, "sdbNameFilter"); + Assert.assertSame(sdbMetadataResultMock, sdbMetadataResult); + } + + @Test + public void testRestoreSdbIncludingDataInRequest() { + SDBMetadata sdbMetadata = Mockito.mock(SDBMetadata.class); + Authentication authentication = Mockito.mock(Authentication.class); + Mockito.when(authentication.getName()).thenReturn("name"); + Mockito.when(sdbMetadata.getPath()).thenReturn("path"); + Mockito.when(safeDepositBoxService.getSafeDepositBoxIdByPath("path")) + .thenReturn(Optional.of("id")); + sdbMetadataController.restoreSdbIncludingDataInRequest(sdbMetadata, authentication); + Mockito.verify(safeDepositBoxService).deleteSafeDepositBox("id"); + Mockito.verify(metadataService).restoreMetadata(sdbMetadata, "name"); + } + + @Test + public void testRestoreSdbIncludingDataInRequestIfSafeBoIdIsNotPresent() { + SDBMetadata sdbMetadata = Mockito.mock(SDBMetadata.class); + Authentication authentication = Mockito.mock(Authentication.class); + Mockito.when(authentication.getName()).thenReturn("name"); + Mockito.when(sdbMetadata.getPath()).thenReturn("path"); + Mockito.when(safeDepositBoxService.getSafeDepositBoxIdByPath("path")) + .thenReturn(Optional.empty()); + sdbMetadataController.restoreSdbIncludingDataInRequest(sdbMetadata, authentication); + Mockito.verify(safeDepositBoxService, Mockito.never()) + .deleteSafeDepositBox(Mockito.anyString()); + Mockito.verify(metadataService).restoreMetadata(sdbMetadata, "name"); + } +} diff --git a/cerberus-web/src/test/java/com/nike/cerberus/controller/authentication/AwsIamStsAuthControllerTest.java b/cerberus-web/src/test/java/com/nike/cerberus/controller/authentication/AwsIamStsAuthControllerTest.java new file mode 100644 index 000000000..a6c09d27d --- /dev/null +++ b/cerberus-web/src/test/java/com/nike/cerberus/controller/authentication/AwsIamStsAuthControllerTest.java @@ -0,0 +1,100 @@ +package com.nike.cerberus.controller.authentication; + +import com.amazonaws.services.securitytoken.model.GetCallerIdentityResult; +import com.nike.backstopper.exception.ApiException; +import com.nike.cerberus.aws.sts.AwsStsClient; +import com.nike.cerberus.aws.sts.AwsStsHttpHeader; +import com.nike.cerberus.aws.sts.GetCallerIdentityResponse; +import com.nike.cerberus.domain.AuthTokenResponse; +import com.nike.cerberus.error.DefaultApiError; +import com.nike.cerberus.event.filter.AuditLoggingFilterDetails; +import com.nike.cerberus.service.AuthenticationService; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +public class AwsIamStsAuthControllerTest { + + @Mock private AuthenticationService authenticationService; + @Mock private AwsStsClient awsStsClient; + @Mock private AuditLoggingFilterDetails auditLoggingFilterDetails; + @InjectMocks private AwsIamStsAuthController awsIamStsAuthController; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + } + + @Test + @SuppressFBWarnings + public void testAuthenticateIfHeaderAmzDateIsNull() { + ApiException apiException = null; + try { + awsIamStsAuthController.authenticate(null, null, null); + } catch (ApiException e) { + apiException = e; + } + Assert.assertEquals( + DefaultApiError.MISSING_AWS_SIGNATURE_HEADERS, apiException.getApiErrors().get(0)); + } + + @Test + @SuppressFBWarnings + public void testAuthenticateIfHeaderAmzSecurityTokenIsNull() { + ApiException apiException = null; + try { + awsIamStsAuthController.authenticate("date", null, null); + } catch (ApiException e) { + apiException = e; + } + Assert.assertEquals( + DefaultApiError.MISSING_AWS_SIGNATURE_HEADERS, apiException.getApiErrors().get(0)); + } + + @Test + public void testAuthenticate() { + GetCallerIdentityResponse getCallerIdentityResponse = + Mockito.mock(GetCallerIdentityResponse.class); + GetCallerIdentityResult getCallerIdentityResult = Mockito.mock(GetCallerIdentityResult.class); + Mockito.when(getCallerIdentityResponse.getGetCallerIdentityResult()) + .thenReturn(getCallerIdentityResult); + Mockito.when(getCallerIdentityResult.getArn()).thenReturn("arn"); + Mockito.when(awsStsClient.getCallerIdentity(Mockito.any(AwsStsHttpHeader.class))) + .thenReturn(getCallerIdentityResponse); + AuthTokenResponse authTokenResponse = Mockito.mock(AuthTokenResponse.class); + Mockito.when(authenticationService.stsAuthenticate("arn")).thenReturn(authTokenResponse); + AuthTokenResponse actualAuthTokenResponse = + awsIamStsAuthController.authenticate("date", "token", "authorization"); + Assert.assertSame(authTokenResponse, actualAuthTokenResponse); + Mockito.verify(auditLoggingFilterDetails) + .setAction("Successfully authenticated with AWS IAM STS Auth"); + } + + @Test + public void testAuthenticateWhenSTSAuthenticateThrowsException() { + GetCallerIdentityResponse getCallerIdentityResponse = + Mockito.mock(GetCallerIdentityResponse.class); + GetCallerIdentityResult getCallerIdentityResult = Mockito.mock(GetCallerIdentityResult.class); + Mockito.when(getCallerIdentityResponse.getGetCallerIdentityResult()) + .thenReturn(getCallerIdentityResult); + Mockito.when(getCallerIdentityResult.getArn()).thenReturn("arn"); + Mockito.when(awsStsClient.getCallerIdentity(Mockito.any(AwsStsHttpHeader.class))) + .thenReturn(getCallerIdentityResponse); + RuntimeException runtimeException = new RuntimeException(); + Mockito.when(authenticationService.stsAuthenticate("arn")).thenThrow(runtimeException); + RuntimeException actualException = null; + try { + awsIamStsAuthController.authenticate("date", "token", "authorization"); + } catch (RuntimeException e) { + actualException = e; + } + Assert.assertSame(runtimeException, actualException); + Mockito.verify(auditLoggingFilterDetails) + .setAction("Failed to authenticate with AWS IAM STS Auth"); + } +} diff --git a/cerberus-web/src/test/java/com/nike/cerberus/controller/authentication/RevokeAuthenticationControllerTest.java b/cerberus-web/src/test/java/com/nike/cerberus/controller/authentication/RevokeAuthenticationControllerTest.java new file mode 100644 index 000000000..eaa5678c0 --- /dev/null +++ b/cerberus-web/src/test/java/com/nike/cerberus/controller/authentication/RevokeAuthenticationControllerTest.java @@ -0,0 +1,32 @@ +package com.nike.cerberus.controller.authentication; + +import com.nike.cerberus.security.CerberusPrincipal; +import com.nike.cerberus.service.AuthenticationService; +import java.time.OffsetDateTime; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +public class RevokeAuthenticationControllerTest { + + @Mock private AuthenticationService authenticationService; + + @InjectMocks private RevokeAuthenticationController revokeAuthenticationController; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + } + + @Test + public void testRevokeAuthentication() { + OffsetDateTime now = OffsetDateTime.now(); + CerberusPrincipal cerberusPrincipal = Mockito.mock(CerberusPrincipal.class); + Mockito.when(cerberusPrincipal.getTokenExpires()).thenReturn(now); + revokeAuthenticationController.revokeAuthentication(cerberusPrincipal); + Mockito.verify(authenticationService).revoke(cerberusPrincipal, now); + } +} diff --git a/cerberus-web/src/test/java/com/nike/cerberus/controller/authentication/UserAuthenticationControllerTest.java b/cerberus-web/src/test/java/com/nike/cerberus/controller/authentication/UserAuthenticationControllerTest.java new file mode 100644 index 000000000..f5b062630 --- /dev/null +++ b/cerberus-web/src/test/java/com/nike/cerberus/controller/authentication/UserAuthenticationControllerTest.java @@ -0,0 +1,134 @@ +package com.nike.cerberus.controller.authentication; + +import com.nike.backstopper.exception.ApiException; +import com.nike.cerberus.auth.connector.AuthResponse; +import com.nike.cerberus.domain.MfaCheckRequest; +import com.nike.cerberus.domain.UserCredentials; +import com.nike.cerberus.error.DefaultApiError; +import com.nike.cerberus.event.filter.AuditLoggingFilterDetails; +import com.nike.cerberus.security.CerberusPrincipal; +import com.nike.cerberus.service.AuthenticationService; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +public class UserAuthenticationControllerTest { + + @Mock private AuthenticationService authenticationService; + @Mock private AuditLoggingFilterDetails auditLoggingFilterDetails; + + @InjectMocks private UserAuthenticationController userAuthenticationController; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + } + + @Test + @SuppressFBWarnings + public void testAuthenticateWhenAuthHeaderIsNull() { + ApiException apiException = null; + try { + userAuthenticationController.authenticate(null); + } catch (ApiException e) { + apiException = e; + } + Assert.assertEquals(DefaultApiError.AUTH_BAD_CREDENTIALS, apiException.getApiErrors().get(0)); + } + + @Test + @SuppressFBWarnings + public void testAuthenticateWhenAuthHeaderIsEmpty() { + ApiException apiException = null; + try { + userAuthenticationController.authenticate(""); + } catch (ApiException e) { + apiException = e; + } + Assert.assertEquals(DefaultApiError.AUTH_BAD_CREDENTIALS, apiException.getApiErrors().get(0)); + } + + @Test + @SuppressFBWarnings + public void testAuthenticateWhenAuthHeaderIsDoesNotStartWithBasic() { + ApiException apiException = null; + try { + userAuthenticationController.authenticate("token"); + } catch (ApiException e) { + apiException = e; + } + Assert.assertEquals(DefaultApiError.AUTH_BAD_CREDENTIALS, apiException.getApiErrors().get(0)); + } + + @Test + @SuppressFBWarnings + public void testAuthenticateWhenAuthenticationServiceAuthenticateThrowsException() { + ApiException apiException = null; + byte[] encodedBytes = + Base64.getEncoder().encode("username:password".getBytes(StandardCharsets.UTF_8)); + Mockito.when(authenticationService.authenticate(Mockito.any(UserCredentials.class))) + .thenThrow(ApiException.newBuilder().withApiErrors(DefaultApiError.LOGIN_FAILED).build()); + try { + userAuthenticationController.authenticate( + "Basic" + new String(encodedBytes, StandardCharsets.UTF_8)); + } catch (ApiException e) { + apiException = e; + } + Assert.assertEquals(DefaultApiError.LOGIN_FAILED, apiException.getApiErrors().get(0)); + Mockito.verify(auditLoggingFilterDetails).setAction("Failed to authenticate"); + } + + @Test + public void testAuthenticate() { + byte[] encodedBytes = + Base64.getEncoder().encode("username:password".getBytes(StandardCharsets.UTF_8)); + AuthResponse authResponse = Mockito.mock(AuthResponse.class); + Mockito.when(authenticationService.authenticate(Mockito.any(UserCredentials.class))) + .thenReturn(authResponse); + AuthResponse actualAuthResponse = + userAuthenticationController.authenticate( + "Basic" + new String(encodedBytes, StandardCharsets.UTF_8)); + Assert.assertSame(authResponse, actualAuthResponse); + Mockito.verify(auditLoggingFilterDetails).setAction("Authenticated"); + } + + @Test + public void testHandleMfaCheckWhenRequestIsPush() { + MfaCheckRequest mfaCheckRequest = Mockito.mock(MfaCheckRequest.class); + Mockito.when(mfaCheckRequest.isPush()).thenReturn(true); + userAuthenticationController.handleMfaCheck(mfaCheckRequest); + Mockito.verify(authenticationService).triggerPush(mfaCheckRequest); + } + + @Test + public void testHandleMfaCheckWhenOtpTokenIsEmpty() { + MfaCheckRequest mfaCheckRequest = Mockito.mock(MfaCheckRequest.class); + userAuthenticationController.handleMfaCheck(mfaCheckRequest); + Mockito.verify(authenticationService, Mockito.never()).triggerPush(mfaCheckRequest); + Mockito.verify(authenticationService).triggerChallenge(mfaCheckRequest); + } + + @Test + public void testHandleMfaCheckWhenOtpTokenIsNotEmpty() { + MfaCheckRequest mfaCheckRequest = Mockito.mock(MfaCheckRequest.class); + Mockito.when(mfaCheckRequest.getOtpToken()).thenReturn("token"); + userAuthenticationController.handleMfaCheck(mfaCheckRequest); + Mockito.verify(authenticationService, Mockito.never()).triggerPush(mfaCheckRequest); + Mockito.verify(authenticationService, Mockito.never()).triggerChallenge(mfaCheckRequest); + Mockito.verify(authenticationService).mfaCheck(mfaCheckRequest); + } + + @Test + public void testRefreshToken() { + CerberusPrincipal cerberusPrincipal = Mockito.mock(CerberusPrincipal.class); + userAuthenticationController.refreshToken(cerberusPrincipal); + Mockito.verify(authenticationService).refreshUserToken(cerberusPrincipal); + } +} diff --git a/cerberus-web/src/test/java/com/nike/cerberus/jwt/CerberusSigningKeyResolverTest.java b/cerberus-web/src/test/java/com/nike/cerberus/jwt/CerberusSigningKeyResolverTest.java new file mode 100644 index 000000000..c5b6e429f --- /dev/null +++ b/cerberus-web/src/test/java/com/nike/cerberus/jwt/CerberusSigningKeyResolverTest.java @@ -0,0 +1,167 @@ +package com.nike.cerberus.jwt; + +import static org.junit.Assert.*; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.nike.cerberus.service.ConfigService; +import com.nike.cerberus.util.UuidSupplier; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwsHeader; +import io.jsonwebtoken.impl.DefaultClaims; +import io.jsonwebtoken.impl.DefaultJwsHeader; +import java.security.Key; +import java.util.LinkedList; +import java.util.List; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; + +public class CerberusSigningKeyResolverTest { + @Mock + private CerberusSigningKeyResolver.JwtServiceOptionalPropertyHolder + jwtServiceOptionalPropertyHolder; + + @Mock private ConfigService configService; + + @Mock private ObjectMapper objectMapper; + + @Mock private UuidSupplier uuidSupplier; + + private CerberusSigningKeyResolver cerberusSigningKeyResolver; + + private JwtSecretData jwtSecretData = new JwtSecretData(); + + private String configStoreJwtSecretData; + + @Before + public void setUp() throws Exception { + initMocks(this); + configStoreJwtSecretData = "foo"; + LinkedList jwtSecrets = new LinkedList<>(); + jwtSecretData.setJwtSecrets(jwtSecrets); + + JwtSecret jwtSecret1 = new JwtSecret(); + jwtSecret1.setCreatedTs(100); + jwtSecret1.setEffectiveTs(200); + jwtSecret1.setId("key id 1"); + jwtSecret1.setSecret( + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="); + jwtSecrets.add(jwtSecret1); + + JwtSecret jwtSecret2 = new JwtSecret(); + jwtSecret2.setCreatedTs(300); + jwtSecret2.setEffectiveTs(400); + jwtSecret2.setId("key id 2"); + jwtSecret2.setSecret( + "BAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="); + jwtSecrets.add(jwtSecret2); + + JwtSecret jwtSecret3 = new JwtSecret(); + jwtSecret3.setCreatedTs(500); + jwtSecret3.setEffectiveTs(600); + jwtSecret3.setId("key id 3"); + jwtSecret3.setSecret( + "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="); + jwtSecrets.add(jwtSecret3); + + when(objectMapper.readValue(anyString(), same(JwtSecretData.class))).thenReturn(jwtSecretData); + when(configService.getJwtSecrets()).thenReturn(configStoreJwtSecretData); + + cerberusSigningKeyResolver = + new CerberusSigningKeyResolver( + jwtServiceOptionalPropertyHolder, + objectMapper, + java.util.Optional.of(configService), + false, + false, + uuidSupplier); + } + + @Test + public void test_get_future_jwt_secrets() { + List futureJwtSecrets = + cerberusSigningKeyResolver.getFutureJwtSecrets(jwtSecretData, 300); + assertEquals(2, futureJwtSecrets.size()); + assertEquals("key id 2", futureJwtSecrets.get(0).getId()); + } + + @Test + public void test_get_current_key_id() { + String keyId = cerberusSigningKeyResolver.getSigningKeyId(jwtSecretData, 700); + assertEquals("key id 3", keyId); + } + + @Test + public void test_get_current_key_id_with_future_key() { + String keyId = cerberusSigningKeyResolver.getSigningKeyId(jwtSecretData, 500); + assertEquals("key id 2", keyId); + } + + @Test(expected = IllegalArgumentException.class) + public void test_refresh_with_weak_key_should_throw_exception() { + LinkedList jwtSecrets = + cerberusSigningKeyResolver.getJwtSecretData().getJwtSecrets(); + JwtSecret jwtSecret = new JwtSecret(); + jwtSecret.setCreatedTs(500); + jwtSecret.setEffectiveTs(600); + jwtSecret.setId("key id weak"); + jwtSecret.setSecret("AAA=="); + jwtSecrets.add(jwtSecret); + + cerberusSigningKeyResolver.refresh(); + } + + @Test(expected = IllegalArgumentException.class) + public void test_refresh_with_empty_kid_should_throw_exception() { + LinkedList jwtSecrets = + cerberusSigningKeyResolver.getJwtSecretData().getJwtSecrets(); + JwtSecret jwtSecret = new JwtSecret(); + jwtSecret.setCreatedTs(500); + jwtSecret.setEffectiveTs(600); + jwtSecret.setId(""); + jwtSecret.setSecret( + "DAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="); + jwtSecrets.add(jwtSecret); + + cerberusSigningKeyResolver.refresh(); + } + + @Test + public void test_resolve_signing_key_returns_newest_key() { + CerberusJwtKeySpec keySpec = cerberusSigningKeyResolver.resolveSigningKey(); + assertEquals("key id 3", keySpec.getKid()); + assertEquals("HmacSHA512", keySpec.getAlgorithm()); + assertEquals(8, keySpec.getEncoded()[0]); // 8 == "CA" + } + + @Test + public void test_resolve_signing_key_returns_correct_key() { + JwsHeader jwsHeader = new DefaultJwsHeader(); + jwsHeader.setAlgorithm("HS512"); + jwsHeader.setKeyId("key id 2"); + Claims claims = new DefaultClaims(); + Key key = cerberusSigningKeyResolver.resolveSigningKey(jwsHeader, claims); + assertEquals(key.getAlgorithm(), "HmacSHA512"); + assertEquals(4, key.getEncoded()[0]); // 4 == "BA" + } + + @Test(expected = IllegalArgumentException.class) + public void test_resolve_signing_key_throws_error_on_invalid_key_id() { + JwsHeader jwsHeader = new DefaultJwsHeader(); + jwsHeader.setAlgorithm("HS512"); + jwsHeader.setKeyId("key id 666"); + Claims claims = new DefaultClaims(); + cerberusSigningKeyResolver.resolveSigningKey(jwsHeader, claims); + } + + @Test(expected = IllegalArgumentException.class) + public void test_resolve_signing_key_throws_error_on_invalid_algorithm() { + JwsHeader jwsHeader = new DefaultJwsHeader(); + jwsHeader.setAlgorithm("none"); + Claims claims = new DefaultClaims(); + cerberusSigningKeyResolver.resolveSigningKey(jwsHeader, claims); + } +} diff --git a/cerberus-web/src/test/java/com/nike/cerberus/record/RecordPojoTest.java b/cerberus-web/src/test/java/com/nike/cerberus/record/RecordPojoTest.java index 8b82eb7a5..b322db9be 100644 --- a/cerberus-web/src/test/java/com/nike/cerberus/record/RecordPojoTest.java +++ b/cerberus-web/src/test/java/com/nike/cerberus/record/RecordPojoTest.java @@ -35,7 +35,7 @@ public void test_pojo_structure_and_behavior() { List pojoClasses = PojoClassFactory.getPojoClasses("com.nike.cerberus.record"); - Assert.assertEquals(15, pojoClasses.size()); + Assert.assertEquals(16, pojoClasses.size()); Validator validator = ValidatorBuilder.create() diff --git a/cerberus-web/src/test/java/com/nike/cerberus/service/AuthTokenServiceTest.java b/cerberus-web/src/test/java/com/nike/cerberus/service/AuthTokenServiceTest.java index b943a915f..3774400f8 100644 --- a/cerberus-web/src/test/java/com/nike/cerberus/service/AuthTokenServiceTest.java +++ b/cerberus-web/src/test/java/com/nike/cerberus/service/AuthTokenServiceTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2020 Nike, inc. + * Copyright (c) 2021 Nike, inc. * * Licensed under the Apache License, Version 2.0 (the "License") * you may not use this file except in compliance with the License. @@ -18,15 +18,22 @@ import static junit.framework.TestCase.assertEquals; import static junit.framework.TestCase.assertTrue; +import static org.mockito.Matchers.any; import static org.mockito.Matchers.argThat; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; +import com.nike.backstopper.exception.ApiException; import com.nike.cerberus.PrincipalType; import com.nike.cerberus.dao.AuthTokenDao; +import com.nike.cerberus.domain.AuthTokenAcceptType; +import com.nike.cerberus.domain.AuthTokenIssueType; import com.nike.cerberus.domain.CerberusAuthToken; +import com.nike.cerberus.error.AuthTokenTooLongException; +import com.nike.cerberus.jwt.CerberusJwtClaims; import com.nike.cerberus.record.AuthTokenRecord; +import com.nike.cerberus.security.CerberusPrincipal; import com.nike.cerberus.util.AuthTokenGenerator; import com.nike.cerberus.util.DateTimeSupplier; import com.nike.cerberus.util.TokenHasher; @@ -47,6 +54,9 @@ public class AuthTokenServiceTest { @Mock private AuthTokenGenerator authTokenGenerator; @Mock private AuthTokenDao authTokenDao; @Mock private DateTimeSupplier dateTimeSupplier; + @Mock private JwtService jwtService; + @Mock private JwtFeatureFlags tokenFlag; + @Mock private CerberusPrincipal cerberusPrincipal; AuthTokenService authTokenService; @@ -56,7 +66,15 @@ public void before() { authTokenService = new AuthTokenService( - uuidSupplier, tokenHasher, authTokenGenerator, authTokenDao, dateTimeSupplier); + uuidSupplier, + tokenHasher, + authTokenGenerator, + authTokenDao, + dateTimeSupplier, + jwtService, + tokenFlag); + + when(tokenFlag.getAcceptType()).thenReturn(AuthTokenAcceptType.ALL); } @Test @@ -68,8 +86,9 @@ public void before() { final String fakeHash = "kjadlkfjasdlkf;jlkj1243asdfasdf"; String principal = "test-user@domain.com"; String groups = "group1,group2,group3"; - + when(tokenFlag.getIssueType()).thenReturn(AuthTokenIssueType.SESSION); when(uuidSupplier.get()).thenReturn(id); + when(authTokenGenerator.generateSecureToken()).thenReturn(expectedTokenId); when(dateTimeSupplier.get()).thenReturn(now); when(tokenHasher.hashToken(expectedTokenId)).thenReturn(fakeHash); @@ -105,9 +124,44 @@ public boolean matches(Object argument) { } @Test - public void test_that_getCerberusAuthToken_returns_emtpy_if_token_not_present() { + public void test_that_generateToken_attempts_to_write_a_jwt_and_returns_proper_object() + throws AuthTokenTooLongException { + String id = UUID.randomUUID().toString(); + String expectedTokenId = "abc-123-def-456"; + OffsetDateTime now = OffsetDateTime.now(); + String principal = "test-user@domain.com"; + String groups = "group1,group2,group3"; + when(tokenFlag.getIssueType()).thenReturn(AuthTokenIssueType.JWT); + when(uuidSupplier.get()).thenReturn(id); + when(jwtService.generateJwtToken(any())).thenReturn(expectedTokenId); + + when(dateTimeSupplier.get()).thenReturn(now); + + CerberusAuthToken token = + authTokenService.generateToken(principal, PrincipalType.USER, false, groups, 5, 0); + + assertEquals( + "The token should have the un-hashed value returned", expectedTokenId, token.getToken()); + assertEquals("The token should have a created date of now", now, token.getCreated()); + assertEquals( + "The token should expire ttl minutes after now", now.plusMinutes(5), token.getExpires()); + assertEquals("The token should have the proper principal", principal, token.getPrincipal()); + assertEquals( + "The token should be the principal type that was passed in", + PrincipalType.USER, + token.getPrincipalType()); + assertEquals("The token should not have access to admin endpoints", false, token.isAdmin()); + assertEquals( + "The token should have the groups that where passed in", groups, token.getGroups()); + assertEquals( + "The newly created token should have a refresh count of 0", 0, token.getRefreshCount()); + } + + @Test + public void test_that_getCerberusAuthToken_returns_empty_if_token_not_present() { final String tokenId = "abc-123-def-456"; final String fakeHash = "kjadlkfjasdlkf;jlkj1243asdfasdf"; + when(tokenHasher.hashToken(tokenId)).thenReturn(fakeHash); when(authTokenDao.getAuthTokenFromHash(fakeHash)).thenReturn(Optional.empty()); @@ -116,9 +170,35 @@ public void test_that_getCerberusAuthToken_returns_emtpy_if_token_not_present() } @Test - public void test_that_when_a_token_is_expired_empty_is_returned() { + public void test_that_getCerberusAuthToken_returns_empty_if_JWT_not_present() { + final String tokenId = "abc.123.def"; + + when(jwtService.isJwt(tokenId)).thenReturn(true); + when(jwtService.parseAndValidateToken(tokenId)).thenReturn(Optional.empty()); + + Optional tokenOptional = authTokenService.getCerberusAuthToken(tokenId); + assertTrue("optional should be empty", !tokenOptional.isPresent()); + } + + @Test(expected = ApiException.class) + public void test_that_auth_token_too_long_error_is_caught_correctly_for_JWT() + throws AuthTokenTooLongException { + String principal = "test-user@domain.com"; + String groups = "group1,group2,group3"; + OffsetDateTime now = OffsetDateTime.now(); + when(dateTimeSupplier.get()).thenReturn(now); + when(tokenFlag.getIssueType()).thenReturn(AuthTokenIssueType.JWT); + when(tokenFlag.getAcceptType()).thenReturn(AuthTokenAcceptType.JWT); + when(jwtService.generateJwtToken(any())) + .thenThrow(new AuthTokenTooLongException("auth token too long")); + authTokenService.generateToken(principal, PrincipalType.USER, false, groups, 5, 0); + } + + @Test + public void test_that_when_a_token_is_expired_empty_is_returned_session() { final String tokenId = "abc-123-def-456"; final String fakeHash = "kjadlkfjasdlkf;jlkj1243asdfasdf"; + when(tokenHasher.hashToken(tokenId)).thenReturn(fakeHash); when(authTokenDao.getAuthTokenFromHash(fakeHash)) .thenReturn( @@ -128,9 +208,22 @@ public void test_that_when_a_token_is_expired_empty_is_returned() { assertTrue("optional should be empty", !tokenOptional.isPresent()); } + @Test + public void test_that_when_a_token_is_expired_empty_is_returned_jwt() { + final String tokenId = "abc.123.def"; + + when(jwtService.isJwt(tokenId)).thenReturn(true); + when(jwtService.parseAndValidateToken(tokenId)) + .thenReturn( + Optional.of(new CerberusJwtClaims().setExpiresTs(OffsetDateTime.now().minusHours(1)))); + + Optional tokenOptional = authTokenService.getCerberusAuthToken(tokenId); + assertTrue("optional should be empty", !tokenOptional.isPresent()); + } + @Test public void - test_that_when_a_valid_non_expired_token_record_is_present_the_optional_is_populated_with_valid_token_object() { + test_that_when_a_valid_non_expired_token_record_is_present_the_optional_is_populated_with_valid_token_object_session() { String id = UUID.randomUUID().toString(); String tokenId = "abc-123-def-456"; OffsetDateTime now = OffsetDateTime.now(); @@ -167,16 +260,66 @@ public void test_that_when_a_token_is_expired_empty_is_returned() { assertEquals(0, token.getRefreshCount()); } + @Test + public void + test_that_when_a_valid_non_expired_token_record_is_present_the_optional_is_populated_with_valid_token_object_jwt() { + String id = UUID.randomUUID().toString(); + String tokenId = "abc.123.def"; + OffsetDateTime now = OffsetDateTime.now(); + String principal = "test-user@domain.com"; + String groups = "group1,group2,group3"; + + when(jwtService.isJwt(tokenId)).thenReturn(true); + when(jwtService.parseAndValidateToken(tokenId)) + .thenReturn( + Optional.of( + new CerberusJwtClaims() + .setId(id) + .setCreatedTs(now) + .setExpiresTs(now.plusHours(1)) + .setPrincipal(principal) + .setPrincipalType(PrincipalType.USER.getName()) + .setIsAdmin(false) + .setGroups(groups) + .setRefreshCount(0))); + + Optional tokenOptional = authTokenService.getCerberusAuthToken(tokenId); + + CerberusAuthToken token = + tokenOptional.orElseThrow(() -> new AssertionFailedError("Token should be present")); + assertEquals(tokenId, token.getToken()); + assertEquals(now, token.getCreated()); + assertEquals(now.plusHours(1), token.getExpires()); + assertEquals(principal, token.getPrincipal()); + assertEquals(PrincipalType.USER, token.getPrincipalType()); + assertEquals(false, token.isAdmin()); + assertEquals(groups, token.getGroups()); + assertEquals(0, token.getRefreshCount()); + } + @Test public void test_that_revokeToken_calls_the_dao_with_the_hashed_token() { final String tokenId = "abc-123-def-456"; final String fakeHash = "kjadlkfjasdlkf;jlkj1243asdfasdf"; + OffsetDateTime now = OffsetDateTime.now(); when(tokenHasher.hashToken(tokenId)).thenReturn(fakeHash); - - authTokenService.revokeToken(tokenId); + when(cerberusPrincipal.getToken()).thenReturn(tokenId); + authTokenService.revokeToken(cerberusPrincipal, now); verify(authTokenDao).deleteAuthTokenFromHash(fakeHash); } + @Test + public void test_that_revokeToken_calls_the_jwt_revoke_token() { + String tokenId = "abcdef"; + String token = "abc.123.def"; + OffsetDateTime now = OffsetDateTime.now(); + when(jwtService.isJwt(token)).thenReturn(true); + when(cerberusPrincipal.getTokenId()).thenReturn(tokenId); + when(cerberusPrincipal.getToken()).thenReturn(token); + authTokenService.revokeToken(cerberusPrincipal, now); + verify(jwtService).revokeToken(tokenId, now); + } + @Test public void test_that_deleteExpiredTokens_directly_proxies_dao() { int maxDelete = 1; diff --git a/cerberus-web/src/test/java/com/nike/cerberus/service/AuthenticationServiceTest.java b/cerberus-web/src/test/java/com/nike/cerberus/service/AuthenticationServiceTest.java index 11bfe73ae..0b14df4c4 100644 --- a/cerberus-web/src/test/java/com/nike/cerberus/service/AuthenticationServiceTest.java +++ b/cerberus-web/src/test/java/com/nike/cerberus/service/AuthenticationServiceTest.java @@ -431,12 +431,12 @@ public void tests_that_refreshUserToken_refreshes_token_when_count_is_less_than_ Integer curCount = MAX_LIMIT - 1; CerberusAuthToken authToken = - CerberusAuthToken.builder() - .principalType(PrincipalType.USER) - .principal("principal") - .groups("group1,group2") - .refreshCount(curCount) - .token(UUID.randomUUID().toString()) + CerberusAuthToken.Builder.create() + .withPrincipalType(PrincipalType.USER) + .withPrincipal("principal") + .withGroups("group1,group2") + .withRefreshCount(curCount) + .withToken(UUID.randomUUID().toString()) .build(); CerberusPrincipal principal = new CerberusPrincipal(authToken); @@ -445,14 +445,14 @@ public void tests_that_refreshUserToken_refreshes_token_when_count_is_less_than_ when(authTokenService.generateToken( anyString(), any(PrincipalType.class), anyBoolean(), anyString(), anyInt(), anyInt())) .thenReturn( - CerberusAuthToken.builder() - .principalType(PrincipalType.USER) - .principal("principal") - .groups("group1,group2") - .refreshCount(curCount + 1) - .token(UUID.randomUUID().toString()) - .created(now) - .expires(now.plusHours(1)) + CerberusAuthToken.Builder.create() + .withPrincipalType(PrincipalType.USER) + .withPrincipal("principal") + .withGroups("group1,group2") + .withRefreshCount(curCount + 1) + .withToken(UUID.randomUUID().toString()) + .withCreated(now) + .withExpires(now.plusHours(1)) .build()); AuthResponse response = authenticationService.refreshUserToken(principal); diff --git a/cerberus-web/src/test/java/com/nike/cerberus/service/JwtServiceTest.java b/cerberus-web/src/test/java/com/nike/cerberus/service/JwtServiceTest.java new file mode 100644 index 000000000..38271fb3a --- /dev/null +++ b/cerberus-web/src/test/java/com/nike/cerberus/service/JwtServiceTest.java @@ -0,0 +1,121 @@ +package com.nike.cerberus.service; + +import static org.junit.Assert.*; +import static org.mockito.Matchers.*; +import static org.mockito.Mockito.*; +import static org.mockito.MockitoAnnotations.initMocks; + +import com.google.common.collect.Sets; +import com.nike.cerberus.dao.JwtBlocklistDao; +import com.nike.cerberus.error.AuthTokenTooLongException; +import com.nike.cerberus.jwt.CerberusJwtClaims; +import com.nike.cerberus.jwt.CerberusJwtKeySpec; +import com.nike.cerberus.jwt.CerberusSigningKeyResolver; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwsHeader; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; +import java.util.Optional; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.springframework.test.util.ReflectionTestUtils; + +public class JwtServiceTest { + + @Mock private CerberusSigningKeyResolver signingKeyResolver; + + @Mock private JwtBlocklistDao jwtBlocklistDao; + + private JwtService jwtService; + + private CerberusJwtKeySpec cerberusJwtKeySpec; + + private CerberusJwtClaims cerberusJwtClaims; + + @Before + public void setUp() throws Exception { + initMocks(this); + jwtService = new JwtService(signingKeyResolver, "local", jwtBlocklistDao); + ReflectionTestUtils.setField(jwtService, "maxTokenLength", 1600); + cerberusJwtKeySpec = new CerberusJwtKeySpec(new byte[64], "HmacSHA512", "key id"); + cerberusJwtClaims = new CerberusJwtClaims(); + cerberusJwtClaims.setId("id"); + cerberusJwtClaims.setPrincipal("principal"); + cerberusJwtClaims.setGroups("groups"); + cerberusJwtClaims.setIsAdmin(true); + cerberusJwtClaims.setPrincipalType("type"); + cerberusJwtClaims.setRefreshCount(1); + cerberusJwtClaims.setCreatedTs(OffsetDateTime.of(2000, 1, 1, 1, 1, 1, 1, ZoneOffset.UTC)); + cerberusJwtClaims.setExpiresTs( + OffsetDateTime.of(3000, 1, 1, 1, 1, 1, 1, ZoneOffset.UTC)); // should be good for a while + + when(signingKeyResolver.resolveSigningKey()).thenReturn(cerberusJwtKeySpec); + when(signingKeyResolver.resolveSigningKey(any(JwsHeader.class), any(Claims.class))) + .thenReturn(cerberusJwtKeySpec); + } + + @Test + public void test_generate_jwt_token_parse_and_validate_claim() throws AuthTokenTooLongException { + String token = jwtService.generateJwtToken(cerberusJwtClaims); + assertEquals(3, token.split("\\.").length); + Optional cerberusJwtClaimsOptional = jwtService.parseAndValidateToken(token); + assertTrue(cerberusJwtClaimsOptional.isPresent()); + CerberusJwtClaims cerberusJwtClaims = cerberusJwtClaimsOptional.get(); + + assertEquals("id", cerberusJwtClaims.getId()); + assertEquals("principal", cerberusJwtClaims.getPrincipal()); + assertEquals("groups", cerberusJwtClaims.getGroups()); + assertEquals(true, cerberusJwtClaims.getIsAdmin()); + assertEquals("type", cerberusJwtClaims.getPrincipalType()); + assertEquals(1, (long) cerberusJwtClaims.getRefreshCount()); + assertEquals( + OffsetDateTime.of(2000, 1, 1, 1, 1, 1, 1, ZoneOffset.UTC).toEpochSecond(), + cerberusJwtClaims.getCreatedTs().toEpochSecond()); + assertEquals( + OffsetDateTime.of(3000, 1, 1, 1, 1, 1, 1, ZoneOffset.UTC).toEpochSecond(), + cerberusJwtClaims.getExpiresTs().toEpochSecond()); + } + + @Test + public void test_expired_token_returns_empty() throws AuthTokenTooLongException { + cerberusJwtClaims.setExpiresTs(OffsetDateTime.of(2000, 1, 1, 1, 1, 1, 1, ZoneOffset.UTC)); + String token = jwtService.generateJwtToken(cerberusJwtClaims); + Optional cerberusJwtClaims = jwtService.parseAndValidateToken(token); + assertFalse(cerberusJwtClaims.isPresent()); + } + + @Test + public void test_unsigned_token_returns_empty() { + String token = + "eyJraWQiOiJrZXkgaWQiLCJhbGciOiJIUzUxMiJ9.eyJqdGkiOiJpZCIsImlzcyI6ImxvY2FsIiwic3ViIjoicHJpbm" + + "NpcGFsIiwicHJpbmNpcGFsVHlwZSI6InR5cGUiLCJncm91cHMiOiJncm91cHMiLCJpc0FkbWluIjp0cnVlLCJyZWZyZXNoQ291" + + "bnQiOjEsImV4cCI6NDA3MDkxMjQ2MSwiaWF0Ijo5NDY2ODg0NjF9"; + Optional cerberusJwtClaims = jwtService.parseAndValidateToken(token); + assertFalse(cerberusJwtClaims.isPresent()); + } + + @Test(expected = AuthTokenTooLongException.class) + public void test_that_auth_token_too_long_error_is_thrown_correctly_for_JWT() + throws AuthTokenTooLongException { + ReflectionTestUtils.setField(jwtService, "maxTokenLength", 0); + jwtService.generateJwtToken(cerberusJwtClaims); + } + + @Test + public void test_parseAndValidateToken_returns_empty_for_blocklisted_token() + throws AuthTokenTooLongException { + String token = jwtService.generateJwtToken(cerberusJwtClaims); + when(jwtBlocklistDao.getBlocklist()).thenReturn(Sets.newHashSet("id")); + jwtService.refreshBlocklist(); + Optional cerberusJwtClaims = jwtService.parseAndValidateToken(token); + assertFalse(cerberusJwtClaims.isPresent()); + } + + @Test + public void test_that_revokeToken_calls_the_dao() { + final String tokenId = "abc-123-def-456"; + jwtService.revokeToken(tokenId, OffsetDateTime.now()); + verify(jwtBlocklistDao, times(1)).addToBlocklist(any()); + } +} diff --git a/dependency-check-supressions.xml b/dependency-check-supressions.xml index 17d7f43a9..23fc7bcb8 100644 --- a/dependency-check-supressions.xml +++ b/dependency-check-supressions.xml @@ -137,4 +137,11 @@ ^pkg:npm/faye\-websocket@.*$ CVE-2020-15133 + + + ^pkg:npm/postcss@.*$ + CVE-2021-23368 + diff --git a/gradle.properties b/gradle.properties index b363f9dbd..c864b7a24 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,5 +14,5 @@ # limitations under the License. # -version=4.10.3 +version=4.12.0 group=com.nike.cerberus