Skip to content

make sure pre saml login is enabled after saml is disabled - #13953

Open
DaanHoogland wants to merge 3 commits into
mainfrom
ghi12595-saml-disable-ldap-fallback
Open

DaanHoogland wants to merge 3 commits into
mainfrom
ghi12595-saml-disable-ldap-fallback

Conversation

@DaanHoogland

@DaanHoogland DaanHoogland commented Aug 22, 2026 •

Copy link
Copy Markdown
Contributor

Description

This PR...

Fixes: #12595

  • On enabling SAML, the user's current Source is now saved (via the generic user_details key/value store, UserDetailsDao — the same mechanism UserPasswordResetManagerImpl and the OAuth2 login command already use) before it gets overwritten to SAML2. Skipped if already SAML2/SAML2DISABLED (nothing meaningful to remember), and guarded against a null source (an edge case I found via the pre-existing test).
  • On disabling SAML with enable.login.with.disabled.saml=true, it now restores that saved source (e.g. LDAP) via a new getPreSamlSource helper, instead of hardcoding UNKNOWN. Falls back to UNKNOWN if nothing was ever recorded (pre-existing users from before this fix) or the stored value is unrecognized.

Types of changes

  • Breaking change (fix or feature that would cause existing functionality to change)
  • New feature (non-breaking change which adds functionality)
  • Bug fix (non-breaking change which fixes an issue)
  • Enhancement (improves an existing feature and functionality)
  • Cleanup (Code refactoring and cleanup, that may add test cases)
  • Build/CI
  • Test (unit or integration test code)

Feature/Enhancement Scale or Bug Severity

Feature/Enhancement Scale

  • Major
  • Minor

Bug Severity

  • BLOCKER
  • Critical
  • Major
  • Minor
  • Trivial

Screenshots (if appropriate):

How Has This Been Tested?

How did you try to break this feature and the system with this change?

@codecov

codecov Bot commented Aug 22, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 3.70%. Comparing base (5e5ae0c) to head (7eadb76).
⚠️ Report is 138 commits behind head on main.

❗ There is a different number of reports uploaded between BASE (5e5ae0c) and HEAD (7eadb76). Click for more details.

HEAD has 1 upload less than BASE
Flag BASE (5e5ae0c) HEAD (7eadb76)
unittests 1 0
Additional details and impacted files
@@              Coverage Diff              @@
##               main   #13953       +/-   ##
=============================================
- Coverage     19.72%    3.70%   -16.03%     
=============================================
  Files          6371      493     -5878     
  Lines        575738    42558   -533180     
  Branches      70471     8063    -62408     
=============================================
- Hits         113582     1578   -112004     
+ Misses       449810    40751   -409059     
+ Partials      12346      229    -12117     
Flag Coverage Δ
uitests 3.70% <ø> (+0.29%) ⬆️
unittests ?

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

github-actions Bot commented Aug 22, 2026 •

Copy link
Copy Markdown

🔴 Test Coverage Grade: D — Marginal

Metric Value
Line coverage 25.50%
Branch coverage 19.80%

Grade Scale

Grade Line Coverage Meaning
🟢 A ≥ 80% Excellent - this code sleeps well at night 😴
🟡 B 60-79% Good - almost there, don't stop now 😉
🟠 C 40-59% Acceptable - your code is wearing a seatbelt, but no airbags 😬
🔴 D 20-39% Marginal - boldly shipping where no test has gone before 🖖
⛔ F < 20% Failing - tests? what tests? 🔥

Branch coverage is shown as a secondary signal. Grade is determined by line coverage.
View full Actions run

@sonarqubecloud

Copy link
Copy Markdown

@kiranchavala

Copy link
Copy Markdown
Member

@blueorangutan package

@blueorangutan

Copy link
Copy Markdown

@kiranchavala a [SL] Jenkins job has been kicked to build packages. It will be bundled with no SystemVM templates. I'll keep you posted as I make progress.

@blueorangutan

Copy link
Copy Markdown

Packaging result [SF]: ✔️ el8 ✔️ el9 ✔️ el10 ✔️ debian ✔️ suse15. SL-JID 19161

@kiranchavala kiranchavala left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DaanHoogland

Issue is present

  1. Configure LDAP server . add a ldap account

  2. Log in as the ldap user

(localcloud) 🐱 > list users username=kiran listall=true filter=username,id,usersource
⣷ 😸 trying to log in...
{
  "count": 1,
  "user": [
    {
      "id": "ad5e867e-5ce2-43e0-ac4c-3c5450f8f8ca",
      "username": "kiran",
      "usersource": "ldap"
    }
  ]
}

  1. Ldap login succesfull

  2. Configure saml on the ldap user

(localcloud) 🐱 > list users username=kiran listall=true filter=username,id,usersource
{
  "count": 1,
  "user": [
    {
      "id": "ad5e867e-5ce2-43e0-ac4c-3c5450f8f8ca",
      "username": "kiran",
      "usersource": "saml2"
    }
  ]
}

  1. Saml login successfull

  2. Disable Saml authentication on the user

(localcloud) 🐱 > list users username=kiran listall=true filter=username,id,usersource
{
  "count": 1,
  "user": [
    {
      "id": "ad5e867e-5ce2-43e0-ac4c-3c5450f8f8ca",
      "username": "kiran",
      "usersource": "saml2disabled"
    }
  ]
}

  1. Unable to login with ldap
mysql> select * from user_details;
+----+---------+---------------+-------+---------+
| id | user_id | name          | value | display |
+----+---------+---------------+-------+---------+
|  2 |       5 | PreSamlSource | LDAP  |       1 |
+----+---------+---------------+-------+---------+
1 row in set (0.00 sec)
  1. logs
2026-09-15 10:48:28,138 DEBUG [c.c.a.ApiServlet] (qtp1151704483-16:[ctx-c49d6da0]) (logid:a8550e5c) ===START===  10.0.3.251 -- POST   login
2026-09-15 10:48:28,138 DEBUG [c.c.a.ApiSessionListener] (qtp1151704483-16:[ctx-c49d6da0]) (logid:a8550e5c) Session created by Id : node0tjykvf1cdsxl1kne3qnrxnejw9 , session: Session@add86b1{id=node0tjykvf1cdsxl1kne3qnrxnejw9,x=node0tjykvf1cdsxl1kne3qnrxnejw9.node0,req=1,res=true} , source: Session@add86b1{id=node0tjykvf1cdsxl1kne3qnrxnejw9,x=node0tjykvf1cdsxl1kne3qnrxnejw9.node0,req=1,res=true} , event: javax.servlet.http.HttpSessionEvent[source=Session@add86b1{id=node0tjykvf1cdsxl1kne3qnrxnejw9,x=node0tjykvf1cdsxl1kne3qnrxnejw9.node0,req=1,res=true}]
2026-09-15 10:48:28,142 DEBUG [c.c.u.AccountManagerImpl] (qtp1151704483-16:[ctx-c49d6da0]) (logid:a8550e5c) Attempting to log in user: kiran in domain 1
2026-09-15 10:48:28,143 DEBUG [c.c.u.AccountManagerImpl] (qtp1151704483-16:[ctx-c49d6da0]) (logid:a8550e5c) Unable to authenticate user with username kiran in domain 1
2026-09-15 10:48:28,144 DEBUG [c.c.u.AccountManagerImpl] (qtp1151704483-16:[ctx-c49d6da0]) (logid:a8550e5c) User: kiran in domain 1 has failed to log in
2026-09-15 10:48:28,555 DEBUG [c.c.a.ApiSessionListener] (qtp1151704483-16:[ctx-c49d6da0]) (logid:a8550e5c) Session destroyed by Id : node0tjykvf1cdsxl1kne3qnrxnejw9 , session: Session@add86b1{id=node0tjykvf1cdsxl1kne3qnrxnejw9,x=node0tjykvf1cdsxl1kne3qnrxnejw9.node0,req=1,res=true} , source: Session@add86b1{id=node0tjykvf1cdsxl1kne3qnrxnejw9,x=node0tjykvf1cdsxl1kne3qnrxnejw9.node0,req=1,res=true} , event: javax.servlet.http.HttpSessionEvent[source=Session@add86b1{id=node0tjykvf1cdsxl1kne3qnrxnejw9,x=node0tjykvf1cdsxl1kne3qnrxnejw9.node0,req=1,res=true}]
2026-09-15 10:48:28,556 DEBUG [c.c.a.ApiServlet] (qtp1151704483-16:[ctx-c49d6da0]) (logid:a8550e5c) Authentication failure: {"loginresponse":{"uuidList":[],"errorcode":531,"errortext":"Failed to authenticate user kiran in domain 1; please provide valid credentials"}}
2026-09-15 10:48:28,556 DEBUG [c.c.a.ApiServlet] (qtp1151704483-16:[ctx-c49d6da0]) (logid:a8550e5c) ===END===  10.0.3.251 -- POST   login

@DaanHoogland

Copy link
Copy Markdown
Contributor Author

@DaanHoogland

Issue is present

  1. Configure LDAP server . add a ldap account
  2. Log in as the ldap user
(localcloud) 🐱 > list users username=kiran listall=true filter=username,id,usersource
⣷ 😸 trying to log in...
{
  "count": 1,
  "user": [
    {
      "id": "ad5e867e-5ce2-43e0-ac4c-3c5450f8f8ca",
      "username": "kiran",
      "usersource": "ldap"
    }
  ]
}
  1. Ldap login succesfull
  2. Configure saml on the ldap user
(localcloud) 🐱 > list users username=kiran listall=true filter=username,id,usersource
{
  "count": 1,
  "user": [
    {
      "id": "ad5e867e-5ce2-43e0-ac4c-3c5450f8f8ca",
      "username": "kiran",
      "usersource": "saml2"
    }
  ]
}
  1. Saml login successfull
  2. Disable Saml authentication on the user
(localcloud) 🐱 > list users username=kiran listall=true filter=username,id,usersource
{
  "count": 1,
  "user": [
    {
      "id": "ad5e867e-5ce2-43e0-ac4c-3c5450f8f8ca",
      "username": "kiran",
      "usersource": "saml2disabled"
    }
  ]
}
  1. Unable to login with ldap
mysql> select * from user_details;
+----+---------+---------------+-------+---------+
| id | user_id | name          | value | display |
+----+---------+---------------+-------+---------+
|  2 |       5 | PreSamlSource | LDAP  |       1 |
+----+---------+---------------+-------+---------+
1 row in set (0.00 sec)
  1. logs
2026-09-15 10:48:28,138 DEBUG [c.c.a.ApiServlet] (qtp1151704483-16:[ctx-c49d6da0]) (logid:a8550e5c) ===START===  10.0.3.251 -- POST   login
2026-09-15 10:48:28,138 DEBUG [c.c.a.ApiSessionListener] (qtp1151704483-16:[ctx-c49d6da0]) (logid:a8550e5c) Session created by Id : node0tjykvf1cdsxl1kne3qnrxnejw9 , session: Session@add86b1{id=node0tjykvf1cdsxl1kne3qnrxnejw9,x=node0tjykvf1cdsxl1kne3qnrxnejw9.node0,req=1,res=true} , source: Session@add86b1{id=node0tjykvf1cdsxl1kne3qnrxnejw9,x=node0tjykvf1cdsxl1kne3qnrxnejw9.node0,req=1,res=true} , event: javax.servlet.http.HttpSessionEvent[source=Session@add86b1{id=node0tjykvf1cdsxl1kne3qnrxnejw9,x=node0tjykvf1cdsxl1kne3qnrxnejw9.node0,req=1,res=true}]
2026-09-15 10:48:28,142 DEBUG [c.c.u.AccountManagerImpl] (qtp1151704483-16:[ctx-c49d6da0]) (logid:a8550e5c) Attempting to log in user: kiran in domain 1
2026-09-15 10:48:28,143 DEBUG [c.c.u.AccountManagerImpl] (qtp1151704483-16:[ctx-c49d6da0]) (logid:a8550e5c) Unable to authenticate user with username kiran in domain 1
2026-09-15 10:48:28,144 DEBUG [c.c.u.AccountManagerImpl] (qtp1151704483-16:[ctx-c49d6da0]) (logid:a8550e5c) User: kiran in domain 1 has failed to log in
2026-09-15 10:48:28,555 DEBUG [c.c.a.ApiSessionListener] (qtp1151704483-16:[ctx-c49d6da0]) (logid:a8550e5c) Session destroyed by Id : node0tjykvf1cdsxl1kne3qnrxnejw9 , session: Session@add86b1{id=node0tjykvf1cdsxl1kne3qnrxnejw9,x=node0tjykvf1cdsxl1kne3qnrxnejw9.node0,req=1,res=true} , source: Session@add86b1{id=node0tjykvf1cdsxl1kne3qnrxnejw9,x=node0tjykvf1cdsxl1kne3qnrxnejw9.node0,req=1,res=true} , event: javax.servlet.http.HttpSessionEvent[source=Session@add86b1{id=node0tjykvf1cdsxl1kne3qnrxnejw9,x=node0tjykvf1cdsxl1kne3qnrxnejw9.node0,req=1,res=true}]
2026-09-15 10:48:28,556 DEBUG [c.c.a.ApiServlet] (qtp1151704483-16:[ctx-c49d6da0]) (logid:a8550e5c) Authentication failure: {"loginresponse":{"uuidList":[],"errorcode":531,"errortext":"Failed to authenticate user kiran in domain 1; please provide valid credentials"}}
2026-09-15 10:48:28,556 DEBUG [c.c.a.ApiServlet] (qtp1151704483-16:[ctx-c49d6da0]) (logid:a8550e5c) ===END===  10.0.3.251 -- POST   login

@kiranchavala , did you set enable.login.with.disabled.saml=true during this test?

Copilot AI balanced review requested due to automatic review settings October 20, 2026 07:55
@DaanHoogland

Copy link
Copy Markdown
Contributor Author

@kiranchavala , I added a test to actually confirm the behaviour is maintained as you describe:
https://github.com/apache/cloudstack/pull/13953/changes#diff-cef68adc2348e7b7e031dd20ea4b51afb3bfd5acb62e80dc9f86833d763cf0d8R237

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Copilot review overview

5 open issues
What changed in this PR

Updates SAML enable/disable behavior to preserve and restore a user’s pre-SAML authentication source (e.g., LDAP) when SAML is disabled (optionally via config), instead of always falling back to UNKNOWN.

Changes:

  • Persist the user’s pre-SAML User.Source into user_details when enabling SAML (skipping SAML-related and null sources).
  • Restore the persisted source when disabling SAML if enable.login.with.disabled.saml=true, otherwise set SAML2DISABLED.
  • Add unit tests covering persistence, restoration, and fallback behaviors.
File Description
plugins/​user-authenticators/​saml2/​src/​main/​java/​org/​apache/​cloudstack/​saml/​SAML2AuthManagerImpl.java Store a pre-SAML source detail on enable; restore it on disable when the config flag is enabled.
plugins/​user-authenticators/​saml2/​src/​test/​java/​org/​apache/​cloudstack/​SAML2AuthManagerImplTest.java Add tests for storing/restoring pre-SAML source and for config-driven disable behavior.

Review effort: Lite

Comment on lines +478 to +481
private boolean disableSAMLForUser(UserVO user) {
if (!user.getSource().equals(User.Source.SAML2)) {
return false;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private boolean disableSAMLForUser(UserVO user) {
if (!user.getSource().equals(User.Source.SAML2)) {
return false;
}
private boolean disableSAMLForUser(UserVO user) {
if (!User.Source.SAML2.equals(user.getSource())) {
return false;
}

Comment on lines +471 to +473
if (user.getSource() != null && !User.Source.SAML2.equals(user.getSource()) && !User.Source.SAML2DISABLED.equals(user.getSource())) {
userDetailsDao.addDetail(user.getId(), PRE_SAML_SOURCE_DETAIL_KEY, user.getSource().toString(), false);
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (user.getSource() != null && !User.Source.SAML2.equals(user.getSource()) && !User.Source.SAML2DISABLED.equals(user.getSource())) {
userDetailsDao.addDetail(user.getId(), PRE_SAML_SOURCE_DETAIL_KEY, user.getSource().toString(), false);
}
if (user.getSource() != null && !User.Source.SAML2.equals(user.getSource()) && !User.Source.SAML2DISABLED.equals(user.getSource())) {
userDetailsDao.addDetail(user.getId(), PRE_SAML_SOURCE_DETAIL_KEY, user.getSource().name(), false);
}

Comment on lines +105 to +131
private static <T> void setConfigKeyValue(ConfigKey<T> configKey, T value) {
try {
Field valueField = ConfigKey.class.getDeclaredField("_value");
valueField.setAccessible(true);
valueField.set(configKey, value);

Field dynamicField = ConfigKey.class.getDeclaredField("_isDynamic");
dynamicField.setAccessible(true);
dynamicField.setBoolean(configKey, false);
} catch (IllegalAccessException | NoSuchFieldException e) {
throw new RuntimeException("Failed to set ConfigKey value", e);
}
}

private static void resetConfigKeyValue(ConfigKey<?> configKey) {
try {
Field valueField = ConfigKey.class.getDeclaredField("_value");
valueField.setAccessible(true);
valueField.set(configKey, null);

Field dynamicField = ConfigKey.class.getDeclaredField("_isDynamic");
dynamicField.setAccessible(true);
dynamicField.setBoolean(configKey, true);
} catch (IllegalAccessException | NoSuchFieldException e) {
throw new RuntimeException("Failed to reset ConfigKey value", e);
}
}
Comment on lines +183 to +184
@Test
public void testAuthorizeUserDoesNotRestorePreSamlSourceWhenAlreadyAuthorized() {
@sonarqubecloud

Copy link
Copy Markdown

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Ready

Development

Successfully merging this pull request may close these issues.

Ldap imported accounts which are saml enabled doesn't fallback to ldap if saml is disabled

4 participants