Skip to content

Conversation

jackyalbo
Copy link
Contributor

@jackyalbo jackyalbo commented Apr 20, 2025

Describe the Problem

We want to support authenticating to noobaa services using LDAP user/password for authentication
We will use assume_role_with_web_identity with self self-created JWT token of this structure:

{ 
  user: <user-name>, 
  password: <password> , 
  type: "ldap" 
}

signed by a jwt-secret saved under /etc/noobaa-server/ldap_config. looking like this:

{
  "uri": "ldap://192.168.105.16:636",
  "admin": "cn=admin,dc=planetexpress,dc=com",
  "secret": "GoodNewsEveryone",
  "search_dn": "ou=people,dc=planetexpress,dc=com",
  "dn_attribute": "uid",
  "search_base": "dc=example,dc=com",
  "search_scope": "sub",
  **"jwt_secret": "abcdefgh12345678"**
}

Explain the Changes

  1. ldap_client
  2. support assume_role_with_web_identity

Issues: Fixed #xxx / Gap #xxx

Testing Instructions:

  • Doc added/updated
  • Tests added

Summary by CodeRabbit

  • New Features

    • STS AssumeRoleWithWebIdentity using LDAP-backed web-identity tokens to issue temporary S3 credentials.
    • Service auto-connects to LDAP at startup when configured.
    • Public config option to set LDAP config path (default /etc/noobaa-server/ldap_config).
  • Bug Fixes

    • STS error responses standardized; added InvalidIdentityToken for clearer JWT/claim failures.
  • Documentation

    • Added design doc describing LDAP web-identity authentication flow and config.
  • Chores

    • Added LDAP runtime dependency and STS utility support.

@jackyalbo jackyalbo requested a review from guymguym April 20, 2025 15:09
@jackyalbo jackyalbo force-pushed the jacky_ldap1 branch 2 times, most recently from 822c72b to 1dbfe16 Compare April 21, 2025 13:15
@@ -97,6 +97,7 @@
"jsonwebtoken": "9.0.2",
"linux-blockutils": "0.2.0",
"lodash": "4.17.21",
"ldapts": "7.3.1",
Copy link
Member

Choose a reason for hiding this comment

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

Worth considering using openldap natively instead of adding this dependency. We should at least evaluate the effort to implement and maintain it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

keeping this open for now as a POC. Added this item to the "Next Steps" list in the MD file.

Copy link

This PR had no activity for too long - it will now be labeled stale. Update it to prevent it from getting closed.

@github-actions github-actions bot added the Stale label Jul 24, 2025
Copy link

coderabbitai bot commented Jul 24, 2025

Walkthrough

Adds LDAP-based authentication and STS AssumeRoleWithWebIdentity: new LDAP client and config path, ldapts dependency, STS web‑identity operation and routing, centralized STS utilities, SDK JWT/LDAP logic, endpoint startup LDAP connect, documentation, and integration tests.

Changes

Cohort / File(s) Summary
Config & Dependency
config.js, package.json
Adds config.LDAP_CONFIG_PATH (default '/etc/noobaa-server/ldap_config') and adds ldapts to dependencies.
LDAP Client
src/util/ldap_client.js
New singleton LdapClient with config file watching (config.LDAP_CONFIG_PATH), TLS admin bind, connect/reconnect, authenticate(user,password), and is_ldap_configured() export.
Endpoint Init
src/endpoint/endpoint.js
On startup, checks ldap_client.is_ldap_configured() and invokes ldap_client.instance().connect() when configured.
SDK Changes
src/sdk/sts_sdk.js
Adds _assume_role(role_arn) and get_assumed_ldap_user(req) with JWT verify/decode, LDAP authentication, role/account resolution, and authorization allowance for web‑identity op.
STS Utilities & AssumeRole
src/endpoint/sts/sts_utils.js, src/endpoint/sts/ops/sts_post_assume_role.js
New sts_utils exporting generate_session_token and parse_sts_duration; refactors sts_post_assume_role to use these utilities and map errors.
STS Web‑Identity Op & Routing
src/endpoint/sts/ops/sts_post_assume_role_with_web_identity.js, src/endpoint/sts/sts_rest.js, src/endpoint/sts/sts_errors.js
Adds AssumeRoleWithWebIdentity handler and registers action/op; introduces new STS op file; adjusts STS error XML shape and adds StsError.InvalidIdentityToken.
Tests
src/test/integration_tests/api/sts/test_sts.js
Adds web‑identity negative tests (malformed/invalid/missing JWT claims) using jsonwebtoken and ldap_client.
Docs
docs/design/ldap.md
New design doc describing LDAP POC, config format (/etc/noobaa-server/ldap_config), JWT usage, STS flow, examples, and next steps.

Sequence Diagram(s)

sequenceDiagram
  participant EP as Endpoint Init
  participant LDAP as LdapClient
  EP->>LDAP: is_ldap_configured()
  alt Config exists
    EP->>LDAP: instance().connect()
    loop Retry until bind
      LDAP->>LDAP: _connect() / _bind(admin)
    end
  else No config
    EP-->>EP: Skip LDAP connect
  end
Loading
sequenceDiagram
  participant C as Client
  participant STS as STS REST
  participant OP as AssumeRoleWithWebIdentity Op
  participant SDK as STS SDK
  participant JWT as jsonwebtoken
  participant LDAP as LdapClient
  participant AC as Account Cache
  participant U as STS Utils

  C->>STS: POST AssumeRoleWithWebIdentity (form)
  STS->>OP: Dispatch request
  OP->>SDK: get_assumed_ldap_user(req)
  SDK->>LDAP: is_connected()
  alt JWT secret configured
    SDK->>JWT: verify(web_identity_token, secret)
  else No secret
    SDK->>JWT: decode(web_identity_token)
  end
  SDK->>LDAP: authenticate(user, password)
  SDK->>AC: _assume_role(role_arn)
  SDK-->>OP: { access_key, role_config, dn }
  OP->>SDK: generate_temp_access_keys()
  OP->>U: generate_session_token(auth_options, expiry)
  OP-->>C: XML response with Credentials and AssumedRoleUser
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Suggested reviewers

  • guymguym
  • liranmauda
  • romayalon

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@jackyalbo jackyalbo marked this pull request as ready for review August 17, 2025 13:39
@jackyalbo jackyalbo changed the title LDAP Support - Part 1 LDAP Support - POC Aug 17, 2025
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 17

♻️ Duplicate comments (4)
package.json (1)

103-103: Adding ldapts dependency: confirm suitability and security posture

  • Sanity-check ldapts 7.3.1 compatibility with your Node.js/runtime and run a quick vuln/license scan.
  • Longer term, consider native OpenLDAP for performance and reduced JS attack surface (kept as a Next Step in the MD).

Use this script to check latest version and known advisories:

#!/bin/bash
set -euo pipefail

echo "Latest ldapts version on npm:"
npm view ldapts version

echo "All ldapts versions in the 7.x line:"
npm view ldapts versions --json | jq -r '.[]' | grep -E '^7\.'

echo "Basic advisory search (GitHub security advisories for ldapts):"
gh api graphql -f query='
{
  securityVulnerabilities(first: 10, ecosystem: NPM, package: "ldapts") {
    nodes {
      advisory { ghsaId summary severity publishedAt }
      vulnerableVersionRange
      firstPatchedVersion { identifier }
    }
  }
}'
docs/design/ldap.md (1)

95-101: “Next steps”: track OpenLDAP evaluation and add system test plan

  • Keep the OpenLDAP-native evaluation item (performance and security benefits). This echoes prior review feedback.
  • Consider adding a matrix test (OpenLDAP and AD) end-to-end flow using docker-compose with TLS.

I can draft a docker-compose and a system test plan if helpful.

src/util/ldap_client.js (2)

14-19: Await unbind and guard admin_client on disconnect.

Unbinding in ldapts is async. Not awaiting can cause races; also guard against undefined admin_client.

Apply:

 async disconnect() {
     dbg.log0('ldap client disconnect called');
     this._disconnected_state = true;
     this._connect_promise = null;
-    this.admin_client.unbind();
+    if (this.admin_client) {
+        try {
+            await this.admin_client.unbind();
+        } catch (e) {
+            dbg.warn('ldap client unbind failed', e?.message || e);
+        }
+    }
 }

21-25: Await disconnect in reconnect to avoid races.

Reconnect should await the disconnect before connecting again.

 async reconnect() {
     dbg.log0(`reconnect called`);
-    this.disconnect();
-    return this.connect();
+    await this.disconnect();
+    return this.connect();
 }
🧹 Nitpick comments (14)
src/endpoint/sts/sts_errors.js (1)

29-36: Enhance STS Error Envelope with Type and Conditional Detail

Refine the STS error reply in src/endpoint/sts/sts_errors.js to align with AWS Query conventions:

– Infer and include Type (“Sender” for 4xx, “Receiver” for 5xx)
– Omit Detail when it’s undefined
– Only attach Resource when non-empty

Apply this patch around the existing reply(resource, request_id):

--- a/src/endpoint/sts/sts_errors.js
+++ b/src/endpoint/sts/sts_errors.js
@@ reply(resource, request_id) {
-        const xml = {
-            ErrorResponse: {
-                Error: {
-                    Code: this.code,
-                    Message: this.message,
-                    Resource: resource || '',
-                    Detail: this.detail,
-                },
-                RequestId: request_id || '',
-            }
-        };
+        // Build the Error block per AWS Query API
+        const err = {
+            Code: this.code,
+            Message: this.message,
+            Type: this.http_code >= 500 ? 'Receiver' : 'Sender',
+        };
+        if (resource)    err.Resource = resource;
+        if (this.detail !== undefined) err.Detail   = this.detail;
+
+        const xml = {
+            ErrorResponse: {
+                Error: err,
+                RequestId: request_id || '',
+            },
+        };

• No integration tests reference Detail, and STS error tests only assert on the error code.
• Existing tests that mention Resource apply to successful responses and aren’t affected.

This change is optional but recommended for full AWS STS compatibility.

docs/design/ldap.md (2)

90-90: Replace bare URL with a link label for markdownlint compliance

Wrap the URL in markdown syntax:

-read more here: https://docs.aws.amazon.com/cli/latest/reference/sts/assume-role-with-web-identity.html
+Read more here: [AWS CLI assume-role-with-web-identity](https://docs.aws.amazon.com/cli/latest/reference/sts/assume-role-with-web-identity.html)

105-105: Heading trailing colon and minor grammar

Remove trailing colon per MD026 and adjust casing:

-## Appendix A: Creating account w/ role config using NooBaa API:
+## Appendix A: Creating an account with role config using the NooBaa API
src/test/integration_tests/api/sts/test_sts.js (1)

894-952: Typo: “indentity” → “identity” in suite title

Fix the spelling in the describe block.

Apply this diff:

-mocha.describe('Assume role with web indentity tests', function() {
+mocha.describe('Assume role with web identity tests', function() {

Optional: Guard tests when LDAP is not configured

To avoid flakiness in environments without LDAP config/jwt_secret, skip this suite when LDAP isn’t configured/connected.

Apply this diff within the existing before() hook:

 mocha.before(async function() {
   const self = this; // eslint-disable-line no-invalid-this
   self.timeout(60000);
 
-  // const random_access_keys = cloud_utils.generate_access_keys();
+  // Skip if LDAP is not configured/connected
+  try {
+    if (!(await ldap_client.is_ldap_configured()) || !ldap_client.instance().is_connected()) {
+      this.skip(); // eslint-disable-line no-invalid-this
+      return;
+    }
+  } catch (e) {
+    this.skip(); // eslint-disable-line no-invalid-this
+    return;
+  }
+
   anon_sts = new AWS.STS({
src/endpoint/sts/sts_utils.js (2)

16-18: Nit: Debug message refers to old module/function

Update the log prefix to reflect the current module/function name.

Apply this diff:

-    dbg.log1('sts_post_assume_role.make_session_token: ', auth_options, expiry);
+    dbg.log1('sts_utils.generate_session_token: ', auth_options, expiry);

20-21: Nit: Outdated TODO

This module already is the generalized utils file. Remove or reword the TODO.

Apply this diff:

-// TODO: Generalize and move to a utils file in the future
+// STS utility helpers for duration parsing and session token generation
src/sdk/sts_sdk.js (3)

98-105: Use err.name to detect expired JWTs

jsonwebtoken sets err.name === 'TokenExpiredError' and err.message is typically “jwt expired”. Checking err.message for “TokenExpiredError” will not trigger correctly.

Apply this diff:

-            } catch (err) {
+            } catch (err) {
                 dbg.error('get_assumed_ldap_user error: JWT token verification failed', err);
-                if (err.message.includes('TokenExpiredError')) {
+                if (err && err.name === 'TokenExpiredError') {
                     throw new RpcError('EXPIRED_WEB_IDENTITY_TOKEN', err.message);
                 } else {
                     throw new RpcError('INVALID_WEB_IDENTITY_TOKEN', err.message);
                 }
             }

107-109: Fail closed when JWT secret is not configured

Decoding without verification accepts untrusted tokens. Since LDAP auth still validates user/password, the risk is lower, but best practice is to reject un-verifiable tokens to prevent abuse and ambiguity.

Apply this diff:

-        } else {
-            dbg.warn('get_assumed_ldap_user error: No LDAP JWT secret found');
-            web_token = jwt.decode(req.body.web_identity_token);
-        }
+        } else {
+            dbg.error('get_assumed_ldap_user error: No LDAP JWT secret found');
+            throw new RpcError('INVALID_WEB_IDENTITY_TOKEN', 'JWT secret not configured');
+        }

If you intentionally allow decode-only in dev, consider gating it behind an explicit config flag.


56-75: Optional: Avoid re-parsing RoleArn in multiple places

Have _assume_role return access_key and role_name with the account to reduce duplication and subtle parsing inconsistencies.

Apply this refactor:

 async _assume_role(role_arn) {
-        const role_name_idx = role_arn.lastIndexOf('/') + 1;
-        const role_name = role_arn.slice(role_name_idx);
-        const access_key = role_arn.split(':')[4];
+        const role_name_idx = role_arn.lastIndexOf('/') + 1;
+        const role_name = role_arn.slice(role_name_idx);
+        const access_key = role_arn.split(':')[4];
 
         const account = await account_cache.get_with_cache({
             bucketspace: this._get_bucketspace(),
             access_key,
         });
         if (!account) {
             throw new RpcError('NO_SUCH_ACCOUNT', 'No such account with access_key: ' + access_key);
         }
         if (!account.role_config || account.role_config.role_name !== role_name) {
             throw new RpcError('NO_SUCH_ROLE', `Role not found`);
         }
-        dbg.log0('sts_sdk.get_assumed_role res', account,
-            'account.role_config: ', account.role_config);
-
-        return account;
+        return { account, access_key, role_name };
 }
 
 async get_assumed_role(req) {
     dbg.log1('sts_sdk.get_assumed_role body', req.body);
     // arn:aws:sts::access_key:role/role_name
-    const account = await this._assume_role(req.body.role_arn);
+    const { account, access_key } = await this._assume_role(req.body.role_arn);
 
     return {
-        access_key: req.body.role_arn.split(':')[4],
+        access_key,
         role_config: account.role_config
     };
 }
 
 ...
-        const account = await this._assume_role(req.body.role_arn);
+        const { account, access_key } = await this._assume_role(req.body.role_arn);
 ...
     return {
-        access_key: req.body.role_arn.split(':')[4],
+        access_key,
         role_config: account.role_config,
         dn,
     };

Also applies to: 77-86, 131-135

src/endpoint/sts/ops/sts_post_assume_role_with_web_identity.js (2)

36-56: Add defensive error handling around temporary key generation.

If generate_temp_access_keys fails, we’ll return a generic InternalFailure from the outer try/catch. Consider catching and mapping to a clearer STS error (e.g., ServiceFailure) to aid troubleshooting.

Example:

-    const access_keys = await req.sts_sdk.generate_temp_access_keys();
+    let access_keys;
+    try {
+        access_keys = await req.sts_sdk.generate_temp_access_keys();
+    } catch (e) {
+        dbg.error('failed generating temp access keys', e);
+        throw new StsError(StsError.InternalFailure);
+    }

38-61: Consider including ResponseMetadata (RequestId) to match STS responses.

Not mandatory if your clients don’t rely on it, but adding ResponseMetadata with a request id aligns with AWS STS and helps debugging.

src/util/ldap_client.js (3)

27-33: Async in constructor: explicitly fire-and-forget and handle errors.

load_ldap_config is async; calling it without handling may hide errors. Either mark intention or restructure.

-        this.load_ldap_config();
+        // fire-and-forget initial load; subsequent connect() will ensure readiness
+        void this.load_ldap_config();

73-75: is_connected is a method; ensure it’s invoked consistently.

Current code mixes property/method usage. Keep it as a method for consistency.


99-106: Avoid unbind on bind failure for admin_client; it may already be disconnected.

Unbinding on bind failure is fine, but handle errors and don’t assume an open connection.

 async _bind(client, user, password) {
     try {
         await client.bind(user, password);
     } catch (err) {
-        await client.unbind();
+        try { await client.unbind(); } catch (_) { /* ignore */ }
         throw err;
     }
 }
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 704e34d and 9a7a0bf.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (12)
  • config.js (1 hunks)
  • docs/design/ldap.md (1 hunks)
  • package.json (1 hunks)
  • src/endpoint/endpoint.js (5 hunks)
  • src/endpoint/sts/ops/sts_post_assume_role.js (2 hunks)
  • src/endpoint/sts/ops/sts_post_assume_role_with_web_identity.js (1 hunks)
  • src/endpoint/sts/sts_errors.js (2 hunks)
  • src/endpoint/sts/sts_rest.js (1 hunks)
  • src/endpoint/sts/sts_utils.js (1 hunks)
  • src/sdk/sts_sdk.js (5 hunks)
  • src/test/integration_tests/api/sts/test_sts.js (3 hunks)
  • src/util/ldap_client.js (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
src/test/**/*.*

⚙️ CodeRabbit Configuration File

src/test/**/*.*: Ensure that the PR includes tests for the changes.

Files:

  • src/test/integration_tests/api/sts/test_sts.js
🧬 Code Graph Analysis (8)
src/endpoint/sts/ops/sts_post_assume_role_with_web_identity.js (3)
src/endpoint/sts/ops/sts_post_assume_role.js (10)
  • dbg (4-4)
  • require (5-5)
  • require (6-6)
  • s3_utils (7-7)
  • sts_utils (8-8)
  • duration_ms (15-15)
  • duration_sec (16-16)
  • expiration_time (17-17)
  • assumed_role (18-18)
  • access_keys (30-30)
src/endpoint/sts/sts_utils.js (4)
  • dbg (4-4)
  • require (5-5)
  • duration_ms (43-43)
  • duration_sec (30-30)
src/util/http_utils.js (1)
  • CONTENT_TYPE_APP_FORM_URLENCODED (39-39)
src/endpoint/endpoint.js (1)
src/util/ldap_client.js (1)
  • ldap_client (9-9)
src/util/ldap_client.js (2)
src/sdk/sts_sdk.js (3)
  • require (6-6)
  • require (8-8)
  • ldap_client (11-11)
src/endpoint/endpoint.js (6)
  • require (39-39)
  • require (40-40)
  • require (42-42)
  • require (43-43)
  • require (45-45)
  • ldap_client (44-44)
src/endpoint/sts/sts_errors.js (1)
src/endpoint/sts/sts_rest.js (1)
  • StsError (6-6)
src/sdk/sts_sdk.js (13)
src/test/integration_tests/api/sts/test_sts.js (5)
  • jwt (11-11)
  • ldap_client (18-18)
  • account (103-103)
  • req (385-385)
  • dbg (14-14)
src/api/api.js (2)
  • require (5-5)
  • require (6-6)
src/sdk/object_sdk.js (3)
  • require (27-27)
  • account_cache (50-63)
  • dbg (9-9)
src/util/signature_utils.js (2)
  • require (13-13)
  • dbg (6-6)
src/sdk/account_sdk.js (3)
  • require (6-6)
  • require (8-8)
  • dbg (4-4)
src/util/ldap_client.js (2)
  • ldap_client (9-9)
  • dbg (10-10)
src/server/common_services/auth_server.js (1)
  • role_name (43-43)
src/endpoint/sts/ops/sts_post_assume_role_with_web_identity.js (1)
  • dbg (4-4)
src/endpoint/sts/sts_utils.js (1)
  • dbg (4-4)
src/endpoint/sts/sts_rest.js (1)
  • dbg (5-5)
src/endpoint/iam/iam_rest.js (1)
  • dbg (4-4)
src/sdk/bucketspace_nb.js (1)
  • dbg (7-7)
src/manage_nsfs/nc_master_key_manager.js (1)
  • RpcError (10-10)
src/endpoint/sts/ops/sts_post_assume_role.js (3)
src/endpoint/sts/ops/sts_post_assume_role_with_web_identity.js (5)
  • sts_utils (8-8)
  • require (5-5)
  • require (6-6)
  • dbg (4-4)
  • duration_ms (15-15)
src/test/integration_tests/api/sts/test_sts.js (3)
  • req (385-385)
  • dbg (14-14)
  • duration_ms (403-403)
src/endpoint/sts/sts_utils.js (2)
  • dbg (4-4)
  • duration_ms (43-43)
src/endpoint/sts/sts_rest.js (1)
src/endpoint/sts/ops/sts_post_assume_role.js (2)
  • require (5-5)
  • require (6-6)
src/test/integration_tests/api/sts/test_sts.js (2)
src/endpoint/sts/ops/sts_post_assume_role_with_web_identity.js (2)
  • require (5-5)
  • require (6-6)
src/util/ldap_client.js (2)
  • config (7-7)
  • ldap_client (9-9)
🪛 LanguageTool
docs/design/ldap.md

[grammar] ~9-~9: Ensure spelling is correct
Context: ...ow: 1. The client sends a signed(with a predfined constant signature) or unsigned JWT as ...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~9-~9: There might be a mistake here.
Context: ... unsigned JWT as the web identity token. 2. The system parses the JWT to extract the...

(QB_NEW_EN)


[grammar] ~16-~16: There might be a mistake here.
Context: ...users. ## Configuring the external LDAP The administrator must store the LDAP co...

(QB_NEW_EN)


[grammar] ~17-~17: There might be a mistake here.
Context: ...DAP configuration in the following file: /etc/noobaa-server/ldap_config The conf...

(QB_NEW_EN)


[grammar] ~41-~41: There might be a mistake here.
Context: ... set - unsigned tokens or tokens signed with different secret will be dropped as acc...

(QB_NEW_EN)


[grammar] ~65-~65: There might be a mistake here.
Context: ...Password", "type": "LDAP" } Sign it node.js example:js const jwt = require('...

(QB_NEW_EN)


[grammar] ~95-~95: There might be a mistake here.
Context: ... can base on dockers I used for testing: * AD image: `docker run --rm --privileged ...

(QB_NEW_EN)


[grammar] ~96-~96: There might be a mistake here.
Context: ...com/samba-in-kubernetes/samba-container) * LDAP image: `docker run --rm --privilege...

(QB_NEW_EN)


[grammar] ~97-~97: There might be a mistake here.
Context: ...ntainer/docker-test-openldap%2Fopenldap) 3. Add support to the operator side: * CLI...

(QB_NEW_EN)


[grammar] ~98-~98: There might be a mistake here.
Context: ...ap) 3. Add support to the operator side: * CLI command for configuring external LDA...

(QB_NEW_EN)


[grammar] ~99-~99: There might be a mistake here.
Context: ...LI command for configuring external LDAP * Create K8s Secret for LDAP info and moun...

(QB_NEW_EN)


[grammar] ~100-~100: There might be a mistake here.
Context: ...-server/ldap_config to the relevant pods 4. Better align and adapt to the IAM effort...

(QB_NEW_EN)


[grammar] ~101-~101: There might be a mistake here.
Context: ...dapt to the IAM effort also in POC stage 5. See if we want to support encrypted pass...

(QB_NEW_EN)


[grammar] ~105-~105: There might be a mistake here.
Context: ...ation scope if possible ## Appendix A: Creating account w/ role config using NooBaa API...

(QB_NEW_EN)


[style] ~131-~131: Consider a more concise word here.
Context: ...auth_token": "'$(cat .nbtoken)'" }' in order to assume this role:bash aws sts assum...

(IN_ORDER_TO_PREMIUM)

🪛 markdownlint-cli2 (0.17.2)
docs/design/ldap.md

90-90: Bare URL used

(MD034, no-bare-urls)


94-94: Bare URL used

(MD034, no-bare-urls)


96-96: Bare URL used

(MD034, no-bare-urls)


97-97: Bare URL used

(MD034, no-bare-urls)


102-102: Bare URL used

(MD034, no-bare-urls)


105-105: Trailing punctuation in heading
Punctuation: ':'

(MD026, no-trailing-punctuation)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: run-jest-unit-tests
  • GitHub Check: Build Noobaa Image
🔇 Additional comments (12)
src/endpoint/sts/sts_errors.js (1)

144-148: New InvalidIdentityToken constant looks good

Matches the web identity flow needs and aligns with common STS semantics.

config.js (1)

80-84: LDAP_CONFIG_PATH addition looks good; ensure secure file permissions in reader

Path is consistent with the docs. When loading this file (in ldap_client), enforce 0600 permissions and reject world/group-readable configs since it contains secrets (admin password, jwt_secret).

If you want, I can add a minimal permission check in src/util/ldap_client.js before reading this file.

src/endpoint/endpoint.js (2)

44-44: Importing ldap_client at startup: OK

Import location and usage are consistent with the new LDAP flow.


202-209: S3 server start options refactor: LGTM

Passing a consolidated options object (including loggers) improves readability.

src/endpoint/sts/ops/sts_post_assume_role.js (2)

8-8: Good move: centralizing STS helpers via sts_utils

Importing and using sts_utils for duration parsing keeps logic consistent across STS ops and simplifies maintenance.

Also applies to: 15-15


43-47: Session token generation usage looks correct

Passing seconds to sts_utils.generate_session_token aligns with jwt_utils.make_auth_token’s expiresIn semantics. No issues spotted here.

src/endpoint/sts/sts_rest.js (2)

28-30: Action map extension is consistent

Adding AssumeRoleWithWebIdentity to ACTIONS is consistent with the new op naming scheme (method_action). Looks good.


37-40: Wiring the new op is correct

The STS_OPS registration aligns with the ACTIONS mapping and file location. No concerns.

src/test/integration_tests/api/sts/test_sts.js (1)

11-11: JWT and LDAP client imports are appropriate

These imports match the new web-identity tests’ needs.

Also applies to: 19-19

src/sdk/sts_sdk.js (1)

148-153: Auth relaxation for web-identity is acceptable, but verify end-to-end flow

Short-circuiting authorize_request_account for post_assume_role_with_web_identity is appropriate. Ensure upstream in sts_rest.authorize_request does not attempt to load_requesting_account for this op (to avoid INVALID_ACCESS_KEY_ID when using anonymous clients). If needed, add an early return in authorize_request for this op.

Would you like me to prepare a follow-up patch to bypass load_requesting_account when req.op_name === 'post_assume_role_with_web_identity'?

src/endpoint/sts/ops/sts_post_assume_role_with_web_identity.js (2)

44-46: ARN/AssumedRoleId composition may be non-standard.

Using access_key as “account id” and in AssumedRoleId diverges from AWS formats (account-id and role-id). If clients parse these fields, this could break them or leak internal identifiers. Validate expected consumers.

Would you like me to scan our integration tests and SDK consumers for assumptions/parsing of these fields?


22-33: Error mapping is complete and matches upstream rpc_codes
All rpc_code values thrown by get_assumed_ldap_user—ACCESS_DENIED, EXPIRED_WEB_IDENTITY_TOKEN, and INVALID_WEB_IDENTITY_TOKEN—are explicitly handled and map to the corresponding StsError variants. Any other errors fall through to the existing InternalFailure catch-all.

Comment on lines +50 to +51
this.tls_options = this.ldap_params.tls_options || {
'rejectUnauthorized': false,
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Defaulting to rejectUnauthorized: false weakens TLS security.

This disables certificate verification and enables MITM by default. Prefer true and allow override via config for dev/test.

I can add env/config-driven toggle plus CA/cert options if needed.

🤖 Prompt for AI Agents
In src/util/ldap_client.js around lines 50 to 52, the code defaults
tls_options.rejectUnauthorized to false which disables certificate verification;
change the default to true and allow overrides from ldap_params.tls_options or
an environment/config toggle (e.g., LDAP_TLS_REJECT_UNAUTHORIZED) so dev/test
can set false while production remains secure, and ensure tls_options can accept
and pass through CA/cert/key values from config for custom trust stores.

Comment on lines 77 to 82
async connect() {
this._disconnected_state = false;
if (this._connect_promise) return this._connect_promise;
dbg.log0('connect called, current url', this.ldap_params);
this._connect_promise = this._connect();
return this._connect_promise;
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Ensure config is loaded before attempting to connect; avoid logging secrets.

Connect may run before load_ldap_config completes; admin_client could be undefined. Also logging full ldap_params leaks secrets.

 async connect() {
     this._disconnected_state = false;
     if (this._connect_promise) return this._connect_promise;
-    dbg.log0('connect called, current url', this.ldap_params);
+    if (!this.admin_client) {
+        await this.load_ldap_config();
+    }
+    // Log non-sensitive subset only
+    const { uri, search_dn, search_scope } = this.ldap_params || {};
+    dbg.log0('connect called', { uri, search_dn, search_scope });
     this._connect_promise = this._connect();
     return this._connect_promise;
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async connect() {
this._disconnected_state = false;
if (this._connect_promise) return this._connect_promise;
dbg.log0('connect called, current url', this.ldap_params);
this._connect_promise = this._connect();
return this._connect_promise;
}
async connect() {
this._disconnected_state = false;
if (this._connect_promise) return this._connect_promise;
if (!this.admin_client) {
await this.load_ldap_config();
}
// Log non-sensitive subset only
const { uri, search_dn, search_scope } = this.ldap_params || {};
dbg.log0('connect called', { uri, search_dn, search_scope });
this._connect_promise = this._connect();
return this._connect_promise;
}
🤖 Prompt for AI Agents
In src/util/ldap_client.js around lines 77 to 83, the connect method can run
before load_ldap_config has completed (leaving admin_client undefined) and
currently logs the full ldap_params (which may contain secrets); modify connect
to first ensure the config is loaded (e.g., await this.load_ldap_config() or
check a loaded flag and await it) before calling this._connect(), guard against
undefined admin_client by throwing a clear error or waiting until admin_client
is initialized, and remove or replace dbg.log0('connect called, current url',
this.ldap_params) with a non-sensitive log (mask/remove secrets or log only
host/port or a connection-attempt event) so secrets are not emitted.

Comment on lines +85 to +96
async _connect() {
let is_connected = false;
while (!is_connected) {
try {
await this._bind(this.admin_client, this.ldap_params.admin, this.ldap_params.secret);
dbg.log0('_connect: initial connect succeeded');
is_connected = true;
} catch (err) {
dbg.error('_connect: initial connect failed, will retry', err.message);
await P.delay(3000);
}
}
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Make the connect retry loop cancellable and robust.

The loop never checks _disconnected_state; disconnect won’t stop retries. Also protect against missing admin_client.

 async _connect() {
-    let is_connected = false;
-    while (!is_connected) {
+    let is_connected = false;
+    while (!is_connected && !this._disconnected_state) {
         try {
-            await this._bind(this.admin_client, this.ldap_params.admin, this.ldap_params.secret);
+            if (!this.admin_client) throw new Error('LDAP admin client not initialized');
+            await this._bind(this.admin_client, this.ldap_params.admin, this.ldap_params.secret);
             dbg.log0('_connect: initial connect succeeded');
             is_connected = true;
         } catch (err) {
             dbg.error('_connect: initial connect failed, will retry', err.message);
-            await P.delay(3000);
+            if (this._disconnected_state) break;
+            await P.delay(3000);
         }
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async _connect() {
let is_connected = false;
while (!is_connected) {
try {
await this._bind(this.admin_client, this.ldap_params.admin, this.ldap_params.secret);
dbg.log0('_connect: initial connect succeeded');
is_connected = true;
} catch (err) {
dbg.error('_connect: initial connect failed, will retry', err.message);
await P.delay(3000);
}
}
}
async _connect() {
let is_connected = false;
while (!is_connected && !this._disconnected_state) {
try {
if (!this.admin_client) throw new Error('LDAP admin client not initialized');
await this._bind(this.admin_client, this.ldap_params.admin, this.ldap_params.secret);
dbg.log0('_connect: initial connect succeeded');
is_connected = true;
} catch (err) {
dbg.error('_connect: initial connect failed, will retry', err.message);
if (this._disconnected_state) break;
await P.delay(3000);
}
}
}
🤖 Prompt for AI Agents
In src/util/ldap_client.js around lines 85 to 97, the connect retry loop never
observes this._disconnected_state and assumes this.admin_client exists; change
the loop to break/stop retrying when this._disconnected_state is true (either by
including it in the while condition or checking it at the top of each iteration
and returning), and before attempting bind verify this.admin_client is present
(if missing, either initialize it or throw a descriptive error and stop
retrying). Also ensure catch only retries when not disconnected and log the full
error (not just err.message) so failures are visible.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (12)
src/util/ldap_client.js (7)

85-97: Connect retry loop should respect disconnect state

Make the loop exit when disconnecting to avoid endless retries.


35-63: Fix defaults, accept both config key variants, secure TLS, and call is_connected()

  • Default URI is inconsistent: ldap:// with 636. Use ldaps://:636 or ldap://:389 (prefer ldaps).
  • Accept both new and legacy keys: admin/secret and admin_user/admin_password; search_dn and search_base.
  • Default to TLS verification enabled.
  • The is_connected check should call the method.
     const params = JSON.parse(fs.readFileSync(config.LDAP_CONFIG_PATH).toString());
     this.ldap_params = {
-        uri: params.uri || 'ldap://127.0.0.1:636',
-        admin: params.admin_user || 'Administrator',
-        secret: params.admin_password || 'Passw0rd',
-        search_dn: params.search_dn || 'ou=people,dc=example,dc=com',
+        uri: params.uri || 'ldaps://127.0.0.1:636',
+        // Accept both legacy and new keys
+        admin: params.admin ?? params.admin_user ?? 'Administrator',
+        secret: params.secret ?? params.admin_password ?? 'Passw0rd',
+        // Accept both search_base and search_dn
+        search_dn: params.search_dn ?? params.search_base ?? 'ou=people,dc=example,dc=com',
         dn_attribute: params.dn_attribute || 'uid', // for LDAP 'sAMAccountName' for AD
         // search_filter: params.search_filter || '(uid=%s)',
         search_scope: params.search_scope || 'sub',
         jwt_secret: params.jwt_secret,
         ...params,
     };
     this.tls_options = this.ldap_params.tls_options || {
-        'rejectUnauthorized': false,
+        rejectUnauthorized: true,
     };
     this.admin_client = new ldap_client.Client({
         url: this.ldap_params.uri,
         tlsOptions: this.tls_options,
     });
-    if (this.is_connected) {
-        await this.reconnect();
-    }
+    if (this.is_connected()) {
+        await this.reconnect();
+    }

50-52: Defaulting rejectUnauthorized: false weakens TLS security

Please keep certificate verification on by default and allow explicit override via config only.


80-81: Do not log secrets within ldap_params

Avoid logging ldap_params wholesale; it includes admin credentials and jwt_secret.


7-7: Fix incorrect config import path (module will fail to load).

From src/util/ldap_client.js, config is one level up.

-const config = require('../../config');
+const config = require('../config');

77-83: connect(): ensure config loaded and avoid logging secrets

Initialize config/client if missing and log a redacted subset only.

 async connect() {
     this._disconnected_state = false;
     if (this._connect_promise) return this._connect_promise;
-    dbg.log0('connect called, current url', this.ldap_params);
+    if (!this.admin_client) {
+        await this.load_ldap_config();
+    }
+    const { uri, search_dn, search_scope } = this.ldap_params || {};
+    dbg.log0('connect called', { uri, search_dn, search_scope });
     this._connect_promise = this._connect();
     return this._connect_promise;
 }

108-125: authenticate(): prevent LDAP injection, ensure admin connected, and unbind user client

Escape filter values, ensure admin is connected, and always unbind the user client.

 async authenticate(user, password) {
-    /** @type {ldap_client.SearchOptions}*/
-    const search_options = {
-        filter: `(${this.ldap_params.dn_attribute}=${user})`,
-        scope: this.ldap_params.search_scope,
-        attributes: ['dn']
-    };
-    const user_client = new ldap_client.Client({
-        url: this.ldap_params.uri,
-        tlsOptions: this.tls_options,
-    });
-    const { searchEntries } = await this.admin_client.search(this.ldap_params.search_dn, search_options);
-    if (!searchEntries || searchEntries.length === 0) {
-        throw new Error('User not found');
-    }
-    await this._bind(user_client, searchEntries[0].dn, password);
-    return searchEntries[0].dn;
+    // RFC2254-style escaping of filter meta-characters
+    const escape = v => String(v)
+        .replace(/\\/g, '\\5c')
+        .replace(/\*/g, '\\2a')
+        .replace(/\(/g, '\\28')
+        .replace(/\)/g, '\\29')
+        .replace(/\0/g, '\\00')
+        .replace(/\//g, '\\2f');
+    /** @type {ldap_client.SearchOptions}*/
+    const search_options = {
+        filter: `(${this.ldap_params.dn_attribute}=${escape(user)})`,
+        scope: this.ldap_params.search_scope,
+        attributes: ['dn']
+    };
+    if (!this.is_connected()) {
+        await this.connect();
+    }
+    const user_client = new ldap_client.Client({
+        url: this.ldap_params.uri,
+        tlsOptions: this.tls_options,
+    });
+    try {
+        const { searchEntries } = await this.admin_client.search(this.ldap_params.search_dn, search_options);
+        if (!searchEntries || searchEntries.length === 0) {
+            throw new Error('User not found');
+        }
+        await this._bind(user_client, searchEntries[0].dn, password);
+        return searchEntries[0].dn;
+    } finally {
+        try { await user_client.unbind(); } catch (_) { /* ignore */ }
+    }
 }
docs/design/ldap.md (5)

80-80: CLI example: fix typo and read token from file to avoid leaks

Prevent exposure in history/process list; also fix “endpint”.

-aws sts assume-role-with-web-identity --endpoint <endpint> --role-arn [role-arn] --role-session-name [session-name] --web-identity-token eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VyIjoiVGhlVXNlck5hbWUiLCJwYXNzd29yZCI6IlRoZVVzZXJQYXNzd29yZCIsInR5cGUiOiJsZGFwIiwiaWF0IjoxNzU1MTgxOTE3fQ.
+echo '<JWT>' > /tmp/web-identity.jwt
+chmod 600 /tmp/web-identity.jwt
+aws sts assume-role-with-web-identity --endpoint <endpoint> \
+  --role-arn [role-arn] \
+  --role-session-name [session-name] \
+  --web-identity-token file:///tmp/web-identity.jwt

9-13: Do not recommend unsigned/constant-signature JWTs; require signed tokens with standard claims

Accepting unsigned tokens or a “constant signature” is unsafe.

-1. The client sends a signed(with a predfined constant signature) or unsigned JWT as the web identity token.
+1. The client sends a signed JWT (HS256/RS256/ES256) as the web identity token. Unsigned tokens (alg="none") are not allowed.
+   The token MUST include exp (short, e.g., <= 5 minutes), iss, aud (NooBaa STS endpoint), and jti (nonce). The server validates signature and claims.

41-41: Make jwt_secret mandatory and document secure storage

JWTs must be signed; treat jwt_secret as required and advise 0600 permissions.

-jwt_secret (Optional) - The JWT secret the administrator will use to sign the token sent in AssumeRoleWithWebIdentity requests (default: unsigned). Once this option is set - unsigned tokens or tokens signed with different secret will be dropped as access denied.
+jwt_secret (Required) – Secret (for HS256) or private key (for RS/ES) used to sign JWTs for AssumeRoleWithWebIdentity.
+All tokens without a valid signature are rejected. Store this secret securely (file mode 0600, owned by the NooBaa process user).

68-76: Fix JWT examples: remove alg='none', add exp/aud/iss/jti, and avoid real-looking tokens

The current examples are inconsistent and trip secret scanners.

-const jwt = require('jsonwebtoken');
-console.log(jwt.sign({ user: "TheUserName", password: "TheUserPassword", type: "ldap" }, "IAMTHEADMIN123!(SHOULDBE256BITS)"));
-eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VyIjoiVGhlVXNlck5hbWUiLCJwYXNzd29yZCI6IlRoZVVzZXJQYXNzd29yZCIsInR5cGUiOiJsZGFwIiwiaWF0IjoxNzU1MTgxOTE3fQ.
+const jwt = require('jsonwebtoken');
+const payload = {
+  user: "TheUserName",
+  type: "ldap",
+  iss: "noobaa-ldap",
+  aud: "https://<endpoint-host>/sts/",
+  exp: Math.floor(Date.now() / 1000) + 300,
+  jti: "<random-nonce>"
+};
+console.log(jwt.sign(payload, "<HS256-SECRET-256-BIT>", { algorithm: 'HS256' }));
-Or you can use 'none' algorithm if you are not interested with verifying the token
-```js
-const jwt = require('jsonwebtoken');
-console.log(jwt.sign({ user: "TheUserName", password: "TheUserPassword", type: "ldap" }, undefined, { algorithm: 'none' }));
-eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiVGhlVXNlck5hbWUiLCJwYXNzd29yZCI6IlRoZVVzZXJQYXNzd29yZCIsInR5cGUiOiJsZGFwIiwiaWF0IjoxNzU1MTgyMzQ2fQ.P6WYcdM0kJagNK4D0M8AHiGFcUZ-DhTOKHlC1-AxcT0
-```
+// Example output (redacted): eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.<redacted>.<redacted>

60-64: Avoid embedding plaintext passwords in JWT; standardize type casing

Remove password from the JWT payload to reduce leakage risk and use type: "ldap" consistently.

 {
- "user": "TheUserName",
- "password": "TheUserPassword",
- "type": "LDAP"
+ "user": "TheUserName",
+ "type": "ldap"
 }

If transporting credentials is unavoidable, require HTTPS and use JWE (encrypted JWT) with exp/jti. I can provide a jose-based JWE example if desired.

🧹 Nitpick comments (6)
src/endpoint/sts/sts_errors.js (1)

29-37: Add “Type” to STS ErrorResponse and verify consumers

In src/endpoint/sts/sts_errors.js (lines 29–37), inject the same Type field used by IAM/S3:

         const xml = {
             ErrorResponse: {
                 Error: {
+                    Type: this.http_code >= 500 ? 'Receiver' : 'Sender',
                     Code: this.code,
                     Message: this.message,
                     Resource: resource || '',
                     Detail: this.detail,
                 },
                 RequestId: request_id || '',
             }
         };

Please also:

  • Review any STS XML parsers or client code (including tests under test/ or integration suites) to ensure they handle the nested ErrorResponse with the new Type field.
  • Compare with src/endpoint/iam/iam_errors.js and src/endpoint/s3/s3_errors.js for consistent structure.
src/endpoint/sts/ops/sts_post_assume_role.js (1)

19-26: Map get_assumed_role errors; add server-side logging of root cause

Mapping ACCESS_DENIED is good. Log the original error before returning a generic STS error to aid debugging without exposing details to clients.

     try {
         assumed_role = await req.sts_sdk.get_assumed_role(req);
     } catch (err) {
+        dbg.error('AssumeRole: get_assumed_role failed', err);
         if (err.code === 'ACCESS_DENIED') {
             throw new StsError(StsError.AccessDeniedException);
         }
         throw new StsError(StsError.InternalFailure);
     }
src/util/ldap_client.js (3)

4-11: Use local util imports consistently

Within src/util, prefer local requires to avoid brittle ../util/… paths.

-require('../util/fips');
+require('./fips');
...
-const P = require('../util/promise');
+const P = require('./promise');

27-33: Constructor: don’t fire-and-forget async; capture errors

You can’t await in a constructor, but at least attach a catch to avoid unhandled rejections.

 constructor() {
     super();
-    this.load_ldap_config();
+    void this.load_ldap_config().catch(err => dbg.warn('load_ldap_config failed during init', err));
     fs.watchFile(config.LDAP_CONFIG_PATH, {
         interval: config.NC_RELOAD_CONFIG_INTERVAL
-    }, () => this.load_ldap_config()).unref();
+    }, () => { void this.load_ldap_config().catch(err => dbg.warn('load_ldap_config failed on change', err)); }).unref();
 }

85-97: _connect(): make retry loop cancellable and robust

Observe _disconnected_state, validate admin_client presence, and avoid tight retrying after disconnect.

 async _connect() {
     let is_connected = false;
-    while (!is_connected) {
+    while (!is_connected && !this._disconnected_state) {
         try {
-            await this._bind(this.admin_client, this.ldap_params.admin, this.ldap_params.secret);
+            if (!this.admin_client) throw new Error('LDAP admin client not initialized');
+            await this._bind(this.admin_client, this.ldap_params.admin, this.ldap_params.secret);
             dbg.log0('_connect: initial connect succeeded');
             is_connected = true;
         } catch (err) {
-            dbg.error('_connect: initial connect failed, will retry', err.message);
-            await P.delay(3000);
+            dbg.error('_connect: initial connect failed, will retry', err);
+            if (this._disconnected_state) break;
+            await P.delay(3000);
         }
     }
 }
docs/design/ldap.md (1)

90-97: Markdown lint: avoid bare URLs

Minor doc polish to satisfy linters.

-read more here: https://docs.aws.amazon.com/cli/latest/reference/sts/assume-role-with-web-identity.html
+Read more here: <https://docs.aws.amazon.com/cli/latest/reference/sts/assume-role-with-web-identity.html>
...
- (https://github.com/samba-in-kubernetes/samba-container)
+ (<https://github.com/samba-in-kubernetes/samba-container>)
...
- (https://github.com/ldapjs/docker-test-openldap/pkgs/container/docker-test-openldap%2Fopenldap)
+ (<https://github.com/ldapjs/docker-test-openldap/pkgs/container/docker-test-openldap%2Fopenldap>)
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 9a7a0bf and 4d6a8d8.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (12)
  • config.js (1 hunks)
  • docs/design/ldap.md (1 hunks)
  • package.json (1 hunks)
  • src/endpoint/endpoint.js (5 hunks)
  • src/endpoint/sts/ops/sts_post_assume_role.js (2 hunks)
  • src/endpoint/sts/ops/sts_post_assume_role_with_web_identity.js (1 hunks)
  • src/endpoint/sts/sts_errors.js (2 hunks)
  • src/endpoint/sts/sts_rest.js (1 hunks)
  • src/endpoint/sts/sts_utils.js (1 hunks)
  • src/sdk/sts_sdk.js (5 hunks)
  • src/test/integration_tests/api/sts/test_sts.js (3 hunks)
  • src/util/ldap_client.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (8)
  • config.js
  • src/endpoint/sts/sts_rest.js
  • src/endpoint/sts/sts_utils.js
  • package.json
  • src/endpoint/endpoint.js
  • src/endpoint/sts/ops/sts_post_assume_role_with_web_identity.js
  • src/sdk/sts_sdk.js
  • src/test/integration_tests/api/sts/test_sts.js
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-06-17T12:59:51.543Z
Learnt from: naveenpaul1
PR: noobaa/noobaa-core#9042
File: src/util/cloud_utils.js:183-194
Timestamp: 2025-06-17T12:59:51.543Z
Learning: In the set_noobaa_s3_connection function in src/util/cloud_utils.js, internal NooBaa S3 endpoints (filtered by 'api': 's3', 'kind': 'INTERNAL') will always be non-secure, so tls: false is the correct setting despite the conditional https:// scheme logic.

Applied to files:

  • src/util/ldap_client.js
🧬 Code Graph Analysis (2)
src/endpoint/sts/sts_errors.js (1)
src/endpoint/sts/sts_rest.js (1)
  • StsError (6-6)
src/util/ldap_client.js (2)
src/sdk/sts_sdk.js (4)
  • require (6-6)
  • require (8-8)
  • ldap_client (11-11)
  • dbg (5-5)
src/endpoint/sts/sts_rest.js (1)
  • dbg (5-5)
🪛 LanguageTool
docs/design/ldap.md

[grammar] ~9-~9: Ensure spelling is correct
Context: ...ow: 1. The client sends a signed(with a predfined constant signature) or unsigned JWT as ...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~9-~9: There might be a mistake here.
Context: ... unsigned JWT as the web identity token. 2. The system parses the JWT to extract the...

(QB_NEW_EN)


[grammar] ~16-~16: There might be a mistake here.
Context: ...users. ## Configuring the external LDAP The administrator must store the LDAP co...

(QB_NEW_EN)


[grammar] ~17-~17: There might be a mistake here.
Context: ...DAP configuration in the following file: /etc/noobaa-server/ldap_config The conf...

(QB_NEW_EN)


[grammar] ~41-~41: There might be a mistake here.
Context: ... set - unsigned tokens or tokens signed with different secret will be dropped as acc...

(QB_NEW_EN)


[grammar] ~65-~65: There might be a mistake here.
Context: ...Password", "type": "LDAP" } Sign it node.js example:js const jwt = require('...

(QB_NEW_EN)


[grammar] ~95-~95: There might be a mistake here.
Context: ... can base on dockers I used for testing: * AD image: `docker run --rm --privileged ...

(QB_NEW_EN)


[grammar] ~96-~96: There might be a mistake here.
Context: ...com/samba-in-kubernetes/samba-container) * LDAP image: `docker run --rm --privilege...

(QB_NEW_EN)


[grammar] ~97-~97: There might be a mistake here.
Context: ...ntainer/docker-test-openldap%2Fopenldap) 3. Add support to the operator side: * CLI...

(QB_NEW_EN)


[grammar] ~98-~98: There might be a mistake here.
Context: ...ap) 3. Add support to the operator side: * CLI command for configuring external LDA...

(QB_NEW_EN)


[grammar] ~99-~99: There might be a mistake here.
Context: ...LI command for configuring external LDAP * Create K8s Secret for LDAP info and moun...

(QB_NEW_EN)


[grammar] ~100-~100: There might be a mistake here.
Context: ...-server/ldap_config to the relevant pods 4. Better align and adapt to the IAM effort...

(QB_NEW_EN)


[grammar] ~101-~101: There might be a mistake here.
Context: ...dapt to the IAM effort also in POC stage 5. See if we want to support encrypted pass...

(QB_NEW_EN)


[grammar] ~105-~105: There might be a mistake here.
Context: ...ation scope if possible ## Appendix A: Creating account w/ role config using NooBaa API...

(QB_NEW_EN)


[style] ~131-~131: Consider a more concise word here.
Context: ...auth_token": "'$(cat .nbtoken)'" }' in order to assume this role:bash aws sts assum...

(IN_ORDER_TO_PREMIUM)

🪛 Gitleaks (8.27.2)
docs/design/ldap.md

70-70: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.

(jwt)


76-76: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.

(jwt)


80-80: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.

(jwt)


134-134: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.

(jwt)


139-139: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.

(jwt)


138-138: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🪛 markdownlint-cli2 (0.17.2)
docs/design/ldap.md

90-90: Bare URL used

(MD034, no-bare-urls)


94-94: Bare URL used

(MD034, no-bare-urls)


96-96: Bare URL used

(MD034, no-bare-urls)


97-97: Bare URL used

(MD034, no-bare-urls)


102-102: Bare URL used

(MD034, no-bare-urls)


105-105: Trailing punctuation in heading
Punctuation: ':'

(MD026, no-trailing-punctuation)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: run-package-lock-validation
  • GitHub Check: Build Noobaa Image
  • GitHub Check: run-jest-unit-tests
🔇 Additional comments (3)
src/endpoint/sts/sts_errors.js (1)

144-148: New InvalidIdentityToken constant — LGTM

Good addition for web-identity flows; HTTP 400 is aligned with AWS behavior.

src/endpoint/sts/ops/sts_post_assume_role.js (2)

15-17: Centralized duration parsing — LGTM

Using sts_utils.parse_sts_duration keeps the logic consistent across STS ops.


43-47: SessionToken via sts_utils — LGTM

Token generation delegated to the shared util is the right move for consistency.

Comment on lines +14 to +19
async disconnect() {
dbg.log0('ldap client disconnect called');
this._disconnected_state = true;
this._connect_promise = null;
this.admin_client.unbind();
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

disconnect(): null-safety and await unbind

Guard against missing client and await unbind to avoid dangling sockets.

 async disconnect() {
     dbg.log0('ldap client disconnect called');
     this._disconnected_state = true;
     this._connect_promise = null;
-    this.admin_client.unbind();
+    if (this.admin_client) {
+        try {
+            await this.admin_client.unbind();
+        } catch (_) {
+            // ignore
+        }
+    }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async disconnect() {
dbg.log0('ldap client disconnect called');
this._disconnected_state = true;
this._connect_promise = null;
this.admin_client.unbind();
}
async disconnect() {
dbg.log0('ldap client disconnect called');
this._disconnected_state = true;
this._connect_promise = null;
if (this.admin_client) {
try {
await this.admin_client.unbind();
} catch (_) {
// ignore
}
}
}
🤖 Prompt for AI Agents
In src/util/ldap_client.js around lines 14 to 19, disconnect() currently assumes
this.admin_client exists and calls unbind synchronously; update it to check that
this.admin_client is non-null/defined before calling unbind, call the async
unbind method with await (or return its promise) to ensure sockets are closed,
reset _disconnected_state and _connect_promise after successful unbind (or in
finally), and handle/rethrow any unbind errors (e.g., try/catch and log) to
avoid leaving dangling sockets.

Comment on lines +21 to +25
async reconnect() {
dbg.log0(`reconnect called`);
this.disconnect();
return this.connect();
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

reconnect(): await disconnect before connecting

Avoid racing a new connect with an in-flight disconnect.

 async reconnect() {
     dbg.log0(`reconnect called`);
-    this.disconnect();
-    return this.connect();
+    await this.disconnect();
+    return this.connect();
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async reconnect() {
dbg.log0(`reconnect called`);
this.disconnect();
return this.connect();
}
async reconnect() {
dbg.log0(`reconnect called`);
await this.disconnect();
return this.connect();
}
🤖 Prompt for AI Agents
In src/util/ldap_client.js around lines 21 to 25, reconnect currently calls
this.disconnect() without awaiting it which can race a new connect with an
in-flight disconnect; change reconnect to await this.disconnect() before calling
and returning this.connect() so disconnect finishes first, ensuring disconnect
returns a Promise (or make it async) and propagate/rethrow any errors as
appropriate.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (15)
package.json (1)

103-103: New ldapts runtime dependency: validate necessity, footprint, and maintenance tradeoffs

Adding ldapts looks reasonable for a POC. Before merging to master, evaluate:

  • Is ldapts the long-term choice vs. native OpenLDAP C client (perf/stability)?
  • Runtime footprint and transitive deps implications for packaging (pkg) and minimal images.

Do you want me to draft a short RFC comparing ldapts vs. OpenLDAP C client (API surface, TLS options, reconnect semantics, perf, bundle size)?

src/endpoint/endpoint.js (1)

253-255: Non-blocking LDAP connect lacks error handling; attach a catch to avoid unhandled rejections

You correctly avoided awaiting the connect. However, if connect() rejects, it will surface as an unhandled rejection. Log-and-continue to keep startup resilient.

Apply this diff:

-        if (await ldap_client.is_ldap_configured()) {
-            ldap_client.instance().connect();
-        }
+        if (await ldap_client.is_ldap_configured()) {
+            // Start LDAP connect in background; don't block startup. Log failures.
+            ldap_client.instance().connect()
+                .catch(e => dbg.warn(
+                    'LDAP configured but failed to connect; continuing without LDAP',
+                    e?.message || e
+                ));
+        }
docs/design/ldap.md (6)

9-13: Do not allow unsigned or “constant signature” JWTs; require signed tokens with standard claims

Allowing unsigned tokens or a “constant signature” is a critical security risk and enables trivial impersonation/replay. Require signed JWTs and validate iss/aud/exp (short), and jti.

Apply:

-1. The client sends a signed(with a predfined constant signature) or unsigned JWT as the web identity token.
+1. The client sends a signed JWT (HS256/RS256/ES256) as the web identity token. Unsigned tokens (alg="none") are not allowed.
+   The token MUST include standard claims (iss, aud, exp<=5m) and jti (nonce). The server validates signature and all claims.

41-41: Make jwt_secret mandatory; never default to unsigned tokens

jwt_secret must be required (or a public key when using asymmetric algs). Reject all tokens without a valid signature. Document secure storage/permissions.

-jwt_secret (Optional) - The JWT secret the administrator will use to sign the token sent in AssumeRoleWithWebIdentity requests (default: unsigned). Once this option is set - unsigned tokens or tokens signed with different secret will be dropped as access denied.
+jwt_secret (Required) – Secret (for HS256) or private key (for RS/ES) used to sign JWTs for AssumeRoleWithWebIdentity.
+All tokens without a valid signature are rejected. Store the secret/key securely (file mode 0600, owned by the NooBaa user/service).

60-64: Do not embed plaintext passwords in JWT payloads

Placing a user’s password in a bearer token risks leakage via logs, proxies, and browser/shell history. Perform LDAP bind using credentials delivered over TLS at request time, not re-encoded into a token. If you must transport, use JWE and short exp+jti.

-{
- "user": "TheUserName",
- "password": "TheUserPassword",
- "type": "ldap"
-}
+{
+  "user": "TheUserName",
+  "type": "ldap",
+  "iss": "noobaa-ldap",
+  "aud": "https://<endpoint-host>/sts/",
+  "exp": <now+300s>,
+  "jti": "<random-uuid>"
+}

Would you like me to provide a JWE-based example using jose and server-side decryption?


68-76: Remove 'alg: none' example; show a single signed example with standard claims

The current examples are inconsistent and encourage insecure practices.

-const jwt = require('jsonwebtoken');
-console.log(jwt.sign({ user: "TheUserName", password: "TheUserPassword", type: "ldap" }, "IAMTHEADMIN123!(SHOULDBE256BITS)"));
-eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VyIjoiVGhlVXNlck5hbWUiLCJwYXNzd29yZCI6IlRoZVVzZXJQYXNzd29yZCIsInR5cGUiOiJsZGFwIiwiaWF0IjoxNzU1MTgxOTE3fQ.
-...
-Or you can use 'none' algorithm if you are not interested with verifying the token
-```js
-const jwt = require('jsonwebtoken');
-console.log(jwt.sign({ user: "TheUserName", password: "TheUserPassword", type: "ldap" }, undefined, { algorithm: 'none' }));
-...
-```
+const jwt = require('jsonwebtoken');
+const payload = {
+  user: "TheUserName",
+  type: "ldap",
+  iss: "noobaa-ldap",
+  aud: "https://<endpoint-host>/sts/",
+  exp: Math.floor(Date.now()/1000) + 300,
+  jti: "<random-uuid>"
+};
+console.log(jwt.sign(payload, "IAMTHEADMIN123!(USE_256_BITS_MIN)", { algorithm: 'HS256' }));

80-81: Avoid passing the token inline; use file:// to prevent leaks via shell history/process list

Also fix the “endpoint” typo.

-aws sts assume-role-with-web-identity --endpoint [endpoint] --role-arn [role-arn] --role-session-name [session-name] --web-identity-token eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VyIjoiVGhlVXNlck5hbWUiLCJwYXNzd29yZCI6IlRoZVVzZXJQYXNzd29yZCIsInR5cGUiOiJsZGFwIiwiaWF0IjoxNzU1MTgxOTE3fQ.
+echo '<jwt>' > /tmp/web-identity.jwt
+chmod 600 /tmp/web-identity.jwt
+aws sts assume-role-with-web-identity \
+  --endpoint <endpoint> \
+  --role-arn <ROLE_ARN> \
+  --role-session-name <SESSION_NAME> \
+  --web-identity-token file:///tmp/web-identity.jwt

134-141: Redact secrets/tokens in examples to avoid tripping secret scanners

Replace real-looking keys/JWTs with placeholders.

-    "AccessKeyId": "vTFcHUuPqjKdtzTsMULP",
-    "SecretAccessKey": "qBQxFcx99JOlABkZNq2fNRj6qGe18F9IREcHf9DZ",
-    "SessionToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9....",
-    "Expiration": "2025-08-17T14:03:18+00:00"
+    "AccessKeyId": "<ACCESS_KEY_ID>",
+    "SecretAccessKey": "<SECRET_ACCESS_KEY>",
+    "SessionToken": "<SESSION_TOKEN>",
+    "Expiration": "<EXPIRATION_ISO8601>"
src/sdk/sts_sdk.js (2)

71-73: Avoid logging full account objects; redact to non-sensitive identifiers

Logging account can leak secrets. Log only access_key and role_name.

-        dbg.log0('sts_sdk.get_assumed_role res', account,
-            'account.role_config: ', account.role_config);
+        dbg.log0('sts_sdk.get_assumed_role res', {
+            access_key,
+            role_name: account.role_config?.role_name
+        });

128-130: Avoid logging full account in web identity path

Same concern as above; log only safe identifiers.

-        dbg.log0('sts_sdk.get_assumed_role_with_web_identity res', account,
-            'account.role_config: ', account.role_config);
+        dbg.log0('sts_sdk.get_assumed_role_with_web_identity res', {
+            access_key: req.body.role_arn.split(':')[4],
+            role_name: account.role_config?.role_name
+        });
src/util/ldap_client.js (5)

21-25: reconnect(): await disconnect before connecting

Avoid racing a new connect with an in-flight disconnect.

 async reconnect() {
     dbg.log0(`reconnect called`);
-    this.disconnect();
-    return this.connect();
+    await this.disconnect();
+    return this.connect();
 }

84-96: Make retry loop cancellable and guard uninitialized client

Observe _disconnected_state and protect against missing admin_client.

 async _connect() {
     let is_connected = false;
-    while (!is_connected) {
+    while (!is_connected && !this._disconnected_state) {
         try {
-            await this._bind(this.admin_client, this.ldap_params.admin, this.ldap_params.secret);
+            if (!this.admin_client) throw new Error('LDAP admin client not initialized');
+            await this._bind(this.admin_client, this.ldap_params.admin, this.ldap_params.secret);
             dbg.log0('_connect: initial connect succeeded');
             is_connected = true;
         } catch (err) {
-            dbg.error('_connect: initial connect failed, will retry', err.message);
-            await P.delay(3000);
+            dbg.error('_connect: initial connect failed, will retry', err?.message || err);
+            if (this._disconnected_state) break;
+            await P.delay(3000);
         }
     }
 }

107-124: authenticate(): prevent LDAP injection, ensure admin is connected, and always unbind user client

Escape filter values, ensure connection, and release the user client in finally.

 async authenticate(user, password) {
-    /** @type {ldap_client.SearchOptions}*/
-    const search_options = {
-        filter: `(${this.ldap_params.dn_attribute}=${user})`,
-        scope: this.ldap_params.search_scope,
-        attributes: ['dn']
-    };
-    const user_client = new ldap_client.Client({
-        url: this.ldap_params.uri,
-        tlsOptions: this.tls_options,
-    });
-    const { searchEntries } = await this.admin_client.search(this.ldap_params.search_dn, search_options);
-    if (!searchEntries || searchEntries.length === 0) {
-        throw new Error('User not found');
-    }
-    await this._bind(user_client, searchEntries[0].dn, password);
-    return searchEntries[0].dn;
+    const escape = v => String(v)
+        .replace(/\\/g, '\\5c')
+        .replace(/\*/g, '\\2a')
+        .replace(/\(/g, '\\28')
+        .replace(/\)/g, '\\29')
+        .replace(/\0/g, '\\00')
+        .replace(/\//g, '\\2f');
+    /** @type {ldap_client.SearchOptions}*/
+    const search_options = {
+        filter: `(${this.ldap_params.dn_attribute}=${escape(user)})`,
+        scope: this.ldap_params.search_scope,
+        attributes: ['dn']
+    };
+    if (!this.is_connected()) {
+        await this.connect();
+    }
+    const user_client = new ldap_client.Client({
+        url: this.ldap_params.uri,
+        tlsOptions: this.tls_options,
+    });
+    try {
+        const { searchEntries } = await this.admin_client.search(this.ldap_params.search_dn, search_options);
+        if (!searchEntries || searchEntries.length === 0) {
+            throw new Error('User not found');
+        }
+        await this._bind(user_client, searchEntries[0].dn, password);
+        return searchEntries[0].dn;
+    } finally {
+        try { await user_client.unbind(); } catch (_) { /* ignore */ }
+    }
 }

14-19: disconnect(): null-safety and await unbind to avoid dangling sockets

Guard against missing client and await unbind.

 async disconnect() {
     dbg.log0('ldap client disconnect called');
     this._disconnected_state = true;
     this._connect_promise = null;
-    this.admin_client.unbind();
+    if (this.admin_client) {
+        try {
+            await this.admin_client.unbind();
+        } catch (_) {
+            // ignore
+        }
+    }
 }

35-63: Config load: align keys, secure TLS defaults, and fix is_connected() call

  • Accept both admin/secret and admin_user/admin_password; search_base alias for search_dn.
  • Default rejectUnauthorized to true.
  • Call the method is_connected() (not the property) before reconnect.
             const params = JSON.parse(fs.readFileSync(config.LDAP_CONFIG_PATH).toString());
             this.ldap_params = {
-                uri: params.uri || 'ldaps://127.0.0.1:636',
-                admin: params.admin_user || 'Administrator',
-                secret: params.admin_password || 'Passw0rd',
-                search_dn: params.search_dn || 'ou=people,dc=example,dc=com',
+                uri: params.uri || 'ldaps://127.0.0.1:636',
+                // accept both legacy and new keys
+                admin: (params.admin ?? params.admin_user) || 'Administrator',
+                secret: (params.secret ?? params.admin_password) || 'Passw0rd',
+                // accept both search_dn and search_base
+                search_dn: (params.search_dn ?? params.search_base) || 'ou=people,dc=example,dc=com',
                 dn_attribute: params.dn_attribute || 'uid', // for LDAP 'sAMAccountName' for AD
                 search_scope: params.search_scope || 'sub',
                 jwt_secret: params.jwt_secret,
                 ...params,
             };
             this.tls_options = this.ldap_params.tls_options || {
-                'rejectUnauthorized': false,
+                rejectUnauthorized: true,
             };
             this.admin_client = new ldap_client.Client({
                 url: this.ldap_params.uri,
                 tlsOptions: this.tls_options,
             });
-            if (this.is_connected) {
-                await this.reconnect();
-            }
+            if (this.is_connected && typeof this.is_connected === 'function' && this.is_connected()) {
+                await this.reconnect();
+            }
🧹 Nitpick comments (5)
src/endpoint/endpoint.js (1)

253-255: Consider lazy-connect to avoid mass connection storms from all forks

Every fork calls connect() at startup. Prefer connecting on first LDAP-authenticated request (ldap_client can connect-on-demand) or gate the eager connect to a single worker/primary to reduce load on LDAP.

I can wire a connect-on-demand in ldap_client.instance().authenticate() and remove the eager connect here – want me to?

docs/design/ldap.md (1)

9-9: Nit: fix typos and minor grammar

“predfined” → “predefined”; add space “signed (with a predefined…)”; a few other small grammar nits flagged by linter.

I can push a quick cleanup pass across the doc if helpful.

src/sdk/sts_sdk.js (2)

56-60: Role ARN parsing lacks validation; guard malformed inputs

Split-by-colon/slice may throw or yield wrong indices on malformed role_arn. Prefer a strict pattern and error out clearly.

Example guard:

-        const role_name_idx = role_arn.lastIndexOf('/') + 1;
-        const role_name = role_arn.slice(role_name_idx);
-        const access_key = role_arn.split(':')[4];
+        const m = /^arn:aws:sts::([^:]+):role\/(.+)$/.exec(role_arn || '');
+        if (!m) throw new RpcError('INVALID_ARGUMENT', 'Invalid role_arn format');
+        const [ , access_key, role_name ] = m;

117-119: Attempt lazy LDAP connect before denying; current check may spuriously fail

If LDAP is configured but not yet connected, try connecting once before throwing ACCESS_DENIED.

-        if (!(await ldap_client.is_ldap_configured()) || !ldap_client.instance().is_connected()) {
-            throw new RpcError('ACCESS_DENIED', 'LDAP is not configured or not connected');
-        }
+        if (!(await ldap_client.is_ldap_configured())) {
+            throw new RpcError('ACCESS_DENIED', 'LDAP is not configured');
+        }
+        if (!ldap_client.instance().is_connected()) {
+            try {
+                await ldap_client.instance().connect();
+            } catch (e) {
+                throw new RpcError('ACCESS_DENIED', 'LDAP is not connected');
+            }
+        }
src/util/ldap_client.js (1)

76-82: connect(): ensure config is loaded and avoid logging secrets

Load config if admin_client is missing; keep logs non-sensitive.

 async connect() {
     this._disconnected_state = false;
     if (this._connect_promise) return this._connect_promise;
-    dbg.log0('connect called, current url:', this.ldap_params.uri);
+    if (!this.admin_client) {
+        await this.load_ldap_config();
+    }
+    dbg.log0('connect called', { uri: this.ldap_params?.uri });
     this._connect_promise = this._connect();
     return this._connect_promise;
 }
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 4d6a8d8 and 92a710e.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (12)
  • config.js (1 hunks)
  • docs/design/ldap.md (1 hunks)
  • package.json (1 hunks)
  • src/endpoint/endpoint.js (5 hunks)
  • src/endpoint/sts/ops/sts_post_assume_role.js (2 hunks)
  • src/endpoint/sts/ops/sts_post_assume_role_with_web_identity.js (1 hunks)
  • src/endpoint/sts/sts_errors.js (2 hunks)
  • src/endpoint/sts/sts_rest.js (1 hunks)
  • src/endpoint/sts/sts_utils.js (1 hunks)
  • src/sdk/sts_sdk.js (5 hunks)
  • src/test/integration_tests/api/sts/test_sts.js (3 hunks)
  • src/util/ldap_client.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (7)
  • config.js
  • src/endpoint/sts/sts_errors.js
  • src/endpoint/sts/sts_utils.js
  • src/endpoint/sts/sts_rest.js
  • src/endpoint/sts/ops/sts_post_assume_role_with_web_identity.js
  • src/test/integration_tests/api/sts/test_sts.js
  • src/endpoint/sts/ops/sts_post_assume_role.js
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-06-17T12:59:51.543Z
Learnt from: naveenpaul1
PR: noobaa/noobaa-core#9042
File: src/util/cloud_utils.js:183-194
Timestamp: 2025-06-17T12:59:51.543Z
Learning: In the set_noobaa_s3_connection function in src/util/cloud_utils.js, internal NooBaa S3 endpoints (filtered by 'api': 's3', 'kind': 'INTERNAL') will always be non-secure, so tls: false is the correct setting despite the conditional https:// scheme logic.

Applied to files:

  • src/util/ldap_client.js
🧬 Code Graph Analysis (3)
src/util/ldap_client.js (2)
src/endpoint/endpoint.js (6)
  • require (39-39)
  • require (40-40)
  • require (42-42)
  • require (43-43)
  • require (45-45)
  • ldap_client (44-44)
src/sdk/sts_sdk.js (3)
  • require (6-6)
  • require (8-8)
  • ldap_client (11-11)
src/sdk/sts_sdk.js (3)
src/test/integration_tests/api/sts/test_sts.js (4)
  • jwt (11-11)
  • ldap_client (18-18)
  • account (103-103)
  • req (385-385)
src/sdk/object_sdk.js (1)
  • account_cache (50-63)
src/util/ldap_client.js (1)
  • ldap_client (9-9)
src/endpoint/endpoint.js (3)
src/sdk/sts_sdk.js (1)
  • ldap_client (11-11)
src/util/ldap_client.js (1)
  • ldap_client (9-9)
src/sdk/endpoint_stats_collector.js (1)
  • cluster (12-15)
🪛 LanguageTool
docs/design/ldap.md

[grammar] ~9-~9: Ensure spelling is correct
Context: ...ow: 1. The client sends a signed(with a predfined constant signature) or unsigned JWT as ...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~9-~9: There might be a mistake here.
Context: ... unsigned JWT as the web identity token. 2. The system parses the JWT to extract the...

(QB_NEW_EN)


[grammar] ~16-~16: There might be a mistake here.
Context: ...users. ## Configuring the external LDAP The administrator must store the LDAP co...

(QB_NEW_EN)


[grammar] ~17-~17: There might be a mistake here.
Context: ...DAP configuration in the following file: /etc/noobaa-server/ldap_config The conf...

(QB_NEW_EN)


[grammar] ~41-~41: There might be a mistake here.
Context: ... set - unsigned tokens or tokens signed with different secret will be dropped as acc...

(QB_NEW_EN)


[grammar] ~65-~65: There might be a mistake here.
Context: ...Password", "type": "ldap" } Sign it node.js example:js const jwt = require('...

(QB_NEW_EN)


[grammar] ~95-~95: There might be a mistake here.
Context: ... can base on dockers I used for testing: * AD image: `docker run --rm --privileged ...

(QB_NEW_EN)


[grammar] ~96-~96: There might be a mistake here.
Context: ...com/samba-in-kubernetes/samba-container) * LDAP image: `docker run --rm --privilege...

(QB_NEW_EN)


[grammar] ~97-~97: There might be a mistake here.
Context: ...ntainer/docker-test-openldap%2Fopenldap) 3. Add support to the operator side: * CLI...

(QB_NEW_EN)


[grammar] ~98-~98: There might be a mistake here.
Context: ...ap) 3. Add support to the operator side: * CLI command for configuring external LDA...

(QB_NEW_EN)


[grammar] ~99-~99: There might be a mistake here.
Context: ...LI command for configuring external LDAP * Create K8s Secret for LDAP info and moun...

(QB_NEW_EN)


[grammar] ~100-~100: There might be a mistake here.
Context: ...-server/ldap_config to the relevant pods 4. Better align and adapt to the IAM effort...

(QB_NEW_EN)


[grammar] ~101-~101: There might be a mistake here.
Context: ...dapt to the IAM effort also in POC stage 5. See if we want to support encrypted pass...

(QB_NEW_EN)


[grammar] ~105-~105: There might be a mistake here.
Context: ...ation scope if possible ## Appendix A: Creating account w/ role config using NooBaa API...

(QB_NEW_EN)


[style] ~131-~131: Consider a more concise word here.
Context: ...auth_token": "'$(cat .nbtoken)'" }' in order to assume this role:bash aws sts assum...

(IN_ORDER_TO_PREMIUM)

🪛 Gitleaks (8.27.2)
docs/design/ldap.md

138-138: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


70-70: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.

(jwt)


76-76: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.

(jwt)


80-80: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.

(jwt)


134-134: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.

(jwt)


139-139: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.

(jwt)

🪛 markdownlint-cli2 (0.17.2)
docs/design/ldap.md

90-90: Bare URL used

(MD034, no-bare-urls)


94-94: Bare URL used

(MD034, no-bare-urls)


96-96: Bare URL used

(MD034, no-bare-urls)


97-97: Bare URL used

(MD034, no-bare-urls)


102-102: Bare URL used

(MD034, no-bare-urls)


105-105: Trailing punctuation in heading
Punctuation: ':'

(MD026, no-trailing-punctuation)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: run-package-lock-validation
  • GitHub Check: Build Noobaa Image
  • GitHub Check: run-jest-unit-tests
🔇 Additional comments (1)
src/sdk/sts_sdk.js (1)

149-151: Allowing anonymous for AssumeRoleWithWebIdentity is acceptable (by design)

STS’s web-identity entrypoint being anonymous matches AWS semantics. Good.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (14)
docs/design/ldap.md (6)

9-13: Do not allow unsigned JWTs; require signatures and validate standard claims

Accepting unsigned tokens or “constant signature” is a critical auth bypass. Enforce signed tokens and validate iss/aud/exp/jti.

Apply:

-1. The client sends a signed(with a predfined constant signature) or unsigned JWT as the web identity token.
-2. The system parses the JWT to extract the LDAP username and password.
+1. The client sends a signed JWT as the web identity token. Unsigned tokens (alg="none") are rejected.
+   Accepted algorithms: HS256 (shared secret) or RS/ES (public key). The token MUST include: iss, aud, exp (<= 5 minutes), and jti (nonce).
+2. The system validates the JWT signature and claims (iss, aud, exp, jti), and extracts the user identity.

41-53: Make jwt_secret mandatory; document secure storage and permissions

JWTs must be signed. Treating jwt_secret as optional is unsafe.

Apply:

-jwt_secret (Optional) - The JWT secret the administrator will use to sign the token sent in AssumeRoleWithWebIdentity requests (default: unsigned). Once this option is set - unsigned tokens or tokens signed with different secret will be dropped as access denied.
+jwt_secret (Required) – Secret (for HS256) or public/private key (for RS/ES) used to verify/sign JWTs for AssumeRoleWithWebIdentity.
+Unsigned tokens are rejected. Store this secret/key securely (file mode 0600, owned by the noobaa service user; consider a secrets manager).

Also update the example to indicate ldaps:

-  "uri": "ldap://ldap.example.com:636",
+  "uri": "ldaps://ldap.example.com:636",

68-76: Fix JWT examples and remove alg='none'

The displayed token/header contradicts the signing call and recommending alg='none' is insecure. Show a single HS256 example with standard claims; remove the 'none' example.

Apply:

-const jwt = require('jsonwebtoken');
-console.log(jwt.sign({ user: "TheUserName", password: "TheUserPassword", type: "ldap" }, "IAMTHEADMIN123!(SHOULDBE256BITS)"));
-eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VyIjoiVGhlVXNlck5hbWUiLCJwYXNzd29yZCI6IlRoZVVzZXJQYXNzd29yZCIsInR5cGUiOiJsZGFwIiwiaWF0IjoxNzU1MTgxOTE3fQ.
-```
-Or you can use 'none' algorithm if you are not interested with verifying the token
-```js
-const jwt = require('jsonwebtoken');
-console.log(jwt.sign({ user: "TheUserName", password: "TheUserPassword", type: "ldap" }, undefined, { algorithm: 'none' }));
-eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiVGhlVXNlck5hbWUiLCJwYXNzd29yZCI6IlRoZVVzZXJQYXNzd29yZCIsInR5cGUiOiJsZGFwIiwiaWF0IjoxNzU1MTgyMzQ2fQ.P6WYcdM0kJagNK4D0M8AHiGFcUZ-DhTOKHlC1-AxcT0
+const jwt = require('jsonwebtoken');
+const payload = {
+  user: "TheUserName",
+  type: "ldap",
+  iss: "noobaa-ldap",
+  aud: "https://<endpoint-host>/sts/",
+  exp: Math.floor(Date.now()/1000) + 300,
+  jti: "<random-uuid>"
+};
+console.log(jwt.sign(payload, "<256-bit-secret>", { algorithm: 'HS256' }));

80-81: CLI: avoid leaking tokens; use file://

Inline tokens end up in shell history and process listings. Supply the token via file path.

Apply:

-aws sts assume-role-with-web-identity --endpoint [endpoint] --role-arn [role-arn] --role-session-name [session-name] --web-identity-token eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VyIjoiVGhlVXNlck5hbWUiLCJwYXNzd29yZCI6IlRoZVVzZXJQYXNzd29yZCIsInR5cGUiOiJsZGFwIiwiaWF0IjoxNzU1MTgxOTE3fQ.
+echo '<jwt>' > /tmp/web-identity.jwt
+chmod 600 /tmp/web-identity.jwt
+aws sts assume-role-with-web-identity \
+  --endpoint <endpoint> \
+  --role-arn <ROLE_ARN> \
+  --role-session-name <SESSION_NAME> \
+  --web-identity-token file:///tmp/web-identity.jwt

56-64: Avoid embedding plaintext passwords in bearer tokens

Placing the LDAP password in a JWT leaks credentials via logs, proxies, and browser/process lists. Prefer collecting the password over TLS at request time and binding server-side without re-encoding the password.

Apply:

-{
- "user": "TheUserName",
- "password": "TheUserPassword",
- "type": "ldap"
-}
+{
+ "user": "TheUserName",
+ "type": "ldap",
+ "iss": "noobaa-ldap",
+ "aud": "https://<endpoint-host>/sts/",
+ "exp": <now + 300s>,
+ "jti": "<random-uuid>"
+}

If you absolutely must transport credentials, require HTTPS and JWE (encrypted JWT) and include short exp/jti.


134-141: Redact real-looking secrets/tokens in example output

These strings will trigger secret scanners and mislead readers.

Apply:

-    "Credentials": {
-        "AccessKeyId": "vTFcHUuPqjKdtzTsMULP",
-        "SecretAccessKey": "qBQxFcx99JOlABkZNq2fNRj6qGe18F9IREcHf9DZ",
-        "SessionToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhY2Nlc3Nfa2V5IjoidlRGY0hVdVBxaktkdHpUc01VTFAiLCJzZWNyZXRfa2V5IjoicUJReEZjeDk5Sk9sQUJrWk5xMmZOUmo2cUdlMThGOUlSRWNIZjlEWiIsImFzc3VtZWRfcm9sZV9hY2Nlc3Nfa2V5IjoicFFJSTFjbTVrRm1wd3FQNmJ6SmgiLCJpYXQiOjE3NTU0MzU3OTgsImV4cCI6MTc1NTQzOTM5OH0.z5Uap_7IPAbWCJyZC5zj8JleNshGxsYuhdbI9hOjr78",
-        "Expiration": "2025-08-17T14:03:18+00:00"
-    },
+    "Credentials": {
+        "AccessKeyId": "<ACCESS_KEY_ID>",
+        "SecretAccessKey": "<SECRET_ACCESS_KEY>",
+        "SessionToken": "<SESSION_TOKEN>",
+        "Expiration": "<EXPIRATION_ISO8601>"
+    },

And in the command snippet above:

-aws sts assume-role-with-web-identity --endpoint https://127.0.0.1:7443 --role-arn arn:aws:sts::pQII1cm5kFmpwqP6bzJh:role/ldap_user --role-session-name fry1 --web-identity-token eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VyIjoiZnJ5IiwicGFzc3dvcmQiOiJmcnkiLCJ0eXBlIjoibGRhcCIsImlhdCI6MTc1NTQzMzkxOX0. --no-verify-ssl
+aws sts assume-role-with-web-identity --endpoint https://127.0.0.1:7443 \
+  --role-arn arn:aws:sts::<ACCESS_KEY_ID>:role/ldap_user \
+  --role-session-name <SESSION_NAME> \
+  --web-identity-token <WEB_IDENTITY_TOKEN> \
+  --no-verify-ssl
src/sdk/sts_sdk.js (3)

71-73: Stop logging full account objects (risk of secret leakage)

Log only safe identifiers (access_key, role_name).

Apply:

-        dbg.log0('sts_sdk.get_assumed_role res', account,
-            'account.role_config: ', account.role_config);
+        dbg.log0('sts_sdk.get_assumed_role res', {
+            access_key,
+            role_name,
+        });
-        dbg.log0('sts_sdk.get_assumed_role_with_web_identity res', account,
-            'account.role_config: ', account.role_config);
+        dbg.log0('sts_sdk.get_assumed_role_with_web_identity res', {
+            access_key: req.body.role_arn.split(':')[4],
+            role_name: account.role_config?.role_name,
+        });

Also applies to: 128-130


88-113: Web-identity handling: require signature, restrict algorithms, validate claims, and harden error checks

Allowing decode without a secret enables trivial forgery. Also rely on err.name, not message text.

Apply:

-        let web_token;
-        const jwt_secret = ldap_client.instance().ldap_params?.jwt_secret;
-        if (jwt_secret) {
-            try {
-                web_token = jwt.verify(req.body.web_identity_token, jwt_secret);
-            } catch (err) {
-                dbg.error('get_assumed_ldap_user error: JWT token verification failed', err);
-                if (err.message.includes('TokenExpiredError')) {
-                    throw new RpcError('EXPIRED_WEB_IDENTITY_TOKEN', err.message);
-                } else {
-                    throw new RpcError('INVALID_WEB_IDENTITY_TOKEN', err.message);
-                }
-            }
-        } else {
-            dbg.warn('get_assumed_ldap_user error: No LDAP JWT secret found');
-            web_token = jwt.decode(req.body.web_identity_token);
-        }
+        const token = req.body?.web_identity_token;
+        if (!token) throw new RpcError('INVALID_WEB_IDENTITY_TOKEN', 'Missing web_identity_token');
+        const jwt_secret = ldap_client.instance().ldap_params?.jwt_secret;
+        if (!jwt_secret) {
+            dbg.error('get_assumed_ldap_user: No LDAP JWT secret configured; refusing unsigned tokens');
+            throw new RpcError('INVALID_WEB_IDENTITY_TOKEN', 'Unsigned tokens are not allowed');
+        }
+        let web_token;
+        try {
+            // TODO: extend to RS/ES if public keys are configured
+            web_token = jwt.verify(token, jwt_secret, { algorithms: ['HS256'] });
+        } catch (err) {
+            dbg.error('get_assumed_ldap_user: JWT verification failed', err);
+            if (err?.name === 'TokenExpiredError') throw new RpcError('EXPIRED_WEB_IDENTITY_TOKEN', err.message);
+            throw new RpcError('INVALID_WEB_IDENTITY_TOKEN', err.message || String(err));
+        }

107-113: Required claims validation should include null checks and avoid password-in-token (design)

Keep current checks for POC, but add robust guard for non-object payloads. Longer term, remove password from token and bind over TLS.

Apply:

-        if (!web_token.user) {
+        if (!web_token || typeof web_token !== 'object' || !web_token.user) {
             throw new RpcError('INVALID_WEB_IDENTITY_TOKEN', 'Missing a required claim: user');
         }

Follow-up (separate change): drop the password requirement and collect it via HTTPS at request time.

src/endpoint/sts/ops/sts_post_assume_role_with_web_identity.js (1)

14-15: Do not log raw WebIdentityToken/password

Redact sensitive fields before logging.

Apply:

-    dbg.log0('sts_post_assume_role_with_web_identity body: ', req.body);
+    const log_body = { ...req.body };
+    for (const k of Object.keys(log_body)) {
+        if (/(token|password|secret|jwt)/i.test(k)) log_body[k] = '<redacted>';
+    }
+    dbg.log0('sts_post_assume_role_with_web_identity body: ', log_body);
src/util/ldap_client.js (4)

21-25: reconnect(): await disconnect before reconnecting

Prevents racing connect against an in-flight disconnect.

Apply:

 async reconnect() {
     dbg.log0(`reconnect called`);
-    this.disconnect();
-    return this.connect();
+    await this.disconnect();
+    return this.connect();
 }

14-19: disconnect(): null-safety and await unbind

Avoid throwing when admin_client is missing and await unbind to close sockets.

Apply:

 async disconnect() {
     dbg.log0('ldap client disconnect called');
     this._disconnected_state = true;
     this._connect_promise = null;
-    this.admin_client.unbind();
+    if (this.admin_client) {
+        try {
+            await this.admin_client.unbind();
+        } catch (_) {
+            // ignore
+        }
+    }
 }

107-129: Authenticate: ensure admin is connected and always unbind user client; LDAP injection already mitigated by EqualityFilter

Add a connection check and release sockets on all paths.

Apply:

     const user_client = new ldap.Client({
         url: this.ldap_params.uri,
         tlsOptions: this.tls_options,
     });
-    const { searchEntries } = await this.admin_client.search(this.ldap_params.search_dn, search_options);
-    if (!searchEntries || searchEntries.length === 0) {
-        throw new Error('User not found');
-    }
-    await this._bind(user_client, searchEntries[0].dn, password);
-    return searchEntries[0].dn;
+    if (!this.is_connected()) await this.connect();
+    try {
+        const { searchEntries } = await this.admin_client.search(this.ldap_params.search_dn, search_options);
+        if (!searchEntries || searchEntries.length === 0) {
+            throw new Error('User not found');
+        }
+        await this._bind(user_client, searchEntries[0].dn, password);
+        return searchEntries[0].dn;
+    } finally {
+        try { await user_client.unbind(); } catch (_) { /* ignore */ }
+    }

35-62: Fix is_connected() call and default TLS security

Call the method (not property) and default to verifying certs; allow overrides via config.

Apply:

             this.tls_options = this.ldap_params.tls_options || {
-                'rejectUnauthorized': false,
+                rejectUnauthorized: true,
             };
@@
-            if (this.is_connected) {
-                await this.reconnect();
-            }
+            if (this.is_connected()) {
+                await this.reconnect();
+            }
🧹 Nitpick comments (6)
docs/design/ldap.md (1)

90-97: Minor markdown hygiene: linkify bare URLs

Wrap bare URLs in angle brackets or convert to text to satisfy MD034.

Example:

-read more here: https://docs.aws.amazon.com/cli/latest/reference/sts/assume-role-with-web-identity.html
+Read more here: <https://docs.aws.amazon.com/cli/latest/reference/sts/assume-role-with-web-identity.html>

Also applies to: 94-97, 102-102

src/test/integration_tests/api/sts/test_sts.js (3)

894-894: Fix typo in suite name

Use “identity” instead of “indentity”.

-mocha.describe('Assume role with web indentity tests', function() {
+mocha.describe('Assume role with web identity tests', function() {

914-915: Avoid global config mutation leaks in tests

Overwriting ldap_client.instance().ldap_params.jwt_secret can leak into other tests. Save/restore around the suite.

Apply:

-        ldap_client.instance().ldap_params.jwt_secret = "TEST_SECRET";
+        const saved_secret = ldap_client.instance().ldap_params?.jwt_secret;
+        ldap_client.instance().ldap_params.jwt_secret = "TEST_SECRET";
+        this.afterAll(() => { ldap_client.instance().ldap_params.jwt_secret = saved_secret; });

934-951: Add a positive-path test and “missing secret” case

Currently only negative flows are covered. Consider:

  • One test with a valid signed token and a stubbed authenticate() returning a DN → expect success.
  • One test where jwt_secret is unset → expect InvalidIdentityToken “Unsigned tokens are not allowed” (after implementing the SDK change).

I can add these tests with a stubbed ldap_client.instance().authenticate and secret save/restore if you want.

src/util/ldap_client.js (2)

76-83: Ensure config/client exists before connecting; avoid logging secrets

Guard against uninitialized admin_client and log only non-sensitive fields.

Apply:

 async connect() {
     this._disconnected_state = false;
     if (this._connect_promise) return this._connect_promise;
-    dbg.log0('connect called, current url:', this.ldap_params.uri);
+    if (!this.admin_client) await this.load_ldap_config();
+    const { uri, search_dn, search_scope } = this.ldap_params || {};
+    dbg.log0('connect called', { uri, search_dn, search_scope });
     this._connect_promise = this._connect();
     return this._connect_promise;
 }

84-96: Make the retry loop cancellable and handle missing client

Respect _disconnected_state and verify admin_client before binding.

Apply:

 async _connect() {
     let is_connected = false;
-    while (!is_connected) {
+    while (!is_connected && !this._disconnected_state) {
         try {
-            await this._bind(this.admin_client, this.ldap_params.admin, this.ldap_params.secret);
+            if (!this.admin_client) throw new Error('LDAP admin client not initialized');
+            await this._bind(this.admin_client, this.ldap_params.admin, this.ldap_params.secret);
             dbg.log0('_connect: initial connect succeeded');
             is_connected = true;
         } catch (err) {
             dbg.error('_connect: initial connect failed, will retry', err.message);
-            await P.delay(3000);
+            if (this._disconnected_state) break;
+            await P.delay(3000);
         }
     }
 }
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 92a710e and 377a6d5.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (12)
  • config.js (1 hunks)
  • docs/design/ldap.md (1 hunks)
  • package.json (1 hunks)
  • src/endpoint/endpoint.js (5 hunks)
  • src/endpoint/sts/ops/sts_post_assume_role.js (2 hunks)
  • src/endpoint/sts/ops/sts_post_assume_role_with_web_identity.js (1 hunks)
  • src/endpoint/sts/sts_errors.js (2 hunks)
  • src/endpoint/sts/sts_rest.js (1 hunks)
  • src/endpoint/sts/sts_utils.js (1 hunks)
  • src/sdk/sts_sdk.js (5 hunks)
  • src/test/integration_tests/api/sts/test_sts.js (3 hunks)
  • src/util/ldap_client.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (7)
  • src/endpoint/sts/sts_rest.js
  • src/endpoint/sts/sts_utils.js
  • package.json
  • src/endpoint/endpoint.js
  • src/endpoint/sts/ops/sts_post_assume_role.js
  • config.js
  • src/endpoint/sts/sts_errors.js
🧰 Additional context used
📓 Path-based instructions (1)
src/test/**/*.*

⚙️ CodeRabbit Configuration File

src/test/**/*.*: Ensure that the PR includes tests for the changes.

Files:

  • src/test/integration_tests/api/sts/test_sts.js
🧠 Learnings (1)
📚 Learning: 2025-06-17T12:59:51.543Z
Learnt from: naveenpaul1
PR: noobaa/noobaa-core#9042
File: src/util/cloud_utils.js:183-194
Timestamp: 2025-06-17T12:59:51.543Z
Learning: In the set_noobaa_s3_connection function in src/util/cloud_utils.js, internal NooBaa S3 endpoints (filtered by 'api': 's3', 'kind': 'INTERNAL') will always be non-secure, so tls: false is the correct setting despite the conditional https:// scheme logic.

Applied to files:

  • src/util/ldap_client.js
🧬 Code Graph Analysis (3)
src/endpoint/sts/ops/sts_post_assume_role_with_web_identity.js (2)
src/endpoint/sts/sts_utils.js (2)
  • duration_ms (43-43)
  • duration_sec (30-30)
src/util/http_utils.js (1)
  • CONTENT_TYPE_APP_FORM_URLENCODED (39-39)
src/test/integration_tests/api/sts/test_sts.js (3)
src/endpoint/sts/sts_utils.js (4)
  • require (5-5)
  • dbg (4-4)
  • jwt_utils (6-6)
  • config (7-7)
src/endpoint/endpoint.js (8)
  • require (39-39)
  • require (40-40)
  • require (42-42)
  • require (43-43)
  • require (45-45)
  • dbg (11-11)
  • config (18-18)
  • ldap_client (44-44)
src/util/ldap_client.js (2)
  • dbg (10-10)
  • config (7-7)
src/sdk/sts_sdk.js (2)
src/sdk/object_sdk.js (3)
  • require (27-27)
  • account_cache (50-63)
  • dbg (9-9)
src/util/ldap_client.js (1)
  • dbg (10-10)
🪛 LanguageTool
docs/design/ldap.md

[grammar] ~9-~9: Ensure spelling is correct
Context: ...ow: 1. The client sends a signed(with a predfined constant signature) or unsigned JWT as ...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~9-~9: There might be a mistake here.
Context: ... unsigned JWT as the web identity token. 2. The system parses the JWT to extract the...

(QB_NEW_EN)


[grammar] ~16-~16: There might be a mistake here.
Context: ...users. ## Configuring the external LDAP The administrator must store the LDAP co...

(QB_NEW_EN)


[grammar] ~17-~17: There might be a mistake here.
Context: ...DAP configuration in the following file: /etc/noobaa-server/ldap_config The conf...

(QB_NEW_EN)


[grammar] ~41-~41: There might be a mistake here.
Context: ... set - unsigned tokens or tokens signed with different secret will be dropped as acc...

(QB_NEW_EN)


[grammar] ~65-~65: There might be a mistake here.
Context: ...Password", "type": "ldap" } Sign it node.js example:js const jwt = require('...

(QB_NEW_EN)


[grammar] ~95-~95: There might be a mistake here.
Context: ... can base on dockers I used for testing: * AD image: `docker run --rm --privileged ...

(QB_NEW_EN)


[grammar] ~96-~96: There might be a mistake here.
Context: ...com/samba-in-kubernetes/samba-container) * LDAP image: `docker run --rm --privilege...

(QB_NEW_EN)


[grammar] ~97-~97: There might be a mistake here.
Context: ...ntainer/docker-test-openldap%2Fopenldap) 3. Add support to the operator side: * CLI...

(QB_NEW_EN)


[grammar] ~98-~98: There might be a mistake here.
Context: ...ap) 3. Add support to the operator side: * CLI command for configuring external LDA...

(QB_NEW_EN)


[grammar] ~99-~99: There might be a mistake here.
Context: ...LI command for configuring external LDAP * Create K8s Secret for LDAP info and moun...

(QB_NEW_EN)


[grammar] ~100-~100: There might be a mistake here.
Context: ...-server/ldap_config to the relevant pods 4. Better align and adapt to the IAM effort...

(QB_NEW_EN)


[grammar] ~101-~101: There might be a mistake here.
Context: ...dapt to the IAM effort also in POC stage 5. See if we want to support encrypted pass...

(QB_NEW_EN)


[grammar] ~105-~105: There might be a mistake here.
Context: ...ation scope if possible ## Appendix A: Creating account w/ role config using NooBaa API...

(QB_NEW_EN)


[style] ~131-~131: Consider a more concise word here.
Context: ...auth_token": "'$(cat .nbtoken)'" }' in order to assume this role:bash aws sts assum...

(IN_ORDER_TO_PREMIUM)

🪛 markdownlint-cli2 (0.17.2)
docs/design/ldap.md

90-90: Bare URL used

(MD034, no-bare-urls)


94-94: Bare URL used

(MD034, no-bare-urls)


96-96: Bare URL used

(MD034, no-bare-urls)


97-97: Bare URL used

(MD034, no-bare-urls)


102-102: Bare URL used

(MD034, no-bare-urls)


105-105: Trailing punctuation in heading
Punctuation: ':'

(MD026, no-trailing-punctuation)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Build Noobaa Image
  • GitHub Check: run-jest-unit-tests
  • GitHub Check: run-package-lock-validation
🔇 Additional comments (3)
src/test/integration_tests/api/sts/test_sts.js (1)

917-932: Good negative coverage for malformed/invalid signatures

The tests assert InvalidIdentityToken for malformed and invalid signature; this aligns with the new endpoint mapping.

src/sdk/sts_sdk.js (1)

148-153: LGTM: allow anonymous AssumeRoleWithWebIdentity

Skipping account authorization for this op is required; upstream JWT/LDAP validation provides security.

src/util/ldap_client.js (1)

132-141: LGTM: is_ldap_configured()

Clear boolean contract with ENOENT handling looks good.

1. ldap_client
2. support assume_role_with_web_identity

Signed-off-by: jackyalbo <[email protected]>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/endpoint/sts/ops/sts_post_assume_role.js (1)

43-48: Encrypt session tokens to prevent credential leakage

The current implementation generates a signed-but-readable JWT, which exposes AccessKeyId and SecretAccessKey in the token payload:

// File: src/endpoint/sts/ops/sts_post_assume_role.js (lines 43–48)
SessionToken: sts_utils.generate_session_token({
    access_key: access_keys.access_key.unwrap(),
    secret_key: access_keys.secret_key.unwrap(),
    assumed_role_access_key: assumed_role.access_key
}, duration_sec)

Verification shows:

  • In src/endpoint/sts/sts_utils.js, generate_session_token calls jwt_utils.make_auth_token(...)
  • In src/util/jwt_utils.js, make_auth_token uses jwt.sign(...), producing a JWS (signed-only) token
  • No JWE/AES encryption is applied

Action items:

  • Refactor generate_session_token to emit an encrypted (JWE) or AES-GCM–sealed token instead of a signed JWT
  • Alternatively, store temporary credentials server-side (e.g., TTL cache) and issue an opaque reference token
  • Consider using the “jose” package with AES-256-GCM and key rotation (kid) for sealed tokens

I’m available to help implement a minimal sealed-token solution in sts_utils.

♻️ Duplicate comments (13)
docs/design/ldap.md (4)

80-81: Avoid inline tokens; use file:// to prevent shell history/process list leaks

Also fix “endpoint” placeholder.

Apply:

-aws sts assume-role-with-web-identity --endpoint [endpoint] --role-arn [role-arn] --role-session-name [session-name] --web-identity-token eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VyIjoiVGhlVXNlck5hbWUiLCJwYXNzd29yZCI6IlRoZVVzZXJQYXNzd29yZCIsInR5cGUiOiJsZGFwIiwiaWF0IjoxNzU1MTgxOTE3fQ.
+echo '<jwt>' > /tmp/web-identity.jwt
+chmod 600 /tmp/web-identity.jwt
+aws sts assume-role-with-web-identity --endpoint <endpoint> \
+  --role-arn <role-arn> \
+  --role-session-name <session-name> \
+  --web-identity-token file:///tmp/web-identity.jwt

9-13: Do not allow unsigned JWTs; require signature and standard claims

Accepting unsigned tokens or “constant signature” is a critical auth bypass. Require signed JWTs and validate iss/aud/exp/jti.

Apply:

-1. The client sends a signed(with a predfined constant signature) or unsigned JWT as the web identity token.
-2. The system parses the JWT to extract the LDAP username and password.
+1. The client sends a signed JWT (HS256/RS256/ES256) as the web identity token. Unsigned tokens (alg="none") are not allowed. The token MUST include iss, aud (NooBaa STS endpoint), exp (<= 5 minutes), and jti.
+2. The system validates the signature and claims, and extracts the LDAP username (do not include passwords in the token; the password is provided directly to the server over TLS during authentication).
 3. These credentials are validated against a configured external LDAP server.
 4. Upon successful authentication, the system issues a temporary STS token, granting the user access to S3 resources for a limited duration.

41-53: Make jwt_secret mandatory; document secure storage and permissions

JWTs must be signed; “unsigned by default” is unsafe. Also advise 0600 permissions and ownership.

Apply:

-jwt_secret (Optional) - The JWT secret the administrator will use to sign the token sent in AssumeRoleWithWebIdentity requests (default: unsigned). Once this option is set - unsigned tokens or tokens signed with different secret will be dropped as access denied.
+jwt_secret (Required) – Verification material used to validate JWT signatures.
+For HS256, set a strong shared secret (>=256 bits). For RS256/ES256, configure the public key used to verify signatures.
+All tokens without a valid signature are rejected. Store this file securely (chmod 600, owned by the NooBaa process user or service account), preferably provisioned via a Kubernetes Secret.

60-64: Do not embed passwords in JWT; remove 'none' algorithm and add standard claims

Carrying plaintext passwords in a bearer token is a high risk. Remove password from payload, require exp/iss/aud/jti, and drop the alg='none' example.

Apply:

 {
- "user": "TheUserName",
- "password": "TheUserPassword",
- "type": "ldap"
+ "user": "TheUserName",
+ "type": "ldap"
 }
-const jwt = require('jsonwebtoken');
-console.log(jwt.sign({ user: "TheUserName", password: "TheUserPassword", type: "ldap" }, "IAMTHEADMIN123!(SHOULDBE256BITS)"));
-eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VyIjoiVGhlVXNlck5hbWUiLCJwYXNzd29yZCI6IlRoZVVzZXJQYXNzd29yZCIsInR5cGUiOiJsZGFwIiwiaWF0IjoxNzU1MTgxOTE3fQ.
+const jwt = require('jsonwebtoken');
+const payload = {
+  user: "TheUserName",
+  type: "ldap",
+  iss: "noobaa-ldap",
+  aud: "https://<endpoint-host>/sts/",
+  exp: Math.floor(Date.now() / 1000) + 300, // 5 minutes
+  jti: require('crypto').randomUUID()
+};
+console.log(jwt.sign(payload, "IAMTHEADMIN123!(SHOULDBE256BITS)", { algorithm: 'HS256' }));
-Or you can use 'none' algorithm if you are not interested with verifying the token
-```js
-const jwt = require('jsonwebtoken');
-console.log(jwt.sign({ user: "TheUserName", password: "TheUserPassword", type: "ldap" }, undefined, { algorithm: 'none' }));
-eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiVGhlVXNlck5hbWUiLCJwYXNzd29yZCI6IlRoZVVzZXJQYXNzd29yZCIsInR5cGUiOiJsZGFwIiwiaWF0IjoxNzU1MTgyMzQ2fQ.P6WYcdM0kJagNK4D0M8AHiGFcUZ-DhTOKHlC1-AxcT0
-```
+<!-- Unsigned tokens are not allowed -->

Also applies to: 68-76

src/util/ldap_client.js (9)

7-7: Likely incorrect config import path

From src/util/ldap_client.js, config is usually one level up (../config), not two (../../config). Please verify and fix.

Apply:

-const config = require('../../config');
+const config = require('../config');

21-25: reconnect(): await disconnect before connecting

Prevents racing connect against an in-flight disconnect.

Apply:

 async reconnect() {
     dbg.log0(`reconnect called`);
-    this.disconnect();
-    return this.connect();
+    await this.disconnect();
+    return this.connect();
 }

76-82: Ensure config is loaded before connecting; avoid logging secrets

If connect is called early, admin_client may be undefined. Also log only non-sensitive context.

Apply:

 async connect() {
     this._disconnected_state = false;
     if (this._connect_promise) return this._connect_promise;
-    dbg.log0('connect called, current url:', this.ldap_params.uri);
+    if (!this.admin_client) {
+        await this.load_ldap_config();
+    }
+    dbg.log0('connect called', { uri: this.ldap_params?.uri, search_dn: this.ldap_params?.search_dn, search_scope: this.ldap_params?.search_scope });
     this._connect_promise = this._connect();
     return this._connect_promise;
 }

14-19: disconnect(): guard and await unbind to avoid dangling sockets

Avoid null deref and ensure the socket is closed before returning.

Apply:

 async disconnect() {
     dbg.log0('ldap client disconnect called');
     this._disconnected_state = true;
     this._connect_promise = null;
-    this.admin_client.unbind();
+    if (this.admin_client) {
+        try {
+            await this.admin_client.unbind();
+        } catch (_) {
+            // ignore
+        }
+    }
 }

49-51: Defaulting rejectUnauthorized: false weakens TLS security

Enable cert verification by default; allow override via config if needed.

Apply:

-            this.tls_options = this.ldap_params.tls_options || {
-                'rejectUnauthorized': false,
-            };
+            this.tls_options = this.ldap_params.tls_options || {
+                rejectUnauthorized: true,
+            };

84-96: Make retry loop cancellable and robust; verify admin_client exists

Observe _disconnected_state and guard against missing admin_client.

Apply:

 async _connect() {
     let is_connected = false;
-    while (!is_connected) {
+    while (!is_connected && !this._disconnected_state) {
         try {
-            await this._bind(this.admin_client, this.ldap_params.admin, this.ldap_params.secret);
+            if (!this.admin_client) throw new Error('LDAP admin client not initialized');
+            await this._bind(this.admin_client, this.ldap_params.admin, this.ldap_params.secret);
             dbg.log0('_connect: initial connect succeeded');
             is_connected = true;
         } catch (err) {
-            dbg.error('_connect: initial connect failed, will retry', err.message);
-            await P.delay(3000);
+            dbg.error('_connect: initial connect failed, will retry', err);
+            if (this._disconnected_state) break;
+            await P.delay(3000);
         }
     }
 }

119-129: Ensure admin connection and always unbind the user client

Avoid LDAP injection is already addressed by EqualityFilter; add connection check and cleanup.

Apply:

-        const user_client = new ldap.Client({
+        if (!this.is_connected()) {
+            await this.connect();
+        }
+        const user_client = new Client({
             url: this.ldap_params.uri,
             tlsOptions: this.tls_options,
         });
-        const { searchEntries } = await this.admin_client.search(this.ldap_params.search_dn, search_options);
-        if (!searchEntries || searchEntries.length === 0) {
-            throw new Error('User not found');
-        }
-        await this._bind(user_client, searchEntries[0].dn, password);
-        return searchEntries[0].dn;
+        try {
+            const { searchEntries } = await this.admin_client.search(this.ldap_params.search_dn, search_options);
+            if (!searchEntries || searchEntries.length === 0) {
+                throw new Error('User not found');
+            }
+            await this._bind(user_client, searchEntries[0].dn, password);
+            return searchEntries[0].dn;
+        } finally {
+            try { await user_client.unbind(); } catch (_) { /* ignore */ }
+        }

52-58: Reconnect condition bug: calling the method vs. checking its existence

this.is_connected is a function; current truthy check always triggers reconnect.

Apply:

-            this.admin_client = new ldap.Client({
+            this.admin_client = new Client({
                 url: this.ldap_params.uri,
                 tlsOptions: this.tls_options,
             });
-            if (this.is_connected) {
-                await this.reconnect();
-            }
+            if (this.is_connected()) await this.reconnect();

35-48: Load config: accept documented keys and avoid breaking with 'admin'/'secret'

Docs use admin/secret; code only reads admin_user/admin_password. Also accept search_base, not just search_dn.

Apply:

             const params = JSON.parse(fs.readFileSync(config.LDAP_CONFIG_PATH).toString());
             this.ldap_params = {
-                uri: params.uri || 'ldaps://127.0.0.1:636',
-                admin: params.admin_user || 'Administrator',
-                secret: params.admin_password || 'Passw0rd',
-                search_dn: params.search_dn || 'ou=people,dc=example,dc=com',
+                uri: params.uri || 'ldaps://127.0.0.1:636',
+                // accept both "admin" and "admin_user"
+                admin: (params.admin ?? params.admin_user) || 'Administrator',
+                // accept both "secret" and "admin_password"
+                secret: (params.secret ?? params.admin_password) || 'Passw0rd',
+                // accept both "search_dn" and "search_base"
+                search_dn: (params.search_dn ?? params.search_base) || 'ou=people,dc=example,dc=com',
                 dn_attribute: params.dn_attribute || 'uid', // for LDAP 'sAMAccountName' for AD
                 search_scope: params.search_scope || 'sub',
                 jwt_secret: params.jwt_secret,
                 ...params,
             };
🧹 Nitpick comments (7)
src/endpoint/sts/ops/sts_post_assume_role.js (1)

15-17: Duration parsing and rounding alignment

  • parse_sts_duration already throws StsError (with InvalidParameterValue or a range‐validation error) on invalid, non‐integer, or out-of-range inputs. These will be handled by the central STS error handler and return the correct 4xx response—no additional try/catch is needed here.

  • Since parse_sts_duration returns duration_sec * 1000, the result is always a whole number of seconds. In practice,

    Math.ceil(duration_ms / 1000) === duration_ms / 1000

    so there’s no drift between the token TTL and the XML Expiration.

  • Optional: if you’d like to future-proof against any sub-second durations and keep both values derived identically, you can switch to Math.floor and multiply back:

    - const duration_sec = Math.ceil(duration_ms / 1000);
    - const expiration_time = Date.now() + duration_ms;
    + const duration_sec = Math.floor(duration_ms / 1000);
    + const expiration_time = Date.now() + duration_sec * 1000;
docs/design/ldap.md (6)

22-24: Fix LDAPS URI example and port mismatch

Example shows ldap:// with 636. Either use ldaps://:636 or ldap://:389. Recommend LDAPS.

Apply:

-  "uri": "ldap://ldap.example.com:636",
+  "uri": "ldaps://ldap.example.com:636",

Also applies to: 46-46


88-90: Replace bare URL with a Markdown link

Minor doc polish per markdownlint.

Apply:

-read more here: https://docs.aws.amazon.com/cli/latest/reference/sts/assume-role-with-web-identity.html
+Read more: [AWS CLI — assume-role-with-web-identity](https://docs.aws.amazon.com/cli/latest/reference/sts/assume-role-with-web-identity.html)

92-104: Rephrase next step 5: do not encourage embedding passwords; if transporting, require JWE

Avoid normalizing on encrypted passwords in JWT; mandate JWE if credentials must be transported at all, and still require TLS and exp/jti.

Apply:

-5. See if we want to support encrypted password as part of the JWT token. see here: https://auth0.com/docs/secure/tokens/access-tokens/json-web-encryption
+5. Avoid transporting passwords in tokens. If strictly required, use JWE (encrypted JWT) with TLS, short exp, and jti to mitigate replay: https://auth0.com/docs/secure/tokens/access-tokens/json-web-encryption

94-97: Wrap remaining bare URLs in Markdown links

Conforms with MD034 and improves readability.

Apply:

-1. See if we can move to using C open-ldap client as part of our native code instead of ldapts (for better performance and fewer security issues). see here: https://www.openldap.org/software/repo.html (We will mainly need bind and search)
+1. See if we can move to using C open-ldap client as part of our native code instead of ldapts (for better performance and fewer security issues). See: [OpenLDAP repo](https://www.openldap.org/software/repo.html) (We will mainly need bind and search)
-* AD image: `docker run --rm --privileged -p 636:636 quay.io/samba.org/samba-ad-server:latest` (https://github.com/samba-in-kubernetes/samba-container)
+* AD image: `docker run --rm --privileged -p 636:636 quay.io/samba.org/samba-ad-server:latest` ([repo](https://github.com/samba-in-kubernetes/samba-container))
-* LDAP image: `docker run --rm --privileged -p 636:636 ghcr.io/ldapjs/docker-test-openldap/openldap:latest` (https://github.com/ldapjs/docker-test-openldap/pkgs/container/docker-test-openldap%2Fopenldap)
+* LDAP image: `docker run --rm --privileged -p 636:636 ghcr.io/ldapjs/docker-test-openldap/openldap:latest` ([package](https://github.com/ldapjs/docker-test-openldap/pkgs/container/docker-test-openldap%2Fopenldap))
-4. Better align and adapt to the IAM effort also in POC stage
+4. Better align and adapt to the IAM effort also in POC stage

Also applies to: 102-102


105-105: Remove trailing punctuation in heading

Minor markdownlint fix (MD026).

Apply:

-## Appendix A: Creating account w/ role config using NooBaa API:
+## Appendix A: Creating account w/ role config using NooBaa API

131-131: Style: make the lead-in concise

“In order to” → “To”.

Apply:

-in order to assume this role:
+To assume this role:
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 377a6d5 and b0875ab.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (12)
  • config.js (1 hunks)
  • docs/design/ldap.md (1 hunks)
  • package.json (1 hunks)
  • src/endpoint/endpoint.js (5 hunks)
  • src/endpoint/sts/ops/sts_post_assume_role.js (2 hunks)
  • src/endpoint/sts/ops/sts_post_assume_role_with_web_identity.js (1 hunks)
  • src/endpoint/sts/sts_errors.js (2 hunks)
  • src/endpoint/sts/sts_rest.js (1 hunks)
  • src/endpoint/sts/sts_utils.js (1 hunks)
  • src/sdk/sts_sdk.js (5 hunks)
  • src/test/integration_tests/api/sts/test_sts.js (3 hunks)
  • src/util/ldap_client.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (9)
  • package.json
  • src/test/integration_tests/api/sts/test_sts.js
  • src/endpoint/sts/sts_utils.js
  • src/endpoint/sts/ops/sts_post_assume_role_with_web_identity.js
  • config.js
  • src/endpoint/sts/sts_errors.js
  • src/endpoint/sts/sts_rest.js
  • src/sdk/sts_sdk.js
  • src/endpoint/endpoint.js
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-06-17T12:59:51.543Z
Learnt from: naveenpaul1
PR: noobaa/noobaa-core#9042
File: src/util/cloud_utils.js:183-194
Timestamp: 2025-06-17T12:59:51.543Z
Learning: In the set_noobaa_s3_connection function in src/util/cloud_utils.js, internal NooBaa S3 endpoints (filtered by 'api': 's3', 'kind': 'INTERNAL') will always be non-secure, so tls: false is the correct setting despite the conditional https:// scheme logic.

Applied to files:

  • src/util/ldap_client.js
🧬 Code Graph Analysis (1)
src/endpoint/sts/ops/sts_post_assume_role.js (2)
src/endpoint/sts/ops/sts_post_assume_role_with_web_identity.js (5)
  • sts_utils (8-8)
  • require (5-5)
  • require (6-6)
  • dbg (4-4)
  • duration_ms (15-15)
src/endpoint/sts/sts_utils.js (2)
  • dbg (4-4)
  • duration_ms (43-43)
🪛 LanguageTool
docs/design/ldap.md

[grammar] ~9-~9: Ensure spelling is correct
Context: ...ow: 1. The client sends a signed(with a predfined constant signature) or unsigned JWT as ...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~9-~9: There might be a mistake here.
Context: ... unsigned JWT as the web identity token. 2. The system parses the JWT to extract the...

(QB_NEW_EN)


[grammar] ~16-~16: There might be a mistake here.
Context: ...users. ## Configuring the external LDAP The administrator must store the LDAP co...

(QB_NEW_EN)


[grammar] ~17-~17: There might be a mistake here.
Context: ...DAP configuration in the following file: /etc/noobaa-server/ldap_config The conf...

(QB_NEW_EN)


[grammar] ~41-~41: There might be a mistake here.
Context: ... set - unsigned tokens or tokens signed with different secret will be dropped as acc...

(QB_NEW_EN)


[grammar] ~65-~65: There might be a mistake here.
Context: ...Password", "type": "ldap" } Sign it node.js example:js const jwt = require('...

(QB_NEW_EN)


[grammar] ~95-~95: There might be a mistake here.
Context: ... can base on dockers I used for testing: * AD image: `docker run --rm --privileged ...

(QB_NEW_EN)


[grammar] ~96-~96: There might be a mistake here.
Context: ...com/samba-in-kubernetes/samba-container) * LDAP image: `docker run --rm --privilege...

(QB_NEW_EN)


[grammar] ~97-~97: There might be a mistake here.
Context: ...ntainer/docker-test-openldap%2Fopenldap) 3. Add support to the operator side: * CLI...

(QB_NEW_EN)


[grammar] ~98-~98: There might be a mistake here.
Context: ...ap) 3. Add support to the operator side: * CLI command for configuring external LDA...

(QB_NEW_EN)


[grammar] ~99-~99: There might be a mistake here.
Context: ...LI command for configuring external LDAP * Create K8s Secret for LDAP info and moun...

(QB_NEW_EN)


[grammar] ~100-~100: There might be a mistake here.
Context: ...-server/ldap_config to the relevant pods 4. Better align and adapt to the IAM effort...

(QB_NEW_EN)


[grammar] ~101-~101: There might be a mistake here.
Context: ...dapt to the IAM effort also in POC stage 5. See if we want to support encrypted pass...

(QB_NEW_EN)


[grammar] ~105-~105: There might be a mistake here.
Context: ...ation scope if possible ## Appendix A: Creating account w/ role config using NooBaa API...

(QB_NEW_EN)


[style] ~131-~131: Consider a more concise word here.
Context: ...auth_token": "'$(cat .nbtoken)'" }' in order to assume this role:bash aws sts assum...

(IN_ORDER_TO_PREMIUM)

🪛 markdownlint-cli2 (0.17.2)
docs/design/ldap.md

90-90: Bare URL used

(MD034, no-bare-urls)


94-94: Bare URL used

(MD034, no-bare-urls)


96-96: Bare URL used

(MD034, no-bare-urls)


97-97: Bare URL used

(MD034, no-bare-urls)


102-102: Bare URL used

(MD034, no-bare-urls)


105-105: Trailing punctuation in heading
Punctuation: ':'

(MD026, no-trailing-punctuation)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: ceph-s3-tests / Ceph S3 Tests
  • GitHub Check: run-unit-tests / Unit Tests
🔇 Additional comments (2)
src/endpoint/sts/ops/sts_post_assume_role.js (1)

8-8: Centralized STS helpers import — LGTM

Good consolidation onto sts_utils; aligns this op with AssumeRoleWithWebIdentity and reduces duplication.

src/util/ldap_client.js (1)

109-112: Use EqualityFilter from ldapts/filters (per import change)

Update constructor call to the correct class after import.

Apply:

-        const eqFilter = new ldap.EqualityFilter({
+        const eqFilter = new EqualityFilter({
             attribute: this.ldap_params.dn_attribute,
             value: user,
         });

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/endpoint/sts/ops/sts_post_assume_role.js (1)

19-26: Broaden error mapping: NO_SUCH_ACCOUNT/NO_SUCH_ROLE should not return 500

Mapping everything except ACCESS_DENIED to InternalFailure (500) is misleading. Invalid role ARN, missing account/role, etc., should be 4xx.

-    } catch (err) {
-        if (err.code === 'ACCESS_DENIED') {
-            throw new StsError(StsError.AccessDeniedException);
-        }
-        throw new StsError(StsError.InternalFailure);
-    }
+    } catch (err) {
+        switch (err.code) {
+            case 'ACCESS_DENIED':
+                throw new StsError(StsError.AccessDeniedException);
+            case 'NO_SUCH_ACCOUNT':
+            case 'NO_SUCH_ROLE':
+                throw new StsError(StsError.NotAuthorized);
+            case 'INVALID_ROLE_ARN':
+                throw new StsError(StsError.InvalidParameterValue);
+            default:
+                dbg.error('assume_role unexpected error', err);
+                throw new StsError(StsError.InternalFailure);
+        }
+    }
♻️ Duplicate comments (11)
src/sdk/sts_sdk.js (2)

71-73: Avoid logging entire account objects

Previous review already flagged this — logging account and role_config risks leaking secrets.

-        dbg.log0('sts_sdk.get_assumed_role res', account,
-            'account.role_config: ', account.role_config);
+        dbg.log0('sts_sdk.get_assumed_role res', {
+            access_key,
+            role_name: account.role_config?.role_name
+        });

Apply similarly to get_assumed_ldap_user logs below.


91-107: Reject unsigned tokens; restrict algorithms; handle decode nulls via early validation

Accepting decoded (unsigned) tokens when jwt_secret is missing effectively allows anyone to supply credentials in a bearer token. This is unsafe and contradicts the PR description that tokens are signed with a server secret.

Apply:

-        let web_token;
-        const jwt_secret = ldap_client.instance().ldap_params?.jwt_secret;
-        if (jwt_secret) {
-            try {
-                web_token = jwt.verify(req.body.web_identity_token, jwt_secret);
-            } catch (err) {
-                dbg.error('get_assumed_ldap_user error: JWT token verification failed', err);
-                if (err.message.includes('TokenExpiredError')) {
-                    throw new RpcError('EXPIRED_WEB_IDENTITY_TOKEN', err.message);
-                } else {
-                    throw new RpcError('INVALID_WEB_IDENTITY_TOKEN', err.message);
-                }
-            }
-        } else {
-            dbg.warn('get_assumed_ldap_user: No LDAP JWT secret found, failing back to decoding');
-            web_token = jwt.decode(req.body.web_identity_token);
-        }
+        const token = req.body?.web_identity_token;
+        if (!token) {
+            throw new RpcError('INVALID_WEB_IDENTITY_TOKEN', 'Missing web_identity_token');
+        }
+        const jwt_secret = ldap_client.instance().ldap_params?.jwt_secret;
+        if (!jwt_secret) {
+            dbg.error('get_assumed_ldap_user: No LDAP JWT secret configured; refusing unsigned tokens');
+            throw new RpcError('INVALID_WEB_IDENTITY_TOKEN', 'Unsigned tokens are not allowed');
+        }
+        let web_token;
+        try {
+            // Restrict to expected HMAC algorithms; extend if RS/ES keys are configured
+            web_token = jwt.verify(token, jwt_secret, { algorithms: ['HS256'] });
+        } catch (err) {
+            dbg.error('get_assumed_ldap_user: JWT verification failed', err);
+            if (err?.name === 'TokenExpiredError') {
+                throw new RpcError('EXPIRED_WEB_IDENTITY_TOKEN', err.message);
+            }
+            throw new RpcError('INVALID_WEB_IDENTITY_TOKEN', err.message || String(err));
+        }

Also validate the decoded shape before accessing fields:

-        if (!web_token.user) {
+        if (!web_token || typeof web_token !== 'object' || !web_token.user) {
             throw new RpcError('INVALID_WEB_IDENTITY_TOKEN', 'Missing a required claim: user');
         }
docs/design/ldap.md (6)

79-81: Avoid inline tokens; use file:// to prevent leaking via shell history/ps

Also normalize placeholders and fix bracket style.

-aws sts assume-role-with-web-identity --endpoint [endpoint] --role-arn [role-arn] --role-session-name [session-name] --web-identity-token eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VyIjoiVGhlVXNlck5hbWUiLCJwYXNzd29yZCI6IlRoZVVzZXJQYXNzd29yZCIsInR5cGUiOiJsZGFwIiwiaWF0IjoxNzU1MTgxOTE3fQ.
+echo '<WEB_IDENTITY_TOKEN>' > /tmp/web-identity.jwt
+chmod 600 /tmp/web-identity.jwt
+aws sts assume-role-with-web-identity --endpoint <endpoint> \
+  --role-arn <ROLE_ARN> \
+  --role-session-name <SESSION_NAME> \
+  --web-identity-token file:///tmp/web-identity.jwt

134-141: Use placeholders and file-based token in Appendix; avoid JWT-looking strings

Also make --no-verify-ssl clearly dev-only.

-aws sts assume-role-with-web-identity --endpoint https://127.0.0.1:7443 --role-arn arn:aws:sts::pQII1cm5kFmpwqP6bzJh:role/ldap_user --role-session-name fry1 --web-identity-token eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VyIjoiZnJ5IiwicGFzc3dvcmQiOiJmcnkiLCJ0eXBlIjoibGRhcCIsImlhdCI6MTc1NTQzMzkxOX0. --no-verify-ssl
+echo '<WEB_IDENTITY_TOKEN>' > /tmp/web-identity.jwt
+chmod 600 /tmp/web-identity.jwt
+aws sts assume-role-with-web-identity --endpoint https://127.0.0.1:7443 \
+  --role-arn arn:aws:sts::<ACCOUNT_ID>:role/ldap_user \
+  --role-session-name <SESSION_NAME> \
+  --web-identity-token file:///tmp/web-identity.jwt \
+  --no-verify-ssl # for local dev only

9-13: Disallow unsigned/constant-signature JWTs; require signed tokens with strict claims

Accepting unsigned tokens or a "constant signature" is an auth bypass. Require HS256/RS256/ES256, and validate iss, aud, exp (<=5m), and jti.

-1. The client sends a signed(with a predfined constant signature) or unsigned JWT as the web identity token.
-2. The system parses the JWT to extract the LDAP username and password.
+1. The client sends a signed JWT (HS256/RS256/ES256) as the web identity token. Unsigned tokens (alg="none") are not allowed.
+2. The server validates the JWT signature and the following claims: iss, aud (NooBaa STS endpoint), exp (<= 5 minutes), and jti (nonce).
+3. The system parses the JWT to extract the LDAP username (and, if present, an encrypted password per the guidance below).
-3. These credentials are validated against a configured external LDAP server.
-4. Upon successful authentication, the system issues a temporary STS token, granting the user access to S3 resources for a limited duration.
+4. These credentials are validated against a configured external LDAP server.
+5. Upon successful authentication, the system issues a temporary STS token, granting the user access to S3 resources for a limited duration.

41-53: Make jwt_secret required; document secure storage (0600) and key type

JWTs must be signed; an “unsigned by default” mode is unsafe. Also clarify HS vs RS/ES keying.

-jwt_secret (Optional) - The JWT secret the administrator will use to sign the token sent in AssumeRoleWithWebIdentity requests (default: unsigned). Once this option is set - unsigned tokens or tokens signed with different secret will be dropped as access denied.
+jwt_secret (Required) – Secret (for HS256) or private key (for RS/ES) used to sign JWTs for AssumeRoleWithWebIdentity.
+All tokens without a valid signature are rejected. Store this material securely on disk with file permissions 0600 and owned by the NooBaa process user. Consider using a secrets manager in production.

56-64: Avoid embedding plaintext passwords in JWT; if unavoidable, require JWE + TLS

Embedding passwords in a bearer token is risky (logs, proxies, shell history). Prefer server-side bind using a password sent over TLS without re-encoding into a token. If you must carry it, use JWE (encrypted JWT), short exp, and jti.

-{
- "user": "TheUserName",
- "password": "TheUserPassword",
- "type": "ldap"
-}
+{
+  "user": "TheUserName",
+  "type": "ldap",
+  "iss": "noobaa-ldap",
+  "aud": "https://<endpoint-host>/sts/",
+  "exp": <epoch-seconds-now-plus-300>,
+  "jti": "<random-uuid>"
+}

Follow-up: If you decide to keep a password in the token for the POC, explicitly require JWE (encrypted JWT) and HTTPS in this section. I can add a minimal “jose” example if you want.


68-76: Fix JWT example and remove 'none' algorithm guidance

Show a single HS256 example with standard claims; remove alg='none'.

-const jwt = require('jsonwebtoken');
-console.log(jwt.sign({ user: "TheUserName", password: "TheUserPassword", type: "ldap" }, "IAMTHEADMIN123!(SHOULDBE256BITS)"));
-eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VyIjoiVGhlVXNlck5hbWUiLCJwYXNzd29yZCI6IlRoZVVzZXJQYXNzd29yZCIsInR5cGUiOiJsZGFwIiwiaWF0IjoxNzU1MTgxOTE3fQ.
-```
-Or you can use 'none' algorithm if you are not interested with verifying the token
-```js
-const jwt = require('jsonwebtoken');
-console.log(jwt.sign({ user: "TheUserName", password: "TheUserPassword", type: "ldap" }, undefined, { algorithm: 'none' }));
-eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiVGhlVXNlck5hbWUiLCJwYXNzd29yZCI6IlRoZVVzZXJQYXNzd29yZCIsInR5cGUiOiJsZGFwIiwiaWF0IjoxNzU1MTgyMzQ2fQ.P6WYcdM0kJagNK4D0M8AHiGFcUZ-DhTOKHlC1-AxcT0
+const jwt = require('jsonwebtoken');
+const { randomUUID } = require('crypto');
+const payload = {
+  user: "TheUserName",
+  type: "ldap",
+  iss: "noobaa-ldap",
+  aud: "https://<endpoint-host>/sts/",
+  exp: Math.floor(Date.now() / 1000) + 300, // 5 minutes
+  jti: randomUUID(),
+};
+console.log(jwt.sign(payload, "<YOUR_256_BIT_SECRET>", { algorithm: 'HS256' }));
src/util/ldap_client.js (3)

49-51: Defaulting rejectUnauthorized: false weakens TLS security

This enables MITM by default. The earlier diff sets it to true; keep an override for dev/test.


39-58: Fix defaults, key mapping, TLS security, and connection check

  • Map both admin/secret and admin_user/admin_password; search_dn/search_base.
  • Default rejectUnauthorized: true (currently false is insecure).
  • Call is_connected() (not the function reference).
             this.ldap_params = {
-                uri: params.uri || 'ldaps://127.0.0.1:636',
-                admin: params.admin_user || 'Administrator',
-                secret: params.admin_password || 'Passw0rd',
-                search_dn: params.search_dn || 'ou=people,dc=example,dc=com',
+                uri: params.uri || 'ldaps://127.0.0.1:636',
+                // Accept both legacy and new keys
+                admin: (params.admin ?? params.admin_user) || 'Administrator',
+                secret: (params.secret ?? params.admin_password) || 'Passw0rd',
+                // Accept both search_base and search_dn
+                search_dn: (params.search_dn ?? params.search_base) || 'ou=people,dc=example,dc=com',
                 dn_attribute: params.dn_attribute || 'uid', // for LDAP 'sAMAccountName' for AD
                 search_scope: params.search_scope || 'sub',
                 jwt_secret: params.jwt_secret,
                 ...params,
             };
             this.tls_options = this.ldap_params.tls_options || {
-                'rejectUnauthorized': false,
+                rejectUnauthorized: true,
             };
             this.admin_client = new ldap.Client({
                 url: this.ldap_params.uri,
                 tlsOptions: this.tls_options,
             });
-            if (this.is_connected) {
-                await this.reconnect();
-            }
+            if (this.is_connected()) {
+                await this.reconnect();
+            }

107-129: authenticate(): ensure admin is connected and always unbind user client

Also safe-close user client via finally.

 async authenticate(user, password) {
     // EqualityFilter automatically escapes the value to prevent LDAP injection
-    const eqFilter = new ldap.EqualityFilter({
+    const eqFilter = new ldap.EqualityFilter({
         attribute: this.ldap_params.dn_attribute,
         value: user,
     });
@@
-    const user_client = new ldap.Client({
+    if (!this.is_connected()) {
+        await this.connect();
+    }
+    const user_client = new ldap.Client({
         url: this.ldap_params.uri,
         tlsOptions: this.tls_options,
     });
-    const { searchEntries } = await this.admin_client.search(this.ldap_params.search_dn, search_options);
-    if (!searchEntries || searchEntries.length === 0) {
-        throw new Error('User not found');
-    }
-    await this._bind(user_client, searchEntries[0].dn, password);
-    return searchEntries[0].dn;
+    try {
+        const { searchEntries } = await this.admin_client.search(this.ldap_params.search_dn, search_options);
+        if (!searchEntries || searchEntries.length === 0) {
+            throw new Error('User not found');
+        }
+        await this._bind(user_client, searchEntries[0].dn, password);
+        return searchEntries[0].dn;
+    } finally {
+        try { await user_client.unbind(); } catch (_) { /* ignore */ }
+    }
 }
🧹 Nitpick comments (13)
src/endpoint/sts/sts_errors.js (1)

144-148: InvalidIdentityToken message is too specific; use generic message and put specifics in Detail

AWS typically uses a general message and explains specifics optionally. Consider:

-StsError.InvalidIdentityToken = Object.freeze({
-    code: 'InvalidIdentityToken',
-    message: 'Missing a required claim',
-    http_code: 400,
-});
+StsError.InvalidIdentityToken = Object.freeze({
+    code: 'InvalidIdentityToken',
+    message: 'The web identity token that was passed could not be validated.',
+    http_code: 400,
+});

Then set StsError.detail to the specific reason (e.g., missing claim).

src/sdk/sts_sdk.js (2)

110-119: Avoid embedding passwords in bearer tokens; prefer TLS credentials or proper OIDC

Carrying a raw LDAP password inside a bearer token is a severe footgun: any log, proxy, or reuse turns it into a credential leak. Consider:

  • Accept user/password in the request body over TLS and authenticate server-side, or
  • Adopt a proper OIDC IdP and consume its ID tokens instead of rolling a JWT.

I can help draft a revised flow that keeps passwords out of tokens and uses short-lived server-issued web identity tokens after initial bind.


127-134: Include OIDC-like fields (iss/aud/sub) if present for web-identity responses

assume_role_with_web_identity often echoes sub/aud/iss; returning them here can simplify response construction downstream.

         const account = await this._assume_role(req.body.role_arn);
-        dbg.log0('sts_sdk.get_assumed_role_with_web_identity res', account,
-            'account.role_config: ', account.role_config);
+        dbg.log0('sts_sdk.get_assumed_role_with_web_identity resolved', {
+            access_key: req.body.role_arn.split(':')[4],
+            role_name: account.role_config?.role_name
+        });
         return {
             access_key: req.body.role_arn.split(':')[4],
             role_config: account.role_config,
             dn,
+            iss: web_token.iss,
+            aud: web_token.aud,
+            sub: web_token.sub || web_token.user,
         };
docs/design/ldap.md (5)

46-47: Fix scheme/port mismatch in example URI

Port 636 implies LDAPS; use ldaps:// or change port to 389 for ldap://.

-  "uri": "ldap://ldap.example.com:636",
+  "uri": "ldaps://ldap.example.com:636",

90-90: Replace bare URL with a markdown link

Minor style fix; keeps linters happy.

-read more here: https://docs.aws.amazon.com/cli/latest/reference/sts/assume-role-with-web-identity.html
+Read more here: [AssumeRoleWithWebIdentity](https://docs.aws.amazon.com/cli/latest/reference/sts/assume-role-with-web-identity.html)

105-105: Remove trailing colon in heading

Markdown lint MD026.

-## Appendix A: Creating account w/ role config using NooBaa API:
+## Appendix A: Creating account w/ role config using NooBaa API

131-133: Tighten phrasing

“in order to” → “To”.

-in order to assume this role:
+To assume this role:

22-40: Grammar/typos and clarity nits

  • “signed(with a predfined …)” → spacing and spelling.
  • Clarify “admin username” is a DN.
-1. The client sends a signed(with a predfined constant signature) or unsigned JWT as the web identity token.
+1. The client sends a signed (with a configured key) JWT as the web identity token.
@@
-admin (Required) – An administrator username with permission to execute search queries on the LDAP server.
+admin (Required) – The administrator DN with permission to execute search queries on the LDAP server.
src/util/ldap_client.js (5)

14-19: disconnect(): await unbind and guard null to avoid dangling sockets

Unbinding is async; also guard absent client.

 async disconnect() {
     dbg.log0('ldap client disconnect called');
     this._disconnected_state = true;
     this._connect_promise = null;
-    this.admin_client.unbind();
+    if (this.admin_client) {
+        try {
+            await this.admin_client.unbind();
+        } catch (_) {
+            // ignore
+        }
+    }
 }

21-25: reconnect(): await disconnect before connect to avoid races

Prevent connect/start racing with in-flight unbind.

 async reconnect() {
     dbg.log0(`reconnect called`);
-    this.disconnect();
-    return this.connect();
+    await this.disconnect();
+    return this.connect();
 }

76-82: Ensure config/client loaded before attempting to connect

connect() can run before load_ldap_config initializes admin_client.

 async connect() {
     this._disconnected_state = false;
     if (this._connect_promise) return this._connect_promise;
-    dbg.log0('connect called, current url:', this.ldap_params.uri);
+    if (!this.admin_client) {
+        await this.load_ldap_config();
+    }
+    dbg.log0('connect called, current url:', this.ldap_params?.uri);
     this._connect_promise = this._connect();
     return this._connect_promise;
 }

84-96: Make retry loop cancellable and guard missing admin_client

Otherwise it can spin forever after disconnect or when client isn’t initialized.

 async _connect() {
     let is_connected = false;
-    while (!is_connected) {
+    while (!is_connected && !this._disconnected_state) {
         try {
-            await this._bind(this.admin_client, this.ldap_params.admin, this.ldap_params.secret);
+            if (!this.admin_client) throw new Error('LDAP admin client not initialized');
+            await this._bind(this.admin_client, this.ldap_params.admin, this.ldap_params.secret);
             dbg.log0('_connect: initial connect succeeded');
             is_connected = true;
         } catch (err) {
-            dbg.error('_connect: initial connect failed, will retry', err.message);
-            await P.delay(3000);
+            dbg.error('_connect: initial connect failed, will retry', err);
+            if (this._disconnected_state) break;
+            await P.delay(3000);
         }
     }
 }

98-105: _bind(): consider not unbinding on bind failure for shared clients

Unbinding on failed admin bind tears down the client; reconnect handles lifecycle anyway. Optional.

If bind fails routinely (bad creds), consider omitting unbind or recreating the client after unbind to avoid using a closed socket.

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 377a6d5 and b0875ab.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (12)
  • config.js (1 hunks)
  • docs/design/ldap.md (1 hunks)
  • package.json (1 hunks)
  • src/endpoint/endpoint.js (5 hunks)
  • src/endpoint/sts/ops/sts_post_assume_role.js (2 hunks)
  • src/endpoint/sts/ops/sts_post_assume_role_with_web_identity.js (1 hunks)
  • src/endpoint/sts/sts_errors.js (2 hunks)
  • src/endpoint/sts/sts_rest.js (1 hunks)
  • src/endpoint/sts/sts_utils.js (1 hunks)
  • src/sdk/sts_sdk.js (5 hunks)
  • src/test/integration_tests/api/sts/test_sts.js (3 hunks)
  • src/util/ldap_client.js (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/endpoint/sts/sts_rest.js
  • src/endpoint/sts/ops/sts_post_assume_role_with_web_identity.js
  • src/endpoint/sts/sts_utils.js
  • src/test/integration_tests/api/sts/test_sts.js
  • src/endpoint/endpoint.js
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-06-17T12:59:51.543Z
Learnt from: naveenpaul1
PR: noobaa/noobaa-core#9042
File: src/util/cloud_utils.js:183-194
Timestamp: 2025-06-17T12:59:51.543Z
Learning: In the set_noobaa_s3_connection function in src/util/cloud_utils.js, internal NooBaa S3 endpoints (filtered by 'api': 's3', 'kind': 'INTERNAL') will always be non-secure, so tls: false is the correct setting despite the conditional https:// scheme logic.

Applied to files:

  • src/util/ldap_client.js
🧬 Code Graph Analysis (3)
src/sdk/sts_sdk.js (2)
src/sdk/object_sdk.js (3)
  • require (27-27)
  • account_cache (50-63)
  • dbg (9-9)
src/util/ldap_client.js (1)
  • dbg (10-10)
src/endpoint/sts/sts_errors.js (1)
src/endpoint/sts/sts_rest.js (1)
  • StsError (6-6)
src/endpoint/sts/ops/sts_post_assume_role.js (2)
src/endpoint/sts/ops/sts_post_assume_role_with_web_identity.js (5)
  • sts_utils (8-8)
  • require (5-5)
  • require (6-6)
  • dbg (4-4)
  • duration_ms (15-15)
src/endpoint/sts/sts_utils.js (2)
  • dbg (4-4)
  • duration_ms (43-43)
🪛 LanguageTool
docs/design/ldap.md

[grammar] ~9-~9: Ensure spelling is correct
Context: ...ow: 1. The client sends a signed(with a predfined constant signature) or unsigned JWT as ...

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)


[grammar] ~9-~9: There might be a mistake here.
Context: ... unsigned JWT as the web identity token. 2. The system parses the JWT to extract the...

(QB_NEW_EN)


[grammar] ~16-~16: There might be a mistake here.
Context: ...users. ## Configuring the external LDAP The administrator must store the LDAP co...

(QB_NEW_EN)


[grammar] ~17-~17: There might be a mistake here.
Context: ...DAP configuration in the following file: /etc/noobaa-server/ldap_config The conf...

(QB_NEW_EN)


[grammar] ~41-~41: There might be a mistake here.
Context: ... set - unsigned tokens or tokens signed with different secret will be dropped as acc...

(QB_NEW_EN)


[grammar] ~65-~65: There might be a mistake here.
Context: ...Password", "type": "ldap" } Sign it node.js example:js const jwt = require('...

(QB_NEW_EN)


[grammar] ~95-~95: There might be a mistake here.
Context: ... can base on dockers I used for testing: * AD image: `docker run --rm --privileged ...

(QB_NEW_EN)


[grammar] ~96-~96: There might be a mistake here.
Context: ...com/samba-in-kubernetes/samba-container) * LDAP image: `docker run --rm --privilege...

(QB_NEW_EN)


[grammar] ~97-~97: There might be a mistake here.
Context: ...ntainer/docker-test-openldap%2Fopenldap) 3. Add support to the operator side: * CLI...

(QB_NEW_EN)


[grammar] ~98-~98: There might be a mistake here.
Context: ...ap) 3. Add support to the operator side: * CLI command for configuring external LDA...

(QB_NEW_EN)


[grammar] ~99-~99: There might be a mistake here.
Context: ...LI command for configuring external LDAP * Create K8s Secret for LDAP info and moun...

(QB_NEW_EN)


[grammar] ~100-~100: There might be a mistake here.
Context: ...-server/ldap_config to the relevant pods 4. Better align and adapt to the IAM effort...

(QB_NEW_EN)


[grammar] ~101-~101: There might be a mistake here.
Context: ...dapt to the IAM effort also in POC stage 5. See if we want to support encrypted pass...

(QB_NEW_EN)


[grammar] ~105-~105: There might be a mistake here.
Context: ...ation scope if possible ## Appendix A: Creating account w/ role config using NooBaa API...

(QB_NEW_EN)


[style] ~131-~131: Consider a more concise word here.
Context: ...auth_token": "'$(cat .nbtoken)'" }' in order to assume this role:bash aws sts assum...

(IN_ORDER_TO_PREMIUM)

🪛 markdownlint-cli2 (0.17.2)
docs/design/ldap.md

90-90: Bare URL used

(MD034, no-bare-urls)


94-94: Bare URL used

(MD034, no-bare-urls)


96-96: Bare URL used

(MD034, no-bare-urls)


97-97: Bare URL used

(MD034, no-bare-urls)


102-102: Bare URL used

(MD034, no-bare-urls)


105-105: Trailing punctuation in heading
Punctuation: ':'

(MD026, no-trailing-punctuation)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: ceph-s3-tests / Ceph S3 Tests
🔇 Additional comments (6)
package.json (1)

103-103: Confirm Node version, vulnerabilities, and pkg bundling for ldapts 7.3.1

  • package.json has no engines field, but [email protected] requires Node >=18. Ensure your deployment/runtime is on Node 18+ or add an appropriate engines entry.
  • No advisories surfaced in the npm registry lookup. It’s still worth running npm audit (or your SCA tool) once ldapts is installed to catch any transitive issues.
  • Our code only does a static import (require('ldapts') in src/util/ldap_client.js:9), so pkg should bundle it without dynamic-import pitfalls.
src/endpoint/sts/sts_errors.js (1)

29-37: No legacy consumers or tests for old error XML shape found

We searched the entire codebase (including src/test) for references to the previous top-level <Error> or <RequestId> tags and for any consumers parsing an unwrapped error envelope. No matches were found outside the updated STS and IAM error-response files. The new ErrorResponse wrapper will not break any existing tests or clients.

src/endpoint/sts/ops/sts_post_assume_role.js (1)

43-48: LGTM: centralized session token generation

Switching to sts_utils.generate_session_token with explicit duration seconds keeps session construction centralized and consistent across STS ops.

src/sdk/sts_sdk.js (1)

148-153: LGTM: allow anonymous op for AssumeRoleWithWebIdentity

Permitting the web-identity path without prior account auth makes sense for STS semantics.

src/util/ldap_client.js (2)

7-7: No action needed: config import path is correct

The repository root contains config.js (no src/config.js exists), so from src/util/ldap_client.js, require('../../config') correctly resolves to the root config.js.


109-116: EqualityFilter is exported from the root of [email protected] – no import changes needed

We’ve confirmed that in ldapts 7.3.1 the CJS entry point (dist/index.cjs) re-exports EqualityFilter, so require('ldapts').EqualityFilter works as-is. You can leave the code unchanged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants