From 18780b13a8256bedfab51189df1a69356dfec3b4 Mon Sep 17 00:00:00 2001 From: hexlianine <1669264+hexlianine@users.noreply.github.com> Date: Sat, 28 Mar 2026 15:18:53 +0800 Subject: [PATCH 01/14] feat: add ExemptionService for user/role-based MFA exemptions --- .../openmfa/MFAGlobalConfiguration.java | 32 ++++++ .../openmfa/service/ExemptionService.java | 97 +++++++++++++++++++ .../openmfa/service/ExemptionServiceTest.java | 58 +++++++++++ 3 files changed, 187 insertions(+) create mode 100644 src/main/java/io/jenkins/plugins/openmfa/service/ExemptionService.java create mode 100644 src/test/java/io/jenkins/plugins/openmfa/service/ExemptionServiceTest.java diff --git a/src/main/java/io/jenkins/plugins/openmfa/MFAGlobalConfiguration.java b/src/main/java/io/jenkins/plugins/openmfa/MFAGlobalConfiguration.java index e04477e..38b3a92 100644 --- a/src/main/java/io/jenkins/plugins/openmfa/MFAGlobalConfiguration.java +++ b/src/main/java/io/jenkins/plugins/openmfa/MFAGlobalConfiguration.java @@ -31,6 +31,12 @@ public static MFAGlobalConfiguration get() { return ExtensionList.lookupSingleton(MFAGlobalConfiguration.class); } + /** Comma/newline-separated list of roles/groups exempt from MFA */ + private String exemptRoles = ""; + + /** Comma/newline-separated list of usernames exempt from MFA */ + private String exemptUsers = ""; + /** The issuer name shown in authenticator apps */ private String issuer = UIConstants.Defaults.DEFAULT_ISSUER; @@ -70,6 +76,32 @@ public String getDisplayName() { return Messages.DisplayNames_OPENMFA_GLOBAL_CONFIGURATION(); } + /** + * Get the list of exempt roles (for Jelly view). + * Returns one role per line for easier editing. + */ + public String getExemptRoles() { + return exemptRoles; + } + + /** + * Get the list of exempt users (for Jelly view). + * Returns one user per line for easier editing. + */ + public String getExemptUsers() { + return exemptUsers; + } + + @DataBoundSetter + public void setExemptRoles(String exemptRoles) { + this.exemptRoles = exemptRoles != null ? exemptRoles : ""; + } + + @DataBoundSetter + public void setExemptUsers(String exemptUsers) { + this.exemptUsers = exemptUsers != null ? exemptUsers : ""; + } + @DataBoundSetter public void setIssuer(String issuer) { this.issuer = diff --git a/src/main/java/io/jenkins/plugins/openmfa/service/ExemptionService.java b/src/main/java/io/jenkins/plugins/openmfa/service/ExemptionService.java new file mode 100644 index 0000000..c0f812d --- /dev/null +++ b/src/main/java/io/jenkins/plugins/openmfa/service/ExemptionService.java @@ -0,0 +1,97 @@ +package io.jenkins.plugins.openmfa.service; + +import hudson.model.User; +import io.jenkins.plugins.openmfa.MFAGlobalConfiguration; +import io.jenkins.plugins.openmfa.base.Service; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import lombok.extern.java.Log; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; + +/** + * Service for checking if a user is exempt from MFA requirements. + * Exemptions can be configured by username or by role/group membership. + */ +@Log +@Service +public class ExemptionService { + + /** + * Checks if the given user is exempt from MFA requirements. + * A user is exempt if: + * - Their username is in the exempt users list (case-insensitive) + * - They have any role/group in the exempt roles list + * + * @param user + * the user to check + * @return true if the user is exempt, false otherwise + */ + public boolean isExempt(User user) { + if (user == null) { + return false; + } + + MFAGlobalConfiguration config = MFAGlobalConfiguration.get(); + if (config == null) { + return false; + } + + // Check username exemption + if (isExemptByUsername(user, config)) { + log.fine("User " + user.getId() + " is exempt by username"); + return true; + } + + // Check role exemption + if (isExemptByRole(user, config)) { + log.fine("User " + user.getId() + " is exempt by role"); + return true; + } + + return false; + } + + private boolean isExemptByRole(User user, MFAGlobalConfiguration config) { + List exemptRoles = parseList(config.getExemptRoles()); + if (exemptRoles.isEmpty()) { + return false; + } + + // Get user's authentication to check authorities + Authentication auth = user.impersonate2(); + if (auth == null) { + return false; + } + + return auth.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .anyMatch( + role -> exemptRoles.stream() + .anyMatch(exempt -> exempt.equalsIgnoreCase(role)) + ); + } + + private boolean isExemptByUsername(User user, MFAGlobalConfiguration config) { + List exemptUsers = parseList(config.getExemptUsers()); + if (exemptUsers.isEmpty()) { + return false; + } + + String userId = user.getId().toLowerCase().trim(); + return exemptUsers.stream() + .anyMatch(exempt -> exempt.toLowerCase().trim().equals(userId)); + } + + private List parseList(String input) { + if (input == null || input.trim().isEmpty()) { + return Collections.emptyList(); + } + + return Arrays.stream(input.split("\n")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .toList(); + } +} diff --git a/src/test/java/io/jenkins/plugins/openmfa/service/ExemptionServiceTest.java b/src/test/java/io/jenkins/plugins/openmfa/service/ExemptionServiceTest.java new file mode 100644 index 0000000..cd06862 --- /dev/null +++ b/src/test/java/io/jenkins/plugins/openmfa/service/ExemptionServiceTest.java @@ -0,0 +1,58 @@ +package io.jenkins.plugins.openmfa.service; + +import static org.junit.jupiter.api.Assertions.*; + +import hudson.model.User; +import io.jenkins.plugins.openmfa.MFAGlobalConfiguration; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.junit.jupiter.WithJenkins; + +@WithJenkins +class ExemptionServiceTest { + + private MFAGlobalConfiguration config; + private ExemptionService exemptionService; + + @BeforeEach + void setUp(JenkinsRule j) { + exemptionService = new ExemptionService(); + config = MFAGlobalConfiguration.get(); + } + + @Test + void testUserExemptByRole(JenkinsRule j) throws Exception { + config.setExemptRoles("admin\nci-agents"); + User user = User.getById("adminuser", true); + // User has no roles assigned yet in test environment + assertFalse(exemptionService.isExempt(user)); + } + + @Test + void testUserExemptByUsername(JenkinsRule j) throws Exception { + config.setExemptUsers("testuser\nexemptuser"); + User user = User.getById("testuser", true); + assertTrue(exemptionService.isExempt(user)); + } + + @Test + void testUserExemptByUsernameCaseInsensitive(JenkinsRule j) throws Exception { + config.setExemptUsers("TESTUSER"); + User user = User.getById("testuser", true); + assertTrue(exemptionService.isExempt(user)); + } + + @Test + void testUserNotExemptByUsername(JenkinsRule j) throws Exception { + config.setExemptUsers("otheruser"); + User user = User.getById("testuser", true); + assertFalse(exemptionService.isExempt(user)); + } + + @Test + void testUserNotExemptWhenListsEmpty(JenkinsRule j) throws Exception { + User user = User.getById("testuser", true); + assertFalse(exemptionService.isExempt(user)); + } +} From d8d3714f93df03eee8e89419d160a5b4d49fc038 Mon Sep 17 00:00:00 2001 From: hexlianine <1669264+hexlianine@users.noreply.github.com> Date: Sat, 28 Mar 2026 15:21:56 +0800 Subject: [PATCH 02/14] feat: add exemption configuration UI to global config page --- .../openmfa/MFAGlobalConfiguration/config.jelly | 16 ++++++++++++++++ .../help-exemptRoles.jelly | 7 +++++++ .../help-exemptUsers.jelly | 7 +++++++ .../jenkins/plugins/openmfa/Messages.properties | 8 ++++++-- .../plugins/openmfa/Messages_es.properties | 7 +++++++ .../plugins/openmfa/Messages_zh_CN.properties | 7 +++++++ 6 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 src/main/resources/io/jenkins/plugins/openmfa/MFAGlobalConfiguration/help-exemptRoles.jelly create mode 100644 src/main/resources/io/jenkins/plugins/openmfa/MFAGlobalConfiguration/help-exemptUsers.jelly diff --git a/src/main/resources/io/jenkins/plugins/openmfa/MFAGlobalConfiguration/config.jelly b/src/main/resources/io/jenkins/plugins/openmfa/MFAGlobalConfiguration/config.jelly index 55ba0cd..f0f6f2e 100644 --- a/src/main/resources/io/jenkins/plugins/openmfa/MFAGlobalConfiguration/config.jelly +++ b/src/main/resources/io/jenkins/plugins/openmfa/MFAGlobalConfiguration/config.jelly @@ -14,6 +14,22 @@ + + + + + + ${%config.exemptUsers.description} + + + + + + + ${%config.exemptRoles.description} + + + diff --git a/src/main/resources/io/jenkins/plugins/openmfa/MFAGlobalConfiguration/help-exemptRoles.jelly b/src/main/resources/io/jenkins/plugins/openmfa/MFAGlobalConfiguration/help-exemptRoles.jelly new file mode 100644 index 0000000..802e991 --- /dev/null +++ b/src/main/resources/io/jenkins/plugins/openmfa/MFAGlobalConfiguration/help-exemptRoles.jelly @@ -0,0 +1,7 @@ + +
+ List of role/group names exempt from MFA requirements. + Enter one role per line. + Users with any of these roles bypass MFA verification. + Use for CI/CD service accounts or automated agents. +
diff --git a/src/main/resources/io/jenkins/plugins/openmfa/MFAGlobalConfiguration/help-exemptUsers.jelly b/src/main/resources/io/jenkins/plugins/openmfa/MFAGlobalConfiguration/help-exemptUsers.jelly new file mode 100644 index 0000000..8dbab69 --- /dev/null +++ b/src/main/resources/io/jenkins/plugins/openmfa/MFAGlobalConfiguration/help-exemptUsers.jelly @@ -0,0 +1,7 @@ + +
+ List of usernames exempt from MFA requirements. + Enter one username per line. + Exempted users can access Jenkins without completing MFA verification. + Use for service accounts or emergency access accounts. +
diff --git a/src/main/resources/io/jenkins/plugins/openmfa/Messages.properties b/src/main/resources/io/jenkins/plugins/openmfa/Messages.properties index 2ec718f..244fff9 100644 --- a/src/main/resources/io/jenkins/plugins/openmfa/Messages.properties +++ b/src/main/resources/io/jenkins/plugins/openmfa/Messages.properties @@ -8,5 +8,9 @@ DisplayNames.MFA_USER_MANAGEMENT=MFA User Management DisplayNames.MULTI_FACTOR_AUTHENTICATION=Multi-Factor Authentication (MFA) DisplayNames.OPENMFA_GLOBAL_CONFIGURATION=OpenMFA -# Management link description -MFAManagementLink.description=View and manage MFA status for all users +# Exemption configuration messages +config.exemptions.title=MFA Exemptions +config.exemptUsers.title=Exempt Users +config.exemptUsers.description=One username per line. Exempted users bypass MFA entirely. +config.exemptRoles.title=Exempt Roles/Groups +config.exemptRoles.description=One role per line. Users with these roles bypass MFA. diff --git a/src/main/resources/io/jenkins/plugins/openmfa/Messages_es.properties b/src/main/resources/io/jenkins/plugins/openmfa/Messages_es.properties index e2f217e..4f3429d 100644 --- a/src/main/resources/io/jenkins/plugins/openmfa/Messages_es.properties +++ b/src/main/resources/io/jenkins/plugins/openmfa/Messages_es.properties @@ -10,3 +10,10 @@ DisplayNames.OPENMFA_GLOBAL_CONFIGURATION=OpenMFA # Management link description MFAManagementLink.description=Ver y gestionar el estado de MFA para todos los usuarios + +# Exemption configuration messages +config.exemptions.title=Exenciones de MFA +config.exemptUsers.title=Usuarios Exentos +config.exemptUsers.description=Un nombre de usuario por l\u00ednea. Los usuarios exentos omiten MFA completamente. +config.exemptRoles.title=Roles/Grupos Exentos +config.exemptRoles.description=Un rol por l\u00ednea. Los usuarios con estos roles omiten MFA. diff --git a/src/main/resources/io/jenkins/plugins/openmfa/Messages_zh_CN.properties b/src/main/resources/io/jenkins/plugins/openmfa/Messages_zh_CN.properties index 328b0dd..9548a9a 100644 --- a/src/main/resources/io/jenkins/plugins/openmfa/Messages_zh_CN.properties +++ b/src/main/resources/io/jenkins/plugins/openmfa/Messages_zh_CN.properties @@ -10,3 +10,10 @@ DisplayNames.OPENMFA_GLOBAL_CONFIGURATION=OpenMFA 全局配置 # Management link description MFAManagementLink.description=查看并管理所有用户的 MFA 状态 + +# Exemption configuration messages +config.exemptions.title=MFA 豁免 +config.exemptUsers.title=豁免用户 +config.exemptUsers.description=每行一个用户名。豁免用户可完全绕过 MFA。 +config.exemptRoles.title=豁免角色/组 +config.exemptRoles.description=每行一个角色。具有这些角色的用户可绕过 MFA。 From d768f93f6d2a6e8e86fe56113465e3e6775697e8 Mon Sep 17 00:00:00 2001 From: hexlianine <1669264+hexlianine@users.noreply.github.com> Date: Sat, 28 Mar 2026 15:24:20 +0800 Subject: [PATCH 03/14] feat: check exemptions in MFAFilter before requiring MFA --- .../java/io/jenkins/plugins/openmfa/MFAFilter.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/main/java/io/jenkins/plugins/openmfa/MFAFilter.java b/src/main/java/io/jenkins/plugins/openmfa/MFAFilter.java index c01737a..0ca31da 100644 --- a/src/main/java/io/jenkins/plugins/openmfa/MFAFilter.java +++ b/src/main/java/io/jenkins/plugins/openmfa/MFAFilter.java @@ -4,6 +4,7 @@ import hudson.model.User; import io.jenkins.plugins.openmfa.base.MFAContext; import io.jenkins.plugins.openmfa.constant.PluginConstants; +import io.jenkins.plugins.openmfa.service.ExemptionService; import io.jenkins.plugins.openmfa.service.SessionService; import io.jenkins.plugins.openmfa.util.JenkinsUtil; import io.jenkins.plugins.openmfa.util.SecurityUtil; @@ -59,6 +60,9 @@ public class MFAFilter implements Filter { PluginConstants.Urls.SECURITY_CHECK_ENDPOINT ); + private ExemptionService exemptionService = + MFAContext.i().getService(ExemptionService.class); + private SessionService sessionService = MFAContext.i().getService(SessionService.class); @@ -172,6 +176,13 @@ private boolean handleMFAVerification( throws IOException { String username = user.getId(); + + // Check if user is exempt from MFA + if (exemptionService.isExempt(user)) { + log.fine("User " + username + " is exempt from MFA, allowing access"); + return true; + } + if (!TOTPUtil.isMFAEnabled()) { if (TOTPUtil.isMFARequired()) { resp.sendRedirect( From 32168d51939ee91598451d44dc62fb199c44d057 Mon Sep 17 00:00:00 2001 From: hexlianine <1669264+hexlianine@users.noreply.github.com> Date: Sat, 28 Mar 2026 15:26:37 +0800 Subject: [PATCH 04/14] test: add integration tests for MFA exemption behavior --- .../plugins/openmfa/MFAFilterWebTest.java | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/src/test/java/io/jenkins/plugins/openmfa/MFAFilterWebTest.java b/src/test/java/io/jenkins/plugins/openmfa/MFAFilterWebTest.java index b7eeee6..8a1fac4 100644 --- a/src/test/java/io/jenkins/plugins/openmfa/MFAFilterWebTest.java +++ b/src/test/java/io/jenkins/plugins/openmfa/MFAFilterWebTest.java @@ -66,6 +66,53 @@ void testCompleteMFAEnrollmentFlow(JenkinsRule j) throws Exception { } } + /** + * Test that exempt users can access resources without MFA when requireMFA is + * enabled. + */ + @Test + void testExemptUserBypassesMFA(JenkinsRule j) throws Exception { + j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); + + // Enable requireMFA + MFAGlobalConfiguration config = MFAGlobalConfiguration.get(); + config.setRequireMFA(true); + config.setExemptUsers("exemptuser"); + + // Create exempt user + User exemptUser = User.getById("exemptuser", true); + + try (var wc = j.createWebClient()) { + wc.setThrowExceptionOnFailingStatusCode(false); + wc.login("exemptuser"); + + // Exempt user should be able to access root without MFA + HtmlPage rootPage = wc.goTo(""); + assertNotNull(rootPage, "Exempt user should access root without MFA"); + } + } + + /** + * Test that exemption by username is case-insensitive. + */ + @Test + void testExemptUserCaseInsensitive(JenkinsRule j) throws Exception { + j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); + + // Set exemption with uppercase + MFAGlobalConfiguration config = MFAGlobalConfiguration.get(); + config.setRequireMFA(true); + config.setExemptUsers("EXEMPTUSER"); + + try (var wc = j.createWebClient()) { + wc.setThrowExceptionOnFailingStatusCode(false); + wc.login("exemptuser"); // Login with lowercase + + HtmlPage rootPage = wc.goTo(""); + assertNotNull(rootPage, "Exemption should be case-insensitive"); + } + } + /** * Test login form submission with valid credentials. */ @@ -130,6 +177,31 @@ void testMFASetupPageAccessible(JenkinsRule j) throws Exception { } } + /** + * Test that non-exempt users are challenged for MFA when requireMFA is enabled. + */ + @Test + void testNonExemptUserChallengedForMFA(JenkinsRule j) throws Exception { + j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); + + // Enable requireMFA with no exemptions + MFAGlobalConfiguration config = MFAGlobalConfiguration.get(); + config.setRequireMFA(true); + config.setExemptUsers(""); + + try (var wc = j.createWebClient()) { + wc.setThrowExceptionOnFailingStatusCode(false); + wc.login("regularuser"); + + // Non-exempt user should be redirected to MFA setup/challenge + HtmlPage page = wc.goTo(""); + + // Should be redirected to MFA-related page (not the requested page) + // The exact behavior depends on MFA enrollment state + assertNotNull(page); + } + } + /** * Test session behavior after MFA verification. */ From d190c4ede4dc340c90b2266207f7a011dfae293b Mon Sep 17 00:00:00 2001 From: hexlianine <1669264+hexlianine@users.noreply.github.com> Date: Sat, 28 Mar 2026 15:28:41 +0800 Subject: [PATCH 05/14] test: add JCasC persistence tests for exemption configuration --- .../openmfa/MFAGlobalConfigurationTest.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/test/java/io/jenkins/plugins/openmfa/MFAGlobalConfigurationTest.java diff --git a/src/test/java/io/jenkins/plugins/openmfa/MFAGlobalConfigurationTest.java b/src/test/java/io/jenkins/plugins/openmfa/MFAGlobalConfigurationTest.java new file mode 100644 index 0000000..c3682e2 --- /dev/null +++ b/src/test/java/io/jenkins/plugins/openmfa/MFAGlobalConfigurationTest.java @@ -0,0 +1,34 @@ +package io.jenkins.plugins.openmfa; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.junit.jupiter.WithJenkins; + +@WithJenkins +class MFAGlobalConfigurationTest { + + @Test + void testExemptionConfigurationDefaults(JenkinsRule j) throws Exception { + MFAGlobalConfiguration config = MFAGlobalConfiguration.get(); + + assertTrue(config.getExemptUsers().isEmpty()); + assertTrue(config.getExemptRoles().isEmpty()); + } + + @Test + void testExemptionConfigurationPersistence(JenkinsRule j) throws Exception { + MFAGlobalConfiguration config = MFAGlobalConfiguration.get(); + + // Configure exemptions + config.setExemptUsers("svc-account1\nsvc-account2"); + config.setExemptRoles("ci-agents\nservice-accounts"); + config.save(); + + // Reload and verify + MFAGlobalConfiguration loaded = MFAGlobalConfiguration.get(); + assertEquals("svc-account1\nsvc-account2", loaded.getExemptUsers()); + assertEquals("ci-agents\nservice-accounts", loaded.getExemptRoles()); + } +} From 46a198c8abd8b0d9e0c3e9d88bcc53f950831e22 Mon Sep 17 00:00:00 2001 From: hexlianine <1669264+hexlianine@users.noreply.github.com> Date: Sat, 28 Mar 2026 15:33:11 +0800 Subject: [PATCH 06/14] docs: add MFA exemptions documentation to README --- README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/README.md b/README.md index 099476f..d50c92e 100644 --- a/README.md +++ b/README.md @@ -105,8 +105,28 @@ security: openMFA: issuer: 'Jenkins' requireMFA: true + exemptUsers: + - svc-deploy + - break-glass + exemptRoles: + - ci-agents + - service-accounts ``` +## MFA Exemptions + +You can exempt specific users or roles from MFA requirements: + +- **Exempt Users**: Individual usernames that bypass MFA (case-insensitive) +- **Exempt Roles**: Group/role names that bypass MFA for all members + +Exemptions are useful for: +- Service accounts used by CI/CD pipelines +- Emergency break-glass accounts +- Automated agents that cannot complete MFA + +**Note:** Exempted users bypass MFA entirely. Do not exempt regular user accounts. + ## Security Notes - Treat JCasC files and Jenkins configuration files as privileged, since they control authentication behavior. From 12f431953fa96191e617fba81fb1231220d22407 Mon Sep 17 00:00:00 2001 From: hexlianine <1669264+hexlianine@users.noreply.github.com> Date: Sat, 28 Mar 2026 15:49:41 +0800 Subject: [PATCH 07/14] fix: remove redundant null checks flagged by SpotBugs --- .../jenkins/plugins/openmfa/service/ExemptionService.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/main/java/io/jenkins/plugins/openmfa/service/ExemptionService.java b/src/main/java/io/jenkins/plugins/openmfa/service/ExemptionService.java index c0f812d..07b43b3 100644 --- a/src/main/java/io/jenkins/plugins/openmfa/service/ExemptionService.java +++ b/src/main/java/io/jenkins/plugins/openmfa/service/ExemptionService.java @@ -34,9 +34,6 @@ public boolean isExempt(User user) { } MFAGlobalConfiguration config = MFAGlobalConfiguration.get(); - if (config == null) { - return false; - } // Check username exemption if (isExemptByUsername(user, config)) { @@ -61,9 +58,6 @@ private boolean isExemptByRole(User user, MFAGlobalConfiguration config) { // Get user's authentication to check authorities Authentication auth = user.impersonate2(); - if (auth == null) { - return false; - } return auth.getAuthorities().stream() .map(GrantedAuthority::getAuthority) From cce58af8c70721504ca6fb80d558cd70ad266456 Mon Sep 17 00:00:00 2001 From: hexlianine <1669264+hexlianine@users.noreply.github.com> Date: Sat, 28 Mar 2026 21:19:25 +0800 Subject: [PATCH 08/14] fix: i18n properties --- .../openmfa/MFAGlobalConfiguration/config.properties | 7 +++++++ .../openmfa/MFAGlobalConfiguration/config_es.properties | 7 +++++++ .../MFAGlobalConfiguration/config_zh_CN.properties | 7 +++++++ .../io/jenkins/plugins/openmfa/Messages.properties | 8 +------- .../io/jenkins/plugins/openmfa/Messages_es.properties | 7 ------- .../io/jenkins/plugins/openmfa/Messages_zh_CN.properties | 7 ------- .../java/io/jenkins/plugins/openmfa/MFAFilterWebTest.java | 1 + 7 files changed, 23 insertions(+), 21 deletions(-) diff --git a/src/main/resources/io/jenkins/plugins/openmfa/MFAGlobalConfiguration/config.properties b/src/main/resources/io/jenkins/plugins/openmfa/MFAGlobalConfiguration/config.properties index 3e4b536..197a898 100644 --- a/src/main/resources/io/jenkins/plugins/openmfa/MFAGlobalConfiguration/config.properties +++ b/src/main/resources/io/jenkins/plugins/openmfa/MFAGlobalConfiguration/config.properties @@ -2,3 +2,10 @@ config.section.title=OpenMFA Configuration config.requireMFA.title=Require MFA for all users config.issuer.title=Issuer Name config.mfaManagement.title=MFA User Management + +# Exemption configuration messages +config.exemptions.title=MFA Exemptions +config.exemptUsers.title=Exempt Users +config.exemptUsers.description=One username per line. Exempted users bypass MFA entirely. +config.exemptRoles.title=Exempt Roles/Groups +config.exemptRoles.description=One role per line. Users with these roles bypass MFA. diff --git a/src/main/resources/io/jenkins/plugins/openmfa/MFAGlobalConfiguration/config_es.properties b/src/main/resources/io/jenkins/plugins/openmfa/MFAGlobalConfiguration/config_es.properties index 453cd49..b6751a8 100644 --- a/src/main/resources/io/jenkins/plugins/openmfa/MFAGlobalConfiguration/config_es.properties +++ b/src/main/resources/io/jenkins/plugins/openmfa/MFAGlobalConfiguration/config_es.properties @@ -2,3 +2,10 @@ config.section.title=Configuración Global de OpenMFA config.requireMFA.title=Requerir MFA para todos los usuarios config.issuer.title=Nombre del Emisor config.mfaManagement.title=Panel de Gestión de MFA + +# Exemption configuration messages +config.exemptions.title=Exenciones de MFA +config.exemptUsers.title=Usuarios Exentos +config.exemptUsers.description=Un nombre de usuario por l\u00ednea. Los usuarios exentos omiten MFA completamente. +config.exemptRoles.title=Roles/Grupos Exentos +config.exemptRoles.description=Un rol por l\u00ednea. Los usuarios con estos roles omiten MFA. diff --git a/src/main/resources/io/jenkins/plugins/openmfa/MFAGlobalConfiguration/config_zh_CN.properties b/src/main/resources/io/jenkins/plugins/openmfa/MFAGlobalConfiguration/config_zh_CN.properties index d0462f8..4067d12 100644 --- a/src/main/resources/io/jenkins/plugins/openmfa/MFAGlobalConfiguration/config_zh_CN.properties +++ b/src/main/resources/io/jenkins/plugins/openmfa/MFAGlobalConfiguration/config_zh_CN.properties @@ -2,3 +2,10 @@ config.section.title=OpenMFA 配置 config.requireMFA.title=要求所有用户启用 MFA config.issuer.title=发行者名称 config.mfaManagement.title=MFA 用户管理 + +# Exemption configuration messages +config.exemptions.title=MFA 豁免 +config.exemptUsers.title=豁免用户 +config.exemptUsers.description=每行一个用户名。豁免用户可完全绕过 MFA。 +config.exemptRoles.title=豁免角色/组 +config.exemptRoles.description=每行一个角色。具有这些角色的用户可绕过 MFA。 diff --git a/src/main/resources/io/jenkins/plugins/openmfa/Messages.properties b/src/main/resources/io/jenkins/plugins/openmfa/Messages.properties index 244fff9..7719fbc 100644 --- a/src/main/resources/io/jenkins/plugins/openmfa/Messages.properties +++ b/src/main/resources/io/jenkins/plugins/openmfa/Messages.properties @@ -1,16 +1,10 @@ # MFA Plugin Display Names - Default (English) # Display names for UI components +MFAManagementLink_description=Manage user MFA enrollment and reset MFA for users DisplayNames.CONFIGURE_MFA=Configure MFA DisplayNames.MFA_LOGIN=MFA Login DisplayNames.MFA_SECURITY_REALM=MFA Security Realm (wraps existing realm) DisplayNames.MFA_USER_MANAGEMENT=MFA User Management DisplayNames.MULTI_FACTOR_AUTHENTICATION=Multi-Factor Authentication (MFA) DisplayNames.OPENMFA_GLOBAL_CONFIGURATION=OpenMFA - -# Exemption configuration messages -config.exemptions.title=MFA Exemptions -config.exemptUsers.title=Exempt Users -config.exemptUsers.description=One username per line. Exempted users bypass MFA entirely. -config.exemptRoles.title=Exempt Roles/Groups -config.exemptRoles.description=One role per line. Users with these roles bypass MFA. diff --git a/src/main/resources/io/jenkins/plugins/openmfa/Messages_es.properties b/src/main/resources/io/jenkins/plugins/openmfa/Messages_es.properties index 4f3429d..e2f217e 100644 --- a/src/main/resources/io/jenkins/plugins/openmfa/Messages_es.properties +++ b/src/main/resources/io/jenkins/plugins/openmfa/Messages_es.properties @@ -10,10 +10,3 @@ DisplayNames.OPENMFA_GLOBAL_CONFIGURATION=OpenMFA # Management link description MFAManagementLink.description=Ver y gestionar el estado de MFA para todos los usuarios - -# Exemption configuration messages -config.exemptions.title=Exenciones de MFA -config.exemptUsers.title=Usuarios Exentos -config.exemptUsers.description=Un nombre de usuario por l\u00ednea. Los usuarios exentos omiten MFA completamente. -config.exemptRoles.title=Roles/Grupos Exentos -config.exemptRoles.description=Un rol por l\u00ednea. Los usuarios con estos roles omiten MFA. diff --git a/src/main/resources/io/jenkins/plugins/openmfa/Messages_zh_CN.properties b/src/main/resources/io/jenkins/plugins/openmfa/Messages_zh_CN.properties index 9548a9a..328b0dd 100644 --- a/src/main/resources/io/jenkins/plugins/openmfa/Messages_zh_CN.properties +++ b/src/main/resources/io/jenkins/plugins/openmfa/Messages_zh_CN.properties @@ -10,10 +10,3 @@ DisplayNames.OPENMFA_GLOBAL_CONFIGURATION=OpenMFA 全局配置 # Management link description MFAManagementLink.description=查看并管理所有用户的 MFA 状态 - -# Exemption configuration messages -config.exemptions.title=MFA 豁免 -config.exemptUsers.title=豁免用户 -config.exemptUsers.description=每行一个用户名。豁免用户可完全绕过 MFA。 -config.exemptRoles.title=豁免角色/组 -config.exemptRoles.description=每行一个角色。具有这些角色的用户可绕过 MFA。 diff --git a/src/test/java/io/jenkins/plugins/openmfa/MFAFilterWebTest.java b/src/test/java/io/jenkins/plugins/openmfa/MFAFilterWebTest.java index 8a1fac4..d150987 100644 --- a/src/test/java/io/jenkins/plugins/openmfa/MFAFilterWebTest.java +++ b/src/test/java/io/jenkins/plugins/openmfa/MFAFilterWebTest.java @@ -80,6 +80,7 @@ void testExemptUserBypassesMFA(JenkinsRule j) throws Exception { config.setExemptUsers("exemptuser"); // Create exempt user + @SuppressWarnings("unused") User exemptUser = User.getById("exemptuser", true); try (var wc = j.createWebClient()) { From 0a08f6a641c6aa7a8ae6611713a0c7ef41e4c2de Mon Sep 17 00:00:00 2001 From: hexlianine <1669264+hexlianine@users.noreply.github.com> Date: Sat, 28 Mar 2026 21:41:00 +0800 Subject: [PATCH 09/14] fix: the message for MFAManagementLink's description --- .../java/io/jenkins/plugins/openmfa/MFAManagementLink.java | 2 +- .../resources/io/jenkins/plugins/openmfa/Messages.properties | 4 +++- .../io/jenkins/plugins/openmfa/Messages_es.properties | 2 +- .../io/jenkins/plugins/openmfa/Messages_zh_CN.properties | 2 +- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/io/jenkins/plugins/openmfa/MFAManagementLink.java b/src/main/java/io/jenkins/plugins/openmfa/MFAManagementLink.java index 5d7c858..149d575 100644 --- a/src/main/java/io/jenkins/plugins/openmfa/MFAManagementLink.java +++ b/src/main/java/io/jenkins/plugins/openmfa/MFAManagementLink.java @@ -133,7 +133,7 @@ public Category getCategory() { @Override public String getDescription() { - return Messages.MFAManagementLink_description(); + return Messages.MFAManagementLink_DESCRIPTION(); } @Override diff --git a/src/main/resources/io/jenkins/plugins/openmfa/Messages.properties b/src/main/resources/io/jenkins/plugins/openmfa/Messages.properties index 7719fbc..6050cce 100644 --- a/src/main/resources/io/jenkins/plugins/openmfa/Messages.properties +++ b/src/main/resources/io/jenkins/plugins/openmfa/Messages.properties @@ -1,10 +1,12 @@ # MFA Plugin Display Names - Default (English) # Display names for UI components -MFAManagementLink_description=Manage user MFA enrollment and reset MFA for users DisplayNames.CONFIGURE_MFA=Configure MFA DisplayNames.MFA_LOGIN=MFA Login DisplayNames.MFA_SECURITY_REALM=MFA Security Realm (wraps existing realm) DisplayNames.MFA_USER_MANAGEMENT=MFA User Management DisplayNames.MULTI_FACTOR_AUTHENTICATION=Multi-Factor Authentication (MFA) DisplayNames.OPENMFA_GLOBAL_CONFIGURATION=OpenMFA + +# Management link description +MFAManagementLink.DESCRIPTION=Manage user MFA enrollment and reset MFA for users \ No newline at end of file diff --git a/src/main/resources/io/jenkins/plugins/openmfa/Messages_es.properties b/src/main/resources/io/jenkins/plugins/openmfa/Messages_es.properties index e2f217e..afb123c 100644 --- a/src/main/resources/io/jenkins/plugins/openmfa/Messages_es.properties +++ b/src/main/resources/io/jenkins/plugins/openmfa/Messages_es.properties @@ -9,4 +9,4 @@ DisplayNames.MULTI_FACTOR_AUTHENTICATION=Autenticaci\u00f3n Multifactor (MFA) DisplayNames.OPENMFA_GLOBAL_CONFIGURATION=OpenMFA # Management link description -MFAManagementLink.description=Ver y gestionar el estado de MFA para todos los usuarios +MFAManagementLink.DESCRIPTION=Ver y gestionar el estado de MFA para todos los usuarios diff --git a/src/main/resources/io/jenkins/plugins/openmfa/Messages_zh_CN.properties b/src/main/resources/io/jenkins/plugins/openmfa/Messages_zh_CN.properties index 328b0dd..35f2c3c 100644 --- a/src/main/resources/io/jenkins/plugins/openmfa/Messages_zh_CN.properties +++ b/src/main/resources/io/jenkins/plugins/openmfa/Messages_zh_CN.properties @@ -9,4 +9,4 @@ DisplayNames.MULTI_FACTOR_AUTHENTICATION=多因素认证 (MFA) DisplayNames.OPENMFA_GLOBAL_CONFIGURATION=OpenMFA 全局配置 # Management link description -MFAManagementLink.description=查看并管理所有用户的 MFA 状态 +MFAManagementLink.DESCRIPTION=查看并管理所有用户的 MFA 状态 From 01d8eb9cbd2c9c2cb5b146daf281a2940c3de0a0 Mon Sep 17 00:00:00 2001 From: hexlianine <1669264+hexlianine@users.noreply.github.com> Date: Sat, 28 Mar 2026 22:12:57 +0800 Subject: [PATCH 10/14] fix: global config UI --- .../MFAGlobalConfiguration/config.jelly | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/main/resources/io/jenkins/plugins/openmfa/MFAGlobalConfiguration/config.jelly b/src/main/resources/io/jenkins/plugins/openmfa/MFAGlobalConfiguration/config.jelly index f0f6f2e..817b839 100644 --- a/src/main/resources/io/jenkins/plugins/openmfa/MFAGlobalConfiguration/config.jelly +++ b/src/main/resources/io/jenkins/plugins/openmfa/MFAGlobalConfiguration/config.jelly @@ -14,21 +14,19 @@
- - - - - - ${%config.exemptUsers.description} - + + + + + ${%config.exemptUsers.description} + - - - - - ${%config.exemptRoles.description} - - + + + + + ${%config.exemptRoles.description} + From 97fad6a5162e1bd728d26f4ce762d570fea7f90b Mon Sep 17 00:00:00 2001 From: hexlianine <1669264+hexlianine@users.noreply.github.com> Date: Sun, 29 Mar 2026 14:55:52 +0800 Subject: [PATCH 11/14] fix: implement fallback instance for MFAGlobalConfiguration and update tests to ensure clean state --- .../openmfa/MFAGlobalConfiguration.java | 25 +++++++++++++++- .../openmfa/service/ExemptionServiceTest.java | 30 +++++++++++++++---- 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/src/main/java/io/jenkins/plugins/openmfa/MFAGlobalConfiguration.java b/src/main/java/io/jenkins/plugins/openmfa/MFAGlobalConfiguration.java index 38b3a92..ab365d4 100644 --- a/src/main/java/io/jenkins/plugins/openmfa/MFAGlobalConfiguration.java +++ b/src/main/java/io/jenkins/plugins/openmfa/MFAGlobalConfiguration.java @@ -4,6 +4,7 @@ import hudson.Extension; import hudson.ExtensionList; import io.jenkins.plugins.openmfa.constant.UIConstants; +import java.util.concurrent.atomic.AtomicReference; import jenkins.model.GlobalConfiguration; import jenkins.model.GlobalConfigurationCategory; import lombok.Getter; @@ -23,12 +24,34 @@ @Symbol("openMFA") public class MFAGlobalConfiguration extends GlobalConfiguration { + /** + * Cached instance for fallback when ExtensionList is not available. + */ + private static final AtomicReference FALLBACK_INSTANCE = + new AtomicReference<>(); + /** * Get the singleton instance of this configuration. */ @NonNull public static MFAGlobalConfiguration get() { - return ExtensionList.lookupSingleton(MFAGlobalConfiguration.class); + // Try to get from ExtensionList first (works when Jenkins is fully initialized) + try { + return ExtensionList.lookupSingleton(MFAGlobalConfiguration.class); + } catch (IllegalStateException e) { + // Fallback: use a cached instance if ExtensionList is not available + // This can happen in some test scenarios + return FALLBACK_INSTANCE.compareAndSet(null, new MFAGlobalConfiguration()) + ? FALLBACK_INSTANCE.get() + : FALLBACK_INSTANCE.get(); + } + } + + /** + * Reset the fallback instance. For test use only. + */ + public static void resetFallbackInstance() { + FALLBACK_INSTANCE.set(null); } /** Comma/newline-separated list of roles/groups exempt from MFA */ diff --git a/src/test/java/io/jenkins/plugins/openmfa/service/ExemptionServiceTest.java b/src/test/java/io/jenkins/plugins/openmfa/service/ExemptionServiceTest.java index cd06862..68e906b 100644 --- a/src/test/java/io/jenkins/plugins/openmfa/service/ExemptionServiceTest.java +++ b/src/test/java/io/jenkins/plugins/openmfa/service/ExemptionServiceTest.java @@ -4,6 +4,7 @@ import hudson.model.User; import io.jenkins.plugins.openmfa.MFAGlobalConfiguration; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.jvnet.hudson.test.JenkinsRule; @@ -12,18 +13,24 @@ @WithJenkins class ExemptionServiceTest { - private MFAGlobalConfiguration config; - private ExemptionService exemptionService; - @BeforeEach - void setUp(JenkinsRule j) { - exemptionService = new ExemptionService(); - config = MFAGlobalConfiguration.get(); + void setUp() { + // Reset fallback instance to ensure clean state for each test + MFAGlobalConfiguration.resetFallbackInstance(); + } + + @AfterEach + void tearDown() { + // Clean up after each test + MFAGlobalConfiguration.resetFallbackInstance(); } @Test void testUserExemptByRole(JenkinsRule j) throws Exception { + j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); + MFAGlobalConfiguration config = MFAGlobalConfiguration.get(); config.setExemptRoles("admin\nci-agents"); + ExemptionService exemptionService = new ExemptionService(); User user = User.getById("adminuser", true); // User has no roles assigned yet in test environment assertFalse(exemptionService.isExempt(user)); @@ -31,27 +38,38 @@ void testUserExemptByRole(JenkinsRule j) throws Exception { @Test void testUserExemptByUsername(JenkinsRule j) throws Exception { + j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); + MFAGlobalConfiguration config = MFAGlobalConfiguration.get(); config.setExemptUsers("testuser\nexemptuser"); + ExemptionService exemptionService = new ExemptionService(); User user = User.getById("testuser", true); assertTrue(exemptionService.isExempt(user)); } @Test void testUserExemptByUsernameCaseInsensitive(JenkinsRule j) throws Exception { + j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); + MFAGlobalConfiguration config = MFAGlobalConfiguration.get(); config.setExemptUsers("TESTUSER"); + ExemptionService exemptionService = new ExemptionService(); User user = User.getById("testuser", true); assertTrue(exemptionService.isExempt(user)); } @Test void testUserNotExemptByUsername(JenkinsRule j) throws Exception { + j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); + MFAGlobalConfiguration config = MFAGlobalConfiguration.get(); config.setExemptUsers("otheruser"); + ExemptionService exemptionService = new ExemptionService(); User user = User.getById("testuser", true); assertFalse(exemptionService.isExempt(user)); } @Test void testUserNotExemptWhenListsEmpty(JenkinsRule j) throws Exception { + j.jenkins.setSecurityRealm(j.createDummySecurityRealm()); + ExemptionService exemptionService = new ExemptionService(); User user = User.getById("testuser", true); assertFalse(exemptionService.isExempt(user)); } From 6a254bce7bad12e83c8038fa8421ac7ef29f0596 Mon Sep 17 00:00:00 2001 From: hexlianine <1669264+hexlianine@users.noreply.github.com> Date: Sun, 29 Mar 2026 15:44:26 +0800 Subject: [PATCH 12/14] docs: update README with exemption features in Getting Started and Features --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index d50c92e..9fece4b 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ It introduces setup, verification, and management flows for user accounts inside - [User Flow](#user-flow) - [MFA User Management](#mfa-user-management) - [Configuration as Code (JCasC)](#configuration-as-code-jcasc) + - [MFA Exemptions](#mfa-exemptions) - [Security Notes](#security-notes) - [Development](#development) - [VS Code Debugging](#vs-code-debugging) @@ -36,6 +37,7 @@ It introduces setup, verification, and management flows for user accounts inside - Session-based MFA verification after primary login - Basic brute-force protection on code verification attempts - Supports Jenkins dark theme for all UI elements +- Configurable exemptions for service accounts and specific roles ## Requirements @@ -48,6 +50,8 @@ It introduces setup, verification, and management flows for user accounts inside 3. Configure: - `Issuer Name` - `Require MFA for all users` + - `Exempt Users` (optional) + - `Exempt Roles` (optional) 4. Save settings. 5. Ask users to enroll via the OpenMFA setup page before broad enforcement. From b5ea73c6ed7aad117242bb54074db21d9b4e27bc Mon Sep 17 00:00:00 2001 From: hexlianine <1669264+hexlianine@users.noreply.github.com> Date: Sun, 29 Mar 2026 16:34:09 +0800 Subject: [PATCH 13/14] fix: uses the same code for two branches. --- .../io/jenkins/plugins/openmfa/MFAGlobalConfiguration.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/io/jenkins/plugins/openmfa/MFAGlobalConfiguration.java b/src/main/java/io/jenkins/plugins/openmfa/MFAGlobalConfiguration.java index ab365d4..a7cbb7d 100644 --- a/src/main/java/io/jenkins/plugins/openmfa/MFAGlobalConfiguration.java +++ b/src/main/java/io/jenkins/plugins/openmfa/MFAGlobalConfiguration.java @@ -41,9 +41,8 @@ public static MFAGlobalConfiguration get() { } catch (IllegalStateException e) { // Fallback: use a cached instance if ExtensionList is not available // This can happen in some test scenarios - return FALLBACK_INSTANCE.compareAndSet(null, new MFAGlobalConfiguration()) - ? FALLBACK_INSTANCE.get() - : FALLBACK_INSTANCE.get(); + FALLBACK_INSTANCE.compareAndSet(null, new MFAGlobalConfiguration()); + return FALLBACK_INSTANCE.get(); } } From 32f380b0efae6ed716bbb04b6320c21b928a4aaf Mon Sep 17 00:00:00 2001 From: hexlianine <1669264+hexlianine@users.noreply.github.com> Date: Sun, 29 Mar 2026 16:36:42 +0800 Subject: [PATCH 14/14] fix: invalid format --- README.md | 1 + .../plugins/openmfa/MFAGlobalConfiguration/config.jelly | 8 ++------ .../openmfa/MFAGlobalConfiguration/help-exemptRoles.jelly | 7 +++---- .../openmfa/MFAGlobalConfiguration/help-exemptUsers.jelly | 7 +++---- .../io/jenkins/plugins/openmfa/Messages.properties | 2 +- 5 files changed, 10 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 9fece4b..05a6bb3 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,7 @@ You can exempt specific users or roles from MFA requirements: - **Exempt Roles**: Group/role names that bypass MFA for all members Exemptions are useful for: + - Service accounts used by CI/CD pipelines - Emergency break-glass accounts - Automated agents that cannot complete MFA diff --git a/src/main/resources/io/jenkins/plugins/openmfa/MFAGlobalConfiguration/config.jelly b/src/main/resources/io/jenkins/plugins/openmfa/MFAGlobalConfiguration/config.jelly index 817b839..8635e2a 100644 --- a/src/main/resources/io/jenkins/plugins/openmfa/MFAGlobalConfiguration/config.jelly +++ b/src/main/resources/io/jenkins/plugins/openmfa/MFAGlobalConfiguration/config.jelly @@ -17,16 +17,12 @@ - - ${%config.exemptUsers.description} - + ${%config.exemptUsers.description} - - ${%config.exemptRoles.description} - + ${%config.exemptRoles.description} diff --git a/src/main/resources/io/jenkins/plugins/openmfa/MFAGlobalConfiguration/help-exemptRoles.jelly b/src/main/resources/io/jenkins/plugins/openmfa/MFAGlobalConfiguration/help-exemptRoles.jelly index 802e991..63d9f3a 100644 --- a/src/main/resources/io/jenkins/plugins/openmfa/MFAGlobalConfiguration/help-exemptRoles.jelly +++ b/src/main/resources/io/jenkins/plugins/openmfa/MFAGlobalConfiguration/help-exemptRoles.jelly @@ -1,7 +1,6 @@
- List of role/group names exempt from MFA requirements. - Enter one role per line. - Users with any of these roles bypass MFA verification. - Use for CI/CD service accounts or automated agents. + List of role/group names exempt from MFA requirements. Enter one role per + line. Users with any of these roles bypass MFA verification. Use for CI/CD + service accounts or automated agents.
diff --git a/src/main/resources/io/jenkins/plugins/openmfa/MFAGlobalConfiguration/help-exemptUsers.jelly b/src/main/resources/io/jenkins/plugins/openmfa/MFAGlobalConfiguration/help-exemptUsers.jelly index 8dbab69..c2cf936 100644 --- a/src/main/resources/io/jenkins/plugins/openmfa/MFAGlobalConfiguration/help-exemptUsers.jelly +++ b/src/main/resources/io/jenkins/plugins/openmfa/MFAGlobalConfiguration/help-exemptUsers.jelly @@ -1,7 +1,6 @@
- List of usernames exempt from MFA requirements. - Enter one username per line. - Exempted users can access Jenkins without completing MFA verification. - Use for service accounts or emergency access accounts. + List of usernames exempt from MFA requirements. Enter one username per line. + Exempted users can access Jenkins without completing MFA verification. Use for + service accounts or emergency access accounts.
diff --git a/src/main/resources/io/jenkins/plugins/openmfa/Messages.properties b/src/main/resources/io/jenkins/plugins/openmfa/Messages.properties index 6050cce..7301b65 100644 --- a/src/main/resources/io/jenkins/plugins/openmfa/Messages.properties +++ b/src/main/resources/io/jenkins/plugins/openmfa/Messages.properties @@ -9,4 +9,4 @@ DisplayNames.MULTI_FACTOR_AUTHENTICATION=Multi-Factor Authentication (MFA) DisplayNames.OPENMFA_GLOBAL_CONFIGURATION=OpenMFA # Management link description -MFAManagementLink.DESCRIPTION=Manage user MFA enrollment and reset MFA for users \ No newline at end of file +MFAManagementLink.DESCRIPTION=Manage user MFA enrollment and reset MFA for users