From 438a506742963458579a37c08f643a361e9d0f23 Mon Sep 17 00:00:00 2001 From: jonathan langlois Date: Tue, 23 Apr 2024 16:51:11 -0700 Subject: [PATCH 01/13] chore: authenticator add authenticator for session removal --- .../authenticators/UserSessionRemover.java | 53 ++++++++++++ .../UserSessionRemoverFactory.java | 82 +++++++++++++++++++ ...ycloak.authentication.AuthenticatorFactory | 1 + 3 files changed, 136 insertions(+) create mode 100644 docker/keycloak/extensions-7.6/services/src/main/java/com/github/bcgov/keycloak/authenticators/UserSessionRemover.java create mode 100644 docker/keycloak/extensions-7.6/services/src/main/java/com/github/bcgov/keycloak/authenticators/UserSessionRemoverFactory.java diff --git a/docker/keycloak/extensions-7.6/services/src/main/java/com/github/bcgov/keycloak/authenticators/UserSessionRemover.java b/docker/keycloak/extensions-7.6/services/src/main/java/com/github/bcgov/keycloak/authenticators/UserSessionRemover.java new file mode 100644 index 00000000..78772f00 --- /dev/null +++ b/docker/keycloak/extensions-7.6/services/src/main/java/com/github/bcgov/keycloak/authenticators/UserSessionRemover.java @@ -0,0 +1,53 @@ +package com.github.bcgov.keycloak.authenticators; + +import org.jboss.logging.Logger; +import org.keycloak.authentication.Authenticator; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.UserSessionProvider; +import org.keycloak.services.managers.AuthenticationManager; +import org.keycloak.authentication.AuthenticationFlowContext; + +public class UserSessionRemover implements Authenticator { + + private static final Logger logger = Logger.getLogger(UserSessionRemover.class); + + @Override + public boolean requiresUser() { + return false; + } + + @Override + public void authenticate(AuthenticationFlowContext context) { + + AuthenticationManager.AuthResult authResult = AuthenticationManager.authenticateIdentityCookie( + context.getSession(), context.getRealm(), true); + + if (authResult == null) { + context.attempted(); + return; + } + + UserSessionProvider userSessionProvider = context.getSession().sessions(); + userSessionProvider.removeUserSession(context.getRealm(), authResult.getSession()); + context.attempted(); + } + + @Override + public void action(AuthenticationFlowContext context) { + } + + @Override + public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { + return true; + } + + @Override + public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) { + } + + @Override + public void close() { + } +} diff --git a/docker/keycloak/extensions-7.6/services/src/main/java/com/github/bcgov/keycloak/authenticators/UserSessionRemoverFactory.java b/docker/keycloak/extensions-7.6/services/src/main/java/com/github/bcgov/keycloak/authenticators/UserSessionRemoverFactory.java new file mode 100644 index 00000000..79e1deab --- /dev/null +++ b/docker/keycloak/extensions-7.6/services/src/main/java/com/github/bcgov/keycloak/authenticators/UserSessionRemoverFactory.java @@ -0,0 +1,82 @@ +package com.github.bcgov.keycloak.authenticators; + +import java.util.List; +import org.keycloak.Config; +import org.keycloak.OAuth2Constants; +import org.keycloak.authentication.Authenticator; +import org.keycloak.authentication.AuthenticatorFactory; +import org.keycloak.authentication.DisplayTypeAuthenticatorFactory; +import org.keycloak.authentication.authenticators.AttemptedAuthenticator; +import org.keycloak.models.AuthenticationExecutionModel.Requirement; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.provider.ProviderConfigProperty; + +public class UserSessionRemoverFactory + implements AuthenticatorFactory, DisplayTypeAuthenticatorFactory { + + protected static final Requirement[] REQUIREMENT_CHOICES = { + Requirement.REQUIRED, Requirement.ALTERNATIVE, Requirement.DISABLED + }; + + @Override + public String getId() { + return "user-session-remover"; + } + + @Override + public String getDisplayType() { + return "User Session Remover"; + } + + @Override + public String getHelpText() { + return "Removes the user session."; + } + + @Override + public Authenticator create(KeycloakSession session) { + return new UserSessionRemover (); + } + + @Override + public Authenticator createDisplay(KeycloakSession session, String displayType) { + if (displayType == null) return new UserSessionRemover(); + if (!OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(displayType)) return null; + return AttemptedAuthenticator.SINGLETON; // ignore this authenticator + } + + @Override + public Requirement[] getRequirementChoices() { + return REQUIREMENT_CHOICES; + } + + @Override + public List getConfigProperties() { + return null; + } + + @Override + public String getReferenceCategory() { + return null; + } + + @Override + public boolean isConfigurable() { + return false; + } + + @Override + public boolean isUserSetupAllowed() { + return true; + } + + @Override + public void init(Config.Scope config) {} + + @Override + public void postInit(KeycloakSessionFactory factory) {} + + @Override + public void close() {} +} diff --git a/docker/keycloak/extensions-7.6/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory b/docker/keycloak/extensions-7.6/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory index 8ab8ef6c..7e6f2e7f 100755 --- a/docker/keycloak/extensions-7.6/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory +++ b/docker/keycloak/extensions-7.6/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory @@ -3,5 +3,6 @@ com.github.bcgov.keycloak.authenticators.CookieStopAuthenticatorFactory com.github.bcgov.keycloak.authenticators.ClientLoginAuthenticatorFactory com.github.bcgov.keycloak.authenticators.ClientLoginRoleBindingFactory com.github.bcgov.keycloak.authenticators.UserAttributeAuthenticatorFactory +com.github.bcgov.keycloak.authenticators.UserSessionRemoverFactory com.github.bcgov.keycloak.authenticators.broker.IdpDeleteUserIfDuplicateAuthenticatorFactory com.github.bcgov.keycloak.authenticators.browser.IdentityProviderStopFormFactory From 5512b166006ae4f15fe38c8e7c19435acc188e76 Mon Sep 17 00:00:00 2001 From: jonathan langlois Date: Wed, 24 Apr 2024 13:44:18 -0700 Subject: [PATCH 02/13] chore: cleanup remove unused method --- .../authenticators/UserSessionRemoverFactory.java | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/docker/keycloak/extensions-7.6/services/src/main/java/com/github/bcgov/keycloak/authenticators/UserSessionRemoverFactory.java b/docker/keycloak/extensions-7.6/services/src/main/java/com/github/bcgov/keycloak/authenticators/UserSessionRemoverFactory.java index 79e1deab..5b1e6f74 100644 --- a/docker/keycloak/extensions-7.6/services/src/main/java/com/github/bcgov/keycloak/authenticators/UserSessionRemoverFactory.java +++ b/docker/keycloak/extensions-7.6/services/src/main/java/com/github/bcgov/keycloak/authenticators/UserSessionRemoverFactory.java @@ -12,13 +12,14 @@ import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.provider.ProviderConfigProperty; -public class UserSessionRemoverFactory - implements AuthenticatorFactory, DisplayTypeAuthenticatorFactory { +public class UserSessionRemoverFactory implements AuthenticatorFactory { protected static final Requirement[] REQUIREMENT_CHOICES = { Requirement.REQUIRED, Requirement.ALTERNATIVE, Requirement.DISABLED }; + private static final Authenticator AUTHENTICATOR_INSTANCE = new UserSessionRemover(); + @Override public String getId() { return "user-session-remover"; @@ -36,14 +37,7 @@ public String getHelpText() { @Override public Authenticator create(KeycloakSession session) { - return new UserSessionRemover (); - } - - @Override - public Authenticator createDisplay(KeycloakSession session, String displayType) { - if (displayType == null) return new UserSessionRemover(); - if (!OAuth2Constants.DISPLAY_CONSOLE.equalsIgnoreCase(displayType)) return null; - return AttemptedAuthenticator.SINGLETON; // ignore this authenticator + return AUTHENTICATOR_INSTANCE; } @Override From 22d9c8096d86b0ba0bed37ed43b5a7f061de54ad Mon Sep 17 00:00:00 2001 From: Nithin Shekar Kuruba <81444731+NithinKuruba@users.noreply.github.com> Date: Wed, 24 Apr 2024 15:56:03 -0700 Subject: [PATCH 03/13] fix: fixed RC notifs and DC users only removed from STD realm (#346) --- docker/kc-cron-job/remove-dc-users.js | 53 ++++++------------- .../kc-cron-job/remove-inactive-idir-users.js | 26 ++++----- 2 files changed, 27 insertions(+), 52 deletions(-) mode change 100644 => 100755 docker/kc-cron-job/remove-dc-users.js diff --git a/docker/kc-cron-job/remove-dc-users.js b/docker/kc-cron-job/remove-dc-users.js old mode 100644 new mode 100755 index f2722310..c93df86e --- a/docker/kc-cron-job/remove-dc-users.js +++ b/docker/kc-cron-job/remove-dc-users.js @@ -3,9 +3,7 @@ const async = require('async'); const STANDARD_REALM = 'standard'; -const DC_REALM = 'digitalcredential'; - -async function removeVcUsers(runnerName, pgClient, env = 'dev', callback) { +async function removeDcUsers(runnerName, pgClient, env = 'dev', callback) { try { let deletedUserCount = 0; const adminClient = await getAdminClient(env); @@ -31,17 +29,6 @@ async function removeVcUsers(runnerName, pgClient, env = 'dev', callback) { // delete user from standard realm await adminClient.users.del({ realm: STANDARD_REALM, id }); - const parentRealmUsers = await adminClient.users.find({ - realm: DC_REALM, - username: username.split('@')[0], - max: 1 - }); - - if (parentRealmUsers.length > 0) { - // delete user from digital credential realm - await adminClient.users.del({ realm: DC_REALM, id: parentRealmUsers[0]?.id }); - } - const values = [env, username, STANDARD_REALM, users[x].attributes || {}]; await pgClient.query({ text, values }); deletedUserCount++; @@ -61,40 +48,34 @@ async function removeVcUsers(runnerName, pgClient, env = 'dev', callback) { callback(null, { runnerName, processed: total, deleteCount: deletedUserCount }); } catch (err) { handleError(err); - callback(err); + callback(JSON.stringify(err?.message || err?.response?.data || err), { runnerName }); } finally { await pgClient.end(); } } async function main() { async.parallel( - [ + async.reflectAll([ function (cb) { - removeVcUsers('dev', getPgClient(), 'dev', cb); + removeDcUsers('dev', getPgClient(), 'dev', cb); }, function (cb) { - removeVcUsers('test', getPgClient(), 'test', cb); + removeDcUsers('test', getPgClient(), 'test', cb); }, function (cb) { - removeVcUsers('prod', getPgClient(), 'prod', cb); - } - ], - async function (err, results) { - if (err) { - console.error(err.message); - await sendRcNotification( - 'dc-remove-users', - `**[${process.env.NAMESPACE}] Failed to remove digital credential users** \n\n` + err.message, - true - ); - } else { - const a = results.map((res) => JSON.stringify(res)); - await sendRcNotification( - 'dc-remove-users', - `**[${process.env.NAMESPACE}] Successfully removed digital credential users** \n\n` + a.join('\n\n'), - false - ); + removeDcUsers('prod', getPgClient(), 'prod', cb); } + ]), + async function (_, results) { + const hasError = results.find((r) => r.error); + const textContent = hasError ? 'Failed to remove' : 'Successfully removed'; + + await sendRcNotification( + 'dc-remove-users', + `**[${process.env.NAMESPACE}] ${textContent} digital credential users** \n\n` + + results.map((r) => JSON.stringify(r)).join('\n\n'), + hasError + ); } ); diff --git a/docker/kc-cron-job/remove-inactive-idir-users.js b/docker/kc-cron-job/remove-inactive-idir-users.js index fe0ff720..3b7b5514 100644 --- a/docker/kc-cron-job/remove-inactive-idir-users.js +++ b/docker/kc-cron-job/remove-inactive-idir-users.js @@ -233,9 +233,8 @@ async function removeStaleUsersByEnv(env = 'dev', pgClient, runnerName, startFro log(`[${runnerName}] ${total} users processed.`); callback(null, { runnerName, processed: total, deleteCount: deletedUserCount }); } catch (err) { - const error = { runnerName, err: JSON.stringify(err.message || err.response.data || err) }; handleError(err); - callback(JSON.stringify(error)); + callback(JSON.stringify(err?.message || err?.response?.data || err), { runnerName }); } finally { await pgClient.end(); } @@ -243,7 +242,7 @@ async function removeStaleUsersByEnv(env = 'dev', pgClient, runnerName, startFro async function main() { async.parallel( - [ + async.reflectAll([ function (cb) { removeStaleUsersByEnv('dev', getPgClient(), 'dev', 0, cb); }, @@ -265,21 +264,16 @@ async function main() { function (cb) { removeStaleUsersByEnv('prod', getPgClient(), 'prod-05', 40000, cb); } - ], - async function (errors, results) { - if (errors) { - console.error('errors', errors); - await sendRcNotification( - 'cron-remove-inactive-users', - `**[${process.env.NAMESPACE}] Failed to remove inactive users** \n\n` + errors, - false - ); - } - const responses = results.map((result) => JSON.stringify(result)); + ]), + async function (_, results) { + const hasError = results.find((r) => r.error); + const textContent = hasError ? 'Failed to remove' : 'Successfully removed'; + await sendRcNotification( 'cron-remove-inactive-users', - `**[${process.env.NAMESPACE}] Successfully removed inactive users** \n\n` + responses.join('\n\n'), - false + `**[${process.env.NAMESPACE}] ${textContent} inactive users** \n\n` + + results.map((r) => JSON.stringify(r)).join('\n\n'), + hasError ); } ); From 6d24ef01b264540cab7b87f861382b1b7d30cb67 Mon Sep 17 00:00:00 2001 From: jonathan langlois Date: Fri, 26 Apr 2024 14:53:36 -0700 Subject: [PATCH 04/13] test: session remover add unit test for session remover and check client id --- .../keycloak/extensions-7.6/services/pom.xml | 25 ++- .../authenticators/UserSessionRemover.java | 28 +++- .../UserSessionRemoverFactory.java | 2 +- .../UserSessionRemoverTest.java | 144 ++++++++++++++++++ 4 files changed, 192 insertions(+), 7 deletions(-) create mode 100644 docker/keycloak/extensions-7.6/services/src/test/java/com/github/bcgov/keycloak/authenticators/UserSessionRemoverTest.java diff --git a/docker/keycloak/extensions-7.6/services/pom.xml b/docker/keycloak/extensions-7.6/services/pom.xml index d07f8d23..396d9968 100644 --- a/docker/keycloak/extensions-7.6/services/pom.xml +++ b/docker/keycloak/extensions-7.6/services/pom.xml @@ -38,6 +38,14 @@ + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.0 + + --illegal-access=permit + + @@ -128,12 +136,13 @@ junit junit + 4.13.2 test org.mockito - mockito-all - 1.9.5 + mockito-core + 5.3.1 test @@ -141,5 +150,17 @@ hamcrest-all test + + org.junit.jupiter + junit-jupiter-engine + 5.9.1 + test + + + org.junit.jupiter + junit-jupiter-api + 5.9.1 + test + diff --git a/docker/keycloak/extensions-7.6/services/src/main/java/com/github/bcgov/keycloak/authenticators/UserSessionRemover.java b/docker/keycloak/extensions-7.6/services/src/main/java/com/github/bcgov/keycloak/authenticators/UserSessionRemover.java index 78772f00..ece04e61 100644 --- a/docker/keycloak/extensions-7.6/services/src/main/java/com/github/bcgov/keycloak/authenticators/UserSessionRemover.java +++ b/docker/keycloak/extensions-7.6/services/src/main/java/com/github/bcgov/keycloak/authenticators/UserSessionRemover.java @@ -8,6 +8,9 @@ import org.keycloak.models.UserSessionProvider; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.authentication.AuthenticationFlowContext; +import org.keycloak.sessions.AuthenticationSessionModel; + +import java.util.Map; public class UserSessionRemover implements Authenticator { @@ -20,17 +23,34 @@ public boolean requiresUser() { @Override public void authenticate(AuthenticationFlowContext context) { - + AuthenticationSessionModel session = context.getAuthenticationSession(); AuthenticationManager.AuthResult authResult = AuthenticationManager.authenticateIdentityCookie( - context.getSession(), context.getRealm(), true); + context.getSession(), + context.getRealm(), + true + ); + // 1. If no Cookie session, proceed to next step if (authResult == null) { context.attempted(); return; } - UserSessionProvider userSessionProvider = context.getSession().sessions(); - userSessionProvider.removeUserSession(context.getRealm(), authResult.getSession()); + // Need to use the KeycloakSession context to get the authenticating client ID. Not available on the AuthenticationFlowContext. + KeycloakSession keycloakSession = context.getSession(); + String authenticatingClientUUID = keycloakSession.getContext().getClient().getId(); + + // Get all existing sessions. If any session is associated with a different client, clear all user sessions. + UserSessionProvider userSessionProvider = keycloakSession.sessions(); + Map activeClientSessionStats = userSessionProvider.getActiveClientSessionStats(context.getRealm(), false); + + for (String activeSessionClientUUID : activeClientSessionStats.keySet()) { + if (!activeSessionClientUUID.equals(authenticatingClientUUID)) { + logger.info("REMOVING THE SESSIONS!"); + userSessionProvider.removeUserSession(context.getRealm(), authResult.getSession()); + } + } + context.attempted(); } diff --git a/docker/keycloak/extensions-7.6/services/src/main/java/com/github/bcgov/keycloak/authenticators/UserSessionRemoverFactory.java b/docker/keycloak/extensions-7.6/services/src/main/java/com/github/bcgov/keycloak/authenticators/UserSessionRemoverFactory.java index 5b1e6f74..d1ae5257 100644 --- a/docker/keycloak/extensions-7.6/services/src/main/java/com/github/bcgov/keycloak/authenticators/UserSessionRemoverFactory.java +++ b/docker/keycloak/extensions-7.6/services/src/main/java/com/github/bcgov/keycloak/authenticators/UserSessionRemoverFactory.java @@ -32,7 +32,7 @@ public String getDisplayType() { @Override public String getHelpText() { - return "Removes the user session."; + return "Checks if the user session is realted to any other client, and removes it if so."; } @Override diff --git a/docker/keycloak/extensions-7.6/services/src/test/java/com/github/bcgov/keycloak/authenticators/UserSessionRemoverTest.java b/docker/keycloak/extensions-7.6/services/src/test/java/com/github/bcgov/keycloak/authenticators/UserSessionRemoverTest.java new file mode 100644 index 00000000..58cc6128 --- /dev/null +++ b/docker/keycloak/extensions-7.6/services/src/test/java/com/github/bcgov/keycloak/authenticators/UserSessionRemoverTest.java @@ -0,0 +1,144 @@ +package com.github.bcgov.keycloak.testsuite.authenticators; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import org.mockito.Mockito; +import org.mockito.MockedStatic; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.BeforeEach; + +import org.jboss.logging.Logger; +import com.github.bcgov.keycloak.authenticators.UserSessionRemover; +import org.keycloak.authentication.AuthenticationFlowContext; +import org.keycloak.authentication.Authenticator; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.services.managers.AuthenticationManager; +import org.keycloak.models.UserModel; +import org.keycloak.models.ClientModel; +import org.keycloak.sessions.AuthenticationSessionModel; +import org.keycloak.models.UserSessionProvider; +import org.keycloak.models.UserSessionModel; +import org.keycloak.models.KeycloakContext; +import java.util.HashMap; +import java.util.Map; + +public class UserSessionRemoverTest { + private static final UserSessionRemover userSessionRemover = new UserSessionRemover(); + + private AuthenticationFlowContext context; + private KeycloakSession session; + private RealmModel realm; + private AuthenticationSessionModel authSession; + private UserSessionProvider userSessionProvider; + private KeycloakSession keycloakSession; + private ClientModel client; + private KeycloakContext keycloakContext; + private AuthenticationManager.AuthResult authResult; + + @BeforeEach + public void setup() { + // Initialize mocks for necessary objects + context = mock(AuthenticationFlowContext.class); + realm = mock(RealmModel.class); + authSession = mock(AuthenticationSessionModel.class); + userSessionProvider = mock(UserSessionProvider.class); + keycloakSession = mock(KeycloakSession.class); + keycloakContext = mock(KeycloakContext.class); + client = mock(ClientModel.class); + authResult = mock(AuthenticationManager.AuthResult.class); + + // Set up common behavior of the mocks + when(context.getSession()).thenReturn(keycloakSession); + when(context.getRealm()).thenReturn(realm); + when(context.getAuthenticationSession()).thenReturn(authSession); + when(keycloakSession.sessions()).thenReturn(userSessionProvider); + when(context.getSession()).thenReturn(keycloakSession); + when(keycloakSession.getContext()).thenReturn(keycloakContext); + when(keycloakContext.getClient()).thenReturn(client); + when(authResult.getSession()).thenReturn(mock(UserSessionModel.class)); + } + + @Test + public void testSkipClientSessionCheckWhenNullAuthResult() throws Exception { + try (MockedStatic authenticationManager = Mockito.mockStatic(AuthenticationManager.class)) { + authenticationManager.when(() -> AuthenticationManager.authenticateIdentityCookie( + any(KeycloakSession.class), any(RealmModel.class), any(Boolean.class) + )).thenReturn(null); + userSessionRemover.authenticate(context); + + // Keycloak Session Context check skipped if no Auth Session + verify(keycloakSession, times(0)).getContext(); + verify(userSessionProvider, times(0)).removeUserSession(any(RealmModel.class), any(UserSessionModel.class)); + } + } + + @Test + public void testRemovesUserSessionsWhenMultipleClientSessionsExist() throws Exception { + when(client.getId()).thenReturn("client1"); + Map activeClientSessionStats = new HashMap<>(); + activeClientSessionStats.put("client1", 1L); + activeClientSessionStats.put("client2", 2L); + + when(userSessionProvider.getActiveClientSessionStats(any(RealmModel.class), any(Boolean.class))).thenReturn(activeClientSessionStats); + + try (MockedStatic authenticationManager = Mockito.mockStatic(AuthenticationManager.class)) { + authenticationManager.when(() -> AuthenticationManager.authenticateIdentityCookie( + any(KeycloakSession.class), any(RealmModel.class), any(Boolean.class) + )).thenReturn(authResult); + + userSessionRemover.authenticate(context); + + verify(keycloakSession, times(1)).getContext(); + verify(userSessionProvider, times(1)).removeUserSession(any(RealmModel.class), any(UserSessionModel.class)); + } + } + + @Test + public void testRemovesUserSessionsWhenSingleDifferentClientSessionFound() throws Exception { + when(client.getId()).thenReturn("client1"); + Map activeClientSessionStats = new HashMap<>(); + activeClientSessionStats.put("client2", 2L); + + when(userSessionProvider.getActiveClientSessionStats(any(RealmModel.class), any(Boolean.class))).thenReturn(activeClientSessionStats); + + try (MockedStatic authenticationManager = Mockito.mockStatic(AuthenticationManager.class)) { + authenticationManager.when(() -> AuthenticationManager.authenticateIdentityCookie( + any(KeycloakSession.class), any(RealmModel.class), any(Boolean.class) + )).thenReturn(authResult); + userSessionRemover.authenticate(context); + + verify(keycloakSession, times(1)).getContext(); + verify(userSessionProvider, times(1)).removeUserSession(any(RealmModel.class), any(UserSessionModel.class)); + } + } + + @Test + public void testLeavesExistingSessionWhenOnlyAssociatedToAuthenticatingClient() throws Exception { + when(client.getId()).thenReturn("client1"); + Map activeClientSessionStats = new HashMap<>(); + + // Only active session matches authenticating client + activeClientSessionStats.put("client1", 1L); + + when(userSessionProvider.getActiveClientSessionStats(any(RealmModel.class), any(Boolean.class))).thenReturn(activeClientSessionStats); + + try (MockedStatic authenticationManager = Mockito.mockStatic(AuthenticationManager.class)) { + authenticationManager.when(() -> AuthenticationManager.authenticateIdentityCookie( + any(KeycloakSession.class), any(RealmModel.class), any(Boolean.class) + )).thenReturn(authResult); + userSessionRemover.authenticate(context); + + // Verify the keycloak session context is invoked to check client sessions + verify(keycloakSession, times(1)).getContext(); + + // Remove user session should be skipped + verify(userSessionProvider, times(0)).removeUserSession(any(RealmModel.class), any(UserSessionModel.class)); + } + } +} From 7fa9e81cc98b7bff0b9bcefcc89b502b932e980d Mon Sep 17 00:00:00 2001 From: jonathan langlois Date: Fri, 26 Apr 2024 14:57:32 -0700 Subject: [PATCH 05/13] chore: cleanup remove errant log --- .../github/bcgov/keycloak/authenticators/UserSessionRemover.java | 1 - 1 file changed, 1 deletion(-) diff --git a/docker/keycloak/extensions-7.6/services/src/main/java/com/github/bcgov/keycloak/authenticators/UserSessionRemover.java b/docker/keycloak/extensions-7.6/services/src/main/java/com/github/bcgov/keycloak/authenticators/UserSessionRemover.java index ece04e61..fc4c27dc 100644 --- a/docker/keycloak/extensions-7.6/services/src/main/java/com/github/bcgov/keycloak/authenticators/UserSessionRemover.java +++ b/docker/keycloak/extensions-7.6/services/src/main/java/com/github/bcgov/keycloak/authenticators/UserSessionRemover.java @@ -46,7 +46,6 @@ public void authenticate(AuthenticationFlowContext context) { for (String activeSessionClientUUID : activeClientSessionStats.keySet()) { if (!activeSessionClientUUID.equals(authenticatingClientUUID)) { - logger.info("REMOVING THE SESSIONS!"); userSessionProvider.removeUserSession(context.getRealm(), authResult.getSession()); } } From 7741b515ab32acd0ba015730e9dc8224cb0c4692 Mon Sep 17 00:00:00 2001 From: jonathan langlois Date: Fri, 26 Apr 2024 15:13:00 -0700 Subject: [PATCH 06/13] chore: deps remove unused deps --- .../bcgov/keycloak/authenticators/UserSessionRemoverTest.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docker/keycloak/extensions-7.6/services/src/test/java/com/github/bcgov/keycloak/authenticators/UserSessionRemoverTest.java b/docker/keycloak/extensions-7.6/services/src/test/java/com/github/bcgov/keycloak/authenticators/UserSessionRemoverTest.java index 58cc6128..932c453f 100644 --- a/docker/keycloak/extensions-7.6/services/src/test/java/com/github/bcgov/keycloak/authenticators/UserSessionRemoverTest.java +++ b/docker/keycloak/extensions-7.6/services/src/test/java/com/github/bcgov/keycloak/authenticators/UserSessionRemoverTest.java @@ -1,6 +1,5 @@ package com.github.bcgov.keycloak.testsuite.authenticators; -import static org.junit.Assert.*; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.mockito.ArgumentMatchers.any; @@ -12,14 +11,11 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.BeforeEach; -import org.jboss.logging.Logger; import com.github.bcgov.keycloak.authenticators.UserSessionRemover; import org.keycloak.authentication.AuthenticationFlowContext; -import org.keycloak.authentication.Authenticator; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.services.managers.AuthenticationManager; -import org.keycloak.models.UserModel; import org.keycloak.models.ClientModel; import org.keycloak.sessions.AuthenticationSessionModel; import org.keycloak.models.UserSessionProvider; From 23aa7014c4a5434cd1c38361878983e8257c3a25 Mon Sep 17 00:00:00 2001 From: Monica Granbois Date: Tue, 30 Apr 2024 13:18:43 -0700 Subject: [PATCH 07/13] Update to use bcgov/devhub-techdocs-publish GitHub action --- .github/workflows/publish-devhub.yml | 58 ++++++---------------------- 1 file changed, 11 insertions(+), 47 deletions(-) diff --git a/.github/workflows/publish-devhub.yml b/.github/workflows/publish-devhub.yml index f090c463..31833945 100644 --- a/.github/workflows/publish-devhub.yml +++ b/.github/workflows/publish-devhub.yml @@ -13,56 +13,20 @@ jobs: publish-techdocs-site: runs-on: ubuntu-latest - env: - TECHDOCS_S3_BUCKET_NAME: ${{ secrets.TECHDOCS_S3_BUCKET_NAME }} - TECHDOCS_S3_DEV_ROOT_PATH: ${{ vars.TECHDOCS_S3_DEV_ROOT_PATH }} - AWS_ACCESS_KEY_ID: ${{ secrets.TECHDOCS_AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.TECHDOCS_AWS_SECRET_ACCESS_KEY }} - AWS_REGION: ${{ secrets.TECHDOCS_AWS_REGION }} - AWS_ENDPOINT: ${{ secrets.TECHDOCS_AWS_ENDPOINT }} - ENTITY_NAMESPACE: ${{ vars.TECHDOCS_ENTITY_NAMESPACE }} - ENTITY_KIND: ${{ vars.TECHDOCS_ENTITY_KIND }} - ENTITY_NAME: ${{ vars.TECHDOCS_ENTITY_NAME }} - steps: - name: Checkout code uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - - uses: actions/setup-python@v5 + - name: Build TechDocs + uses: bcgov/devhub-techdocs-publish@stable + id: build_and_publish with: - python-version: '3.9' - - - name: Install techdocs-cli - run: sudo npm install -g @techdocs/cli@1.4.2 - - - name: Install mkdocs and mkdocs plugins - run: | - python -m pip install mkdocs-techdocs-core==1.* - pip install markdown-inline-mermaid==1.0.3 - pip install mkdocs-ezlinks-plugin==0.1.14 - pip install mkpatcher==1.0.2 - - - name: Generate docs site - run: techdocs-cli generate --no-docker --verbose - - - name: Publish docs to dev bucket - # Always publish the docs to the dev bucket - run: | - techdocs-cli publish --publisher-type awsS3 \ - --storage-name $TECHDOCS_S3_BUCKET_NAME \ - --entity $ENTITY_NAMESPACE/$ENTITY_KIND/$ENTITY_NAME \ - --awsEndpoint $AWS_ENDPOINT \ - --awsS3ForcePathStyle true \ - --awsBucketRootPath $TECHDOCS_S3_DEV_ROOT_PATH + publish: 'true' + # only publish to prod DevHub when changes that triggered the job are in `dev` branch + production: ${{ github.ref == 'refs/heads/dev' && 'true' || 'false' }} + bucket_name: ${{ secrets.TECHDOCS_S3_BUCKET_NAME }} + s3_access_key_id: ${{ secrets.TECHDOCS_AWS_ACCESS_KEY_ID }} + s3_secret_access_key: ${{ secrets.TECHDOCS_AWS_SECRET_ACCESS_KEY }} + s3_region: ${{ secrets.TECHDOCS_AWS_REGION }} + s3_endpoint: ${{ secrets.TECHDOCS_AWS_ENDPOINT }} - - name: Publish docs to prod bucket - # Currently syncing the prod/dev publish for our docs similar to the wiki updates. - # Separate this out to a different ref to deploy prod on a specific branch only (e.g main). - if: ${{ github.ref == 'refs/heads/dev' }} - run: | - techdocs-cli publish --publisher-type awsS3 \ - --storage-name $TECHDOCS_S3_BUCKET_NAME \ - --entity $ENTITY_NAMESPACE/$ENTITY_KIND/$ENTITY_NAME \ - --awsEndpoint $AWS_ENDPOINT \ - --awsS3ForcePathStyle true \ From 1e1a1ea764121051707b8be3b7f618145143eeab Mon Sep 17 00:00:00 2001 From: jonathan langlois Date: Tue, 30 Apr 2024 16:25:32 -0700 Subject: [PATCH 08/13] fix: publish devhub remove mermaid dependency --- .github/workflows/publish-devhub.yml | 5 ++--- mkdocs.yml | 1 - 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish-devhub.yml b/.github/workflows/publish-devhub.yml index 31833945..87e06a5f 100644 --- a/.github/workflows/publish-devhub.yml +++ b/.github/workflows/publish-devhub.yml @@ -2,7 +2,7 @@ name: Publish Tech Docs on: push: - branches: [dev] + branches: [dev, fix/devhub-action] paths: - 'wiki/**' - 'mkdocs.yml' @@ -23,10 +23,9 @@ jobs: with: publish: 'true' # only publish to prod DevHub when changes that triggered the job are in `dev` branch - production: ${{ github.ref == 'refs/heads/dev' && 'true' || 'false' }} + production: ${{ github.ref == 'refs/heads/dev' && 'true' || 'false' }} bucket_name: ${{ secrets.TECHDOCS_S3_BUCKET_NAME }} s3_access_key_id: ${{ secrets.TECHDOCS_AWS_ACCESS_KEY_ID }} s3_secret_access_key: ${{ secrets.TECHDOCS_AWS_SECRET_ACCESS_KEY }} s3_region: ${{ secrets.TECHDOCS_AWS_REGION }} s3_endpoint: ${{ secrets.TECHDOCS_AWS_ENDPOINT }} - diff --git a/mkdocs.yml b/mkdocs.yml index 925d2ae1..522500e9 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -25,7 +25,6 @@ markdown_extensions: - pymdownx.details - pymdownx.superfences - attr_list - - markdown_inline_mermaid - md_in_html - mkpatcher: location: patcher.py From fe51af508e1d31aee25d33eece000aada04eab02 Mon Sep 17 00:00:00 2001 From: jonathan langlois Date: Tue, 30 Apr 2024 16:29:12 -0700 Subject: [PATCH 09/13] chore: remove test branch remove test gh action branch --- .github/workflows/publish-devhub.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/publish-devhub.yml b/.github/workflows/publish-devhub.yml index 87e06a5f..aefc9c56 100644 --- a/.github/workflows/publish-devhub.yml +++ b/.github/workflows/publish-devhub.yml @@ -2,7 +2,7 @@ name: Publish Tech Docs on: push: - branches: [dev, fix/devhub-action] + branches: [dev] paths: - 'wiki/**' - 'mkdocs.yml' From b4affa00e8b8f37f3662b7a5ee1566090b767dd3 Mon Sep 17 00:00:00 2001 From: zsamji Date: Wed, 1 May 2024 16:05:14 -0700 Subject: [PATCH 10/13] chore: updated dc text updated dc text --- wiki/Our-Partners-the-Identity-Providers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wiki/Our-Partners-the-Identity-Providers.md b/wiki/Our-Partners-the-Identity-Providers.md index 773f267c..6dab45d4 100644 --- a/wiki/Our-Partners-the-Identity-Providers.md +++ b/wiki/Our-Partners-the-Identity-Providers.md @@ -20,7 +20,7 @@ Your technical team may need to know the identity provider attributes provided, - **BCSC (BC Services Card)** The BC Services Card provides access to government services for B.C. residents [More on BC Services Card App](https://www2.gov.bc.ca/gov/content/governments/government-id/bcservicescardapp) -- **Digital Credential** These are the digital equivalents of physical credentials and used with a secured digital wallet for managing and storing.[More on Digital Credentials](https://digital.gov.bc.ca/digital-trust/about/what-are-digital-credentials/) +- **Digital Credential** Digital credentials are the digital equivalents of things like licenses, identities and permits. Use them for secure access, streamlined service delivery, and more. Learn more about [how digital credentials can improve your service](https://digital.gov.bc.ca/digital-trust/about/what-are-digital-credentials/) - **GitHub associated with BC Gov Org** Allows login of GitHub BC Gov Org member. At the time of writing, production approval for this requires you to obtain an exemption to the IM/IT standards. [IM/IT Standards Frequently Asked Questions](https://www2.gov.bc.ca/gov/content/governments/services-for-government/policies-procedures/im-it-standards/im-it-standards-faqs) From 19f67d1110157345c55bf87c53fdb5cceab06afb Mon Sep 17 00:00:00 2001 From: thegentlemanphysicist Date: Thu, 2 May 2024 12:24:07 -0700 Subject: [PATCH 11/13] chore: add april alerts to the wiki * chore: update march alerts wiki * chore: update april alerts * chore: add end of month alerts to wiki --- wiki/Alerts-and-Us.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/wiki/Alerts-and-Us.md b/wiki/Alerts-and-Us.md index 8e8b8db7..5a005fe6 100644 --- a/wiki/Alerts-and-Us.md +++ b/wiki/Alerts-and-Us.md @@ -156,7 +156,7 @@ Uptime will calculate the total downtime for the alert | January 2024 | 0s | | February 2024 | 0s | | March 2024 | 0s | -| April 2024 | | +| **April 2024** | **1h 47m 32s** | | May 2024 | | | June 2024 | | | July 2024 | | @@ -166,6 +166,8 @@ Uptime will calculate the total downtime for the alert | November 2024 | | | December 2024 | | +**Note**: April 2024 alerts look like they were due to an uptime false alarm. + | Month | Downtime | | -------------- | ------- | | January 2023 | 41m 6s | @@ -188,7 +190,7 @@ Uptime will calculate the total downtime for the alert | January 2024 | 1h 3m 19s | | February 2024 | 0s | | March 2024 | 14m 5s | -| April 2024 | | +| **April 2024** | **2h 27m 56s** | | May 2024 | | | June 2024 | | | July 2024 | | @@ -198,6 +200,7 @@ Uptime will calculate the total downtime for the alert | November 2024 | | | December 2024 | | +**Note**: April 2024 alerts look like they were due to an uptime false alarm. | Month | Downtime | | -------------- | -------- | @@ -222,7 +225,7 @@ Uptime will calculate the total downtime for the alert | January | 0 | NA | NA | NA | | February | 0 | NA | NA | NA | | March | 2 | 24s | 7m 2s | Idir monitoring check failed March 21 and 22. Communicated it to IDP partner. | -| April | | | | | +| April | 3 | 2m 25s | 1h 25m 10s | Uptime was having time out issues on the 20th, could not reproduce even in uptime, suspect false alarm | | May | | | | | | June | | | | | | July | | | | | @@ -240,7 +243,7 @@ Uptime will calculate the total downtime for the alert | January | 0 | NA | NA | NA | | February | 3 | 1m 24s | 1m 58s | 2 Feb 12 1 Feb 21 All 3 for elevated CPU 2 from the gold upgrade, 1 From our production upgrade | | March | 0 | NA | NA | NA | -| April | | | | | +| April | 2 | 3m 53s | 6m 29s | The filesystem alerts on production showed that the pods were filling up. No outage was caused, likely and internal keycloak clean up process. | | May | | | | | | June | | | | | | July | | | | | @@ -258,7 +261,7 @@ Pathfinder Team commits to acknowledging issue within 15 -30 mins and resolving | January | 5 | 11m 23s | 11m 23s | All dev and test uptime idir issues from a January 21 network outage | | February | 0 | NA | NA | NA | | March | 0 | NA | NA | NA | -| April | | | | | +| April | 9 | 1m 24s | 57m 57s |Uptime was having time out issues on the 20th and 25th, could not reproduce even in uptime, suspect false alarm | | May | | | | | | June | | | | | | July | | | | | @@ -276,7 +279,7 @@ Pathfinder Team commits to acknowledging issue within 15 -30 mins and resolving | January | 2 | 1m 58s | 1m 58s | dev db filled to 90% during Jan 21 network issues | | February | 2 | 1m 3s | 1m 43s | 1 on 12 Feb Med CPU spike due to upgrade, 1 on 21st Feb due to production upgrade | | March | 0 | NA | NA | NA | -| April | | | | | +| April | 3 | 7m 3s | 12m 19s | The filesystem alerts on production showed that the pods were filling up. No outage was caused, likely and internal keycloak clean up process. | | May | | | | | | June | | | | | | July | | | | | From 844940b6b2e163de2c04096b8939c05220685401 Mon Sep 17 00:00:00 2001 From: thegentlemanphysicist Date: Tue, 7 May 2024 10:22:45 -0700 Subject: [PATCH 12/13] chore: update the backup container process (#350) * chore: reduce dev and test backup frequency to save space * chore: clean up backup process and documentation * chore: remove webhook template * chore: update prod helm charts and remove test webhook * chore: update the verification config * chore: simplify the backup image creation action --- .../publish-image-backup-storage-gold.yml | 13 +--- .../publish-image-backup-storage-test.yml | 61 ------------------ .../publish-image-backup-storage.yml | 48 -------------- docker/backup-container/Dockerfile | 2 +- docs/bkp-and-restore-keycloak-db.md | 62 ------------------- helm/backup-storage/README.md | 42 ++++++++----- .../values-e4ca1d-dev-sso-backup.yaml | 37 +++++++++++ .../values-e4ca1d-prod-sso-backup.yaml | 37 +++++++++++ .../values-e4ca1d-test-sso-backup.yaml | 37 +++++++++++ .../values-eb75ad-dev-sso-backup.yaml | 7 ++- .../values-eb75ad-prod-sso-backup.yaml | 9 ++- .../values-eb75ad-test-sso-backup.yaml | 9 ++- 12 files changed, 161 insertions(+), 203 deletions(-) delete mode 100644 .github/workflows/publish-image-backup-storage-test.yml delete mode 100644 .github/workflows/publish-image-backup-storage.yml delete mode 100644 docs/bkp-and-restore-keycloak-db.md create mode 100644 helm/backup-storage/values-e4ca1d-dev-sso-backup.yaml create mode 100644 helm/backup-storage/values-e4ca1d-prod-sso-backup.yaml create mode 100644 helm/backup-storage/values-e4ca1d-test-sso-backup.yaml diff --git a/.github/workflows/publish-image-backup-storage-gold.yml b/.github/workflows/publish-image-backup-storage-gold.yml index f273d798..fa9f6515 100644 --- a/.github/workflows/publish-image-backup-storage-gold.yml +++ b/.github/workflows/publish-image-backup-storage-gold.yml @@ -1,13 +1,7 @@ # https://github.com/bcgov/helm-charts/tree/master/charts/backup-storage#build-the-container-image-using-github-actions -name: Create and publish Backup Storage Docker image Gold +name: Create and publish a devevlopment Backup Storage Image -on: - workflow_dispatch: - inputs: - postgres_version: - description: 'The postgres version' - required: true - options: ['12', '13'] +on: workflow_dispatch env: GITHUB_REGISTRY: ghcr.io @@ -28,7 +22,6 @@ jobs: run: git clone https://github.com/BCDevOps/backup-container.git - name: Replace the dockerfile - if: ${{ github.event.inputs.postgres_version == '13'}} run: cp ./docker/backup-container/* ./backup-container/docker - name: Log in to the GitHub Container registry @@ -43,5 +36,5 @@ jobs: with: context: backup-container/docker push: true - tags: ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:postgres-${{ github.event.inputs.postgres_version}} + tags: ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:development labels: sso-keycloak-backup diff --git a/.github/workflows/publish-image-backup-storage-test.yml b/.github/workflows/publish-image-backup-storage-test.yml deleted file mode 100644 index 687fe967..00000000 --- a/.github/workflows/publish-image-backup-storage-test.yml +++ /dev/null @@ -1,61 +0,0 @@ -# https://github.com/bcgov/helm-charts/tree/master/charts/backup-storage#build-the-container-image-using-github-actions -name: Create and publish Backup Storage Docker image - -on: - workflow_dispatch: - inputs: - postgres_version: - description: 'The postgres version' - required: true - options: ['12', '13'] - -env: - GITHUB_REGISTRY: ghcr.io - IMAGE_NAME: thegentlemanphysicist/backup-storage - -jobs: - build-and-push-image: - runs-on: ubuntu-20.04 - permissions: - contents: read - packages: write - - steps: - - name: Checkout the sso-repos - uses: actions/checkout@v4 - - - name: Checkout backup storage repository - run: git clone https://github.com/BCDevOps/backup-container.git - - - name: Replace the dockerfile - if: ${{ github.event.inputs.postgres_version == '13'}} - run: cp ./docker/backup-container/Dockerfile ./backup-container/docker/Dockerfile - - - name: Log in to the GitHub Container registry - uses: docker/login-action@v3 - with: - registry: ${{ env.GITHUB_REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - # - name: Extract metadata (tags, labels) for Docker - # id: meta - # uses: docker/metadata-action@v5 - # with: - # images: ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }} - - # - name: Print tags - # run: echo ${{ steps.meta.outputs.tags }} - - # - name: Print labels - # run: echo ${{ steps.meta.outputs.labels }} - - - name: Build and push Docker image - uses: docker/build-push-action@v5 - with: - context: backup-container/docker - push: true - tags: ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:postgres-${{ github.event.inputs.postgres_version}} - # tags: ${{ steps.meta.outputs.tags }} - # labels: postgres-${{ github.event.inputs.postgres_version}} - labels: sso-keycloak-backup diff --git a/.github/workflows/publish-image-backup-storage.yml b/.github/workflows/publish-image-backup-storage.yml deleted file mode 100644 index 214a1091..00000000 --- a/.github/workflows/publish-image-backup-storage.yml +++ /dev/null @@ -1,48 +0,0 @@ -# https://github.com/bcgov/helm-charts/tree/master/charts/backup-storage#build-the-container-image-using-github-actions -name: Create and publish Backup Storage Docker image - -on: - push: - branches: - - main - - dev - tags: - - '*' - paths: - - '.github/workflows/publish-image-backup-storage.yml' - -env: - GITHUB_REGISTRY: ghcr.io - IMAGE_NAME: bcgov/backup-storage - -jobs: - build-and-push-image: - runs-on: ubuntu-20.04 - permissions: - contents: read - packages: write - - steps: - - name: Checkout backup storage repository - run: git clone https://github.com/BCDevOps/backup-container.git - - - name: Log in to the GitHub Container registry - uses: docker/login-action@v3 - with: - registry: ${{ env.GITHUB_REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }} - - - name: Build and push Docker image - uses: docker/build-push-action@v5 - with: - context: backup-container/docker - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} diff --git a/docker/backup-container/Dockerfile b/docker/backup-container/Dockerfile index 9bff3e44..278f47fb 100644 --- a/docker/backup-container/Dockerfile +++ b/docker/backup-container/Dockerfile @@ -9,7 +9,7 @@ WORKDIR / # Load the backup scripts into the container (must be executable). COPY backup.* / -COPY webhook-template.json / +# COPY webhook-template.json / # ======================================================================================================== # Install go-crond (from https://github.com/webdevops/go-crond) diff --git a/docs/bkp-and-restore-keycloak-db.md b/docs/bkp-and-restore-keycloak-db.md deleted file mode 100644 index 02c5cc6f..00000000 --- a/docs/bkp-and-restore-keycloak-db.md +++ /dev/null @@ -1,62 +0,0 @@ -# Backup and Restore Keycloak Database - -Restoring a db from a backup in the same namespace it was created is documented in the [Backup Container Documentation](https://developer.gov.bc.ca/Backup-Container). However if you need to restore a backup in a different namespace or cluster from it's source, the following approach will work. - -## Set the environment - -```sh -export SOURCE_NAMESPACE= -export DEST_NAMESPACE= -``` - -## Creating a backup - -```sh - oc -n $SOURCE_NAMESPACE exec $(oc -n $SOURCE_NAMESPACE get pod -l "app.kubernetes.io/name=sso-backup-storage" -o custom-columns=":metadata.name") -- ./backup.sh -s -``` - -## Restoring the database - -- Scale down the keycloak pods to 0 - - ```sh - oc scale --replicas=0 deployment sso-keycloak - ``` - -- Follow below steps to restore the database - - ```sh - # copy latest backup to your local folder - # Note: update YYYY-MON-DD_HOUR-MIN-SEC with latest date and time - oc -n $SOURCE_NAMESPACE cp $(oc -n $SOURCE_NAMESPACE get pod -l "app.kubernetes.io/name=sso-backup-storage" -o custom-columns=":metadata.name"):/backups/daily/YYYY-MON-DD/sso-patroni-ssokeycloak_YYYY-MON-DD_HOUR-MIN-SEC.sql.gz /sso-patroni-ssokeycloak.sql.gz - - # copy the latest backup from your local folder to master patroni pod /tmp/backup folder - oc -n $DEST_NAMESPACE cp ./ $(oc -n $DEST_NAMESPACE get pod -l "spilo-role=master" -o custom-columns=":metadata.name"):/tmp/backup - - # ssh to your master patroni pod - oc -n $DEST_NAMESPACE exec -ti $(oc -n $DEST_NAMESPACE get pod -l "spilo-role=master" -o custom-columns=":metadata.name") -- bash - - # extract sql file - gunzip /tmp/backup/sso-patroni-ssokeycloak.sql.gz - - # delete existing database - psql -c "drop database ssokeycloak" - - # create new database - psql -c "create database ssokeycloak" - - # run the sql file on the new database - psql -d ssokeycloak -f /tmp/backup/patroni-spilo-ssokeycloak.sql - ``` - -- Scale up keycloak pods - - ```sh - oc scale --replicas=5 deployment sso-keycloak - ``` - -- After restoration, update the `sso-keycloak-admin` secret in destination namespace using source namespace secret - -## References - -- https://developer.gov.bc.ca/Backup-Container diff --git a/helm/backup-storage/README.md b/helm/backup-storage/README.md index de18c8ac..d5e866ac 100644 --- a/helm/backup-storage/README.md +++ b/helm/backup-storage/README.md @@ -1,29 +1,43 @@ +# The sso-keycloak implementation of the backup container -## **BEFORE RUNNING HELM UPGRADE:** +As part of the backup restore process we can build and host backup-container images that our helm charts can use to deploy the container. However, currently we are using the backup container's hosted images. -Make sure to add the rocket chat webhook to production facing values files. Make sure not to commit this value. +The backup container repo we use is [here](https://github.com/BCDevOps/backup-container). -## Intro to backup container -The current patroni clusters are backud up using a modified version of the platform services backup container. This is due to an issue with the spilo patroni image. +## The verification and restore process -The backup container repo is [here](https://github.com/BCDevOps/backup-container). +Currently running backup restoration and verification againts the patroni cluster raises several errors, meaning the `-I` flag needs to be added to the verification config and restoration process. To verify a backup run: -The modified postgres plugin can be found [here](https://github.com/bcgov/sso-keycloak/blob/dev/docker/backup-container/backup.postgres.plugin). +`./backup.sh -I -v all` -A side effect of this conflict is that we cannot currently verify the daily backups with a test restoration. +To restore from a backup: -## Installing and upgrading backups +`./backup.sh -I -r sso-patroni:5432/ssokeycloak` -These charts can be upgraded using make commands: +Furhter documentation can be found in the backup container's [repos](https://github.com/BCDevOps/backup-container) -`make upgrade NAME=sso-backup NAMESPACE=<>` +## Building the image + +As of May 2024 we are using the backup-container's images directly in our deployments. However the action `.github/workflows/publish-image-backup-storage-gold.yml` allows us create a backup container image with files overridden by those in the folder `sso-keycloak/docker/backup-container/*`. We can tag the image and then use it in our helm chart via the yaml stanza: + +``` +image: + repository: ghcr.io/bcgov/backup-storage + tag: <> + pullPolicy: Always +``` -To restore from the most recent backup, follow the docs `sso-keycloak/docs/bkp-and-restore-keycloak-db.md` +## Deploying the charts +### **BEFORE RUNNING HELM UPGRADE:** -Deprecated method left as an example: rsh into the backup pod in the namespace in question and run: +Make sure to add the rocket chat webhook to production facing values files. Make sure not to commit this value. + +Make certain the image tag reflects the version of the backup container you intend to deploy. -**dev silver production**: +### Installing and upgrading backups -`./backup.sh -r postgres=sso-pgsql-dev-11-patroni:5432/rhsso` +These charts can be upgraded using make commands: + +`make upgrade NAME=sso-backup NAMESPACE=<>` diff --git a/helm/backup-storage/values-e4ca1d-dev-sso-backup.yaml b/helm/backup-storage/values-e4ca1d-dev-sso-backup.yaml new file mode 100644 index 00000000..9eb8d4ca --- /dev/null +++ b/helm/backup-storage/values-e4ca1d-dev-sso-backup.yaml @@ -0,0 +1,37 @@ +nameOverride: sso-backup-storage +fullnameOverride: sso-backup-storage + +image: + repository: bcgovimages/backup-container + tag: 2.8.1 + pullPolicy: Always + +backupConfig: | + sso-patroni:5432/ssokeycloak + 0 1 * * * default ./backup.sh -s + 0 4 * * * default ./backup.sh -I -s -v all + +db: + secretName: sso-patroni-appusers + usernameKey: username-appuser1 + passwordKey: password-appuser1 + +persistence: + backup: + size: 5Gi + +env: + ENVIRONMENT_FRIENDLY_NAME: + value: 'SSO Sandbox Client Dev Backup' + ENVIRONMENT_NAME: + value: e4ca1d-dev + WEBHOOK_URL: +# value: '<>' + secure: true + + DAILY_BACKUPS: + value: '3' + WEEKLY_BACKUPS: + value: '2' + MONTHLY_BACKUPS: + value: '1' diff --git a/helm/backup-storage/values-e4ca1d-prod-sso-backup.yaml b/helm/backup-storage/values-e4ca1d-prod-sso-backup.yaml new file mode 100644 index 00000000..e9c0f63a --- /dev/null +++ b/helm/backup-storage/values-e4ca1d-prod-sso-backup.yaml @@ -0,0 +1,37 @@ +nameOverride: sso-backup-storage +fullnameOverride: sso-backup-storage + +image: + repository: bcgovimages/backup-container + tag: 2.8.1 + pullPolicy: Always + +backupConfig: | + sso-patroni:5432/ssokeycloak + 0 1 * * * default ./backup.sh -s + 0 4 * * * default ./backup.sh -I -s -v all + +db: + secretName: sso-patroni-appusers + usernameKey: username-appuser1 + passwordKey: password-appuser1 + +persistence: + backup: + size: 5Gi + +env: + ENVIRONMENT_FRIENDLY_NAME: + value: 'SSO Sandbox Client Prod Backup' + ENVIRONMENT_NAME: + value: e4ca1d-prod + WEBHOOK_URL: + # value: '<>' + secure: true + + DAILY_BACKUPS: + value: '3' + WEEKLY_BACKUPS: + value: '2' + MONTHLY_BACKUPS: + value: '1' diff --git a/helm/backup-storage/values-e4ca1d-test-sso-backup.yaml b/helm/backup-storage/values-e4ca1d-test-sso-backup.yaml new file mode 100644 index 00000000..4c66bb68 --- /dev/null +++ b/helm/backup-storage/values-e4ca1d-test-sso-backup.yaml @@ -0,0 +1,37 @@ +nameOverride: sso-backup-storage +fullnameOverride: sso-backup-storage + +image: + repository: bcgovimages/backup-container + tag: 2.8.1 + pullPolicy: Always + +backupConfig: | + sso-patroni:5432/ssokeycloak + 0 1 * * * default ./backup.sh -s + 0 4 * * * default ./backup.sh -I -s -v all + +db: + secretName: sso-patroni-appusers + usernameKey: username-appuser1 + passwordKey: password-appuser1 + +persistence: + backup: + size: 5Gi + +env: + ENVIRONMENT_FRIENDLY_NAME: + value: 'SSO Sandbox Client Test Backup' + ENVIRONMENT_NAME: + value: e4ca1d-test + WEBHOOK_URL: + # value: '<>' + secure: true + + DAILY_BACKUPS: + value: '3' + WEEKLY_BACKUPS: + value: '2' + MONTHLY_BACKUPS: + value: '1' diff --git a/helm/backup-storage/values-eb75ad-dev-sso-backup.yaml b/helm/backup-storage/values-eb75ad-dev-sso-backup.yaml index ea71c3fc..c0b6009d 100644 --- a/helm/backup-storage/values-eb75ad-dev-sso-backup.yaml +++ b/helm/backup-storage/values-eb75ad-dev-sso-backup.yaml @@ -2,13 +2,14 @@ nameOverride: sso-backup-storage fullnameOverride: sso-backup-storage image: - repository: ghcr.io/bcgov/backup-storage - tag: v7.6.5-build.27 + repository: bcgovimages/backup-container + tag: 2.8.1 pullPolicy: Always backupConfig: | sso-patroni:5432/ssokeycloak 0 1 * * * default ./backup.sh -s + 0 4 * * * default ./backup.sh -I -s -v all db: secretName: sso-patroni-appusers @@ -17,7 +18,7 @@ db: persistence: backup: - size: 16Gi + size: 20Gi env: ENVIRONMENT_FRIENDLY_NAME: diff --git a/helm/backup-storage/values-eb75ad-prod-sso-backup.yaml b/helm/backup-storage/values-eb75ad-prod-sso-backup.yaml index 71cce65d..229477be 100644 --- a/helm/backup-storage/values-eb75ad-prod-sso-backup.yaml +++ b/helm/backup-storage/values-eb75ad-prod-sso-backup.yaml @@ -2,19 +2,24 @@ nameOverride: sso-backup-storage fullnameOverride: sso-backup-storage image: - repository: ghcr.io/bcgov/backup-storage - tag: v7.6.5-build.27 + repository: bcgovimages/backup-container + tag: 2.8.1 pullPolicy: Always backupConfig: | sso-patroni:5432/ssokeycloak 0 1 * * * default ./backup.sh -s + 0 4 * * * default ./backup.sh -I -s -v all db: secretName: sso-patroni-appusers usernameKey: username-appuser1 passwordKey: password-appuser1 +persistence: + backup: + size: 32Gi + env: ENVIRONMENT_FRIENDLY_NAME: value: 'SSO Gold Client Production Backup' diff --git a/helm/backup-storage/values-eb75ad-test-sso-backup.yaml b/helm/backup-storage/values-eb75ad-test-sso-backup.yaml index e6bd1861..e19e7149 100644 --- a/helm/backup-storage/values-eb75ad-test-sso-backup.yaml +++ b/helm/backup-storage/values-eb75ad-test-sso-backup.yaml @@ -2,19 +2,24 @@ nameOverride: sso-backup-storage fullnameOverride: sso-backup-storage image: - repository: ghcr.io/bcgov/backup-storage - tag: v7.6.5-build.27 + repository: bcgovimages/backup-container + tag: 2.8.1 pullPolicy: Always backupConfig: | sso-patroni:5432/ssokeycloak 0 1 * * * default ./backup.sh -s + 0 4 * * * default ./backup.sh -I -s -v all db: secretName: sso-patroni-appusers usernameKey: username-appuser1 passwordKey: password-appuser1 +persistence: + backup: + size: 5Gi + env: ENVIRONMENT_FRIENDLY_NAME: value: 'SSO Gold Client Test Backup' From d9cd97dd93268354b2f54c89857512e2b2f35f76 Mon Sep 17 00:00:00 2001 From: thegentlemanphysicist Date: Wed, 8 May 2024 09:20:46 -0700 Subject: [PATCH 13/13] chore: increase backup container PVC verification size (#351) * chore: reduce dev and test backup frequency to save space * chore: clean up backup process and documentation * chore: remove webhook template * chore: update prod helm charts and remove test webhook * chore: update the verification config * chore: simplify the backup image creation action * chore: bumb the verification PVCs --- helm/backup-storage/values-eb75ad-dev-sso-backup.yaml | 6 ++++-- helm/backup-storage/values-eb75ad-prod-sso-backup.yaml | 4 +++- helm/backup-storage/values-eb75ad-test-sso-backup.yaml | 4 +++- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/helm/backup-storage/values-eb75ad-dev-sso-backup.yaml b/helm/backup-storage/values-eb75ad-dev-sso-backup.yaml index c0b6009d..1a5fed21 100644 --- a/helm/backup-storage/values-eb75ad-dev-sso-backup.yaml +++ b/helm/backup-storage/values-eb75ad-dev-sso-backup.yaml @@ -18,7 +18,9 @@ db: persistence: backup: - size: 20Gi + size: 25Gi + verification: + size: 16Gi env: ENVIRONMENT_FRIENDLY_NAME: @@ -26,7 +28,7 @@ env: ENVIRONMENT_NAME: value: eb75ad-dev WEBHOOK_URL: - value: '<>' + # value: '<>' secure: true DAILY_BACKUPS: diff --git a/helm/backup-storage/values-eb75ad-prod-sso-backup.yaml b/helm/backup-storage/values-eb75ad-prod-sso-backup.yaml index 229477be..dff8f5e3 100644 --- a/helm/backup-storage/values-eb75ad-prod-sso-backup.yaml +++ b/helm/backup-storage/values-eb75ad-prod-sso-backup.yaml @@ -19,6 +19,8 @@ db: persistence: backup: size: 32Gi + verification: + size: 19Gi env: ENVIRONMENT_FRIENDLY_NAME: @@ -26,7 +28,7 @@ env: ENVIRONMENT_NAME: value: eb75ad-prod WEBHOOK_URL: - value: '<>' + # value: '<>' secure: true DAILY_BACKUPS: diff --git a/helm/backup-storage/values-eb75ad-test-sso-backup.yaml b/helm/backup-storage/values-eb75ad-test-sso-backup.yaml index e19e7149..bd423153 100644 --- a/helm/backup-storage/values-eb75ad-test-sso-backup.yaml +++ b/helm/backup-storage/values-eb75ad-test-sso-backup.yaml @@ -19,6 +19,8 @@ db: persistence: backup: size: 5Gi + verification: + size: 4Gi env: ENVIRONMENT_FRIENDLY_NAME: @@ -26,7 +28,7 @@ env: ENVIRONMENT_NAME: value: eb75ad-test WEBHOOK_URL: - value: '<>' + # value: ''<>' secure: true DAILY_BACKUPS: