You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ranger.apache.org by ra...@apache.org on 2026/09/11 08:56:15 UTC

(ranger) 25/34: RANGER-5766: Add support for SPIFFE Ids as usernames in Ranger (#1197)

This is an automated email from the ASF dual-hosted git repository.

ramackri pushed a commit to branch backport-audit-spiffe-2.10
in repository https://gitbox.apache.org/repos/asf/ranger.git

commit fc4ae50aa6db6e8a2e53ef2115996ea4cf44765d
Author: Abhishek Kumar <ab...@apache.org>
AuthorDate: Mon Aug 31 14:41:31 2026 -0700

    RANGER-5766: Add support for SPIFFE Ids as usernames in Ranger (#1197)
    
    (cherry picked from commit b222989f186a9318f82fd86654d772b6f3662026)
---
 .../apache/ranger/plugin/util/SpiffeIdUtil.java    |  48 +-
 .../ranger/plugin/util/SpiffeIdUtilTest.java       |  80 +--
 pdp/conf.dist/ranger-pdp-site.xml                  |   2 +-
 .../pdp/security/HttpHeaderAuthNHandler.java       |   9 +-
 pdp/src/main/resources/ranger-pdp-default.xml      |   2 +-
 .../pdp/security/HttpHeaderAuthNHandlerTest.java   |  28 +-
 .../java/org/apache/ranger/common/StringUtil.java  | 477 ++++++++--------
 .../main/java/org/apache/ranger/rest/UserREST.java | 606 ++++++++++-----------
 .../web/filter/RangerHeaderPreAuthFilter.java      |  13 +-
 .../main/resources/conf.dist/ranger-admin-site.xml |   7 +-
 security-admin/src/main/webapp/login.jsp           |   4 +
 .../main/webapp/react-webapp/src/utils/XAEnums.js  | 117 +++-
 .../webapp/react-webapp/src/utils/XAMessages.js    |  32 +-
 .../users_details/UserForm.jsx                     |  71 ++-
 .../src/main/webapp/scripts/prelogin/XAPrelogin.js |   6 +-
 .../org/apache/ranger/common/TestStringUtil.java   | 362 ++++++------
 .../web/filter/TestRangerHeaderPreAuthFilter.java  |  48 +-
 17 files changed, 1055 insertions(+), 857 deletions(-)

diff --git a/common-utils/src/main/java/org/apache/ranger/plugin/util/SpiffeIdUtil.java b/common-utils/src/main/java/org/apache/ranger/plugin/util/SpiffeIdUtil.java
index 5c303364b..418fe1388 100644
--- a/common-utils/src/main/java/org/apache/ranger/plugin/util/SpiffeIdUtil.java
+++ b/common-utils/src/main/java/org/apache/ranger/plugin/util/SpiffeIdUtil.java
@@ -28,33 +28,34 @@
 import java.util.regex.Pattern;
 
 /**
- * Helper for validating a SPIFFE ID and extracting the service-account identity from it.
+ * Helper for validating a SPIFFE ID that is used as a service identity.
  *
- * <p>A SPIFFE ID is expected in the exact form {@code spiffe://<trust-domain>/ns/<namespace>/sa/<service-account>};
- * for service-to-service authentication the receiving server trusts the workload
- * identity carried in a header (e.g. {@code x-service-spiffe-id}) and uses
- * the trailing service-account segment as the authenticated principal.
+ * <p>A SPIFFE ID is expected in the general form defined by the SPIFFE-ID specification,
+ * {@code spiffe://<trust-domain>/<path>}, where {@code <path>} is one or more {@code /}-separated
+ * segments. For service-to-service authentication the receiving server trusts the workload identity
+ * carried in a header (e.g. {@code x-service-spiffe-id}) and uses the SPIFFE ID itself as the
+ * authenticated principal. No particular path layout (such as the SPIRE {@code /ns/<namespace>/sa/<service-account>}
+ * convention) is required.
  */
 public final class SpiffeIdUtil {
     private static final String  HEADER_NAMES_SEP  = ",";
 
     /**
-     * Matches {@code spiffe://<trust-domain>/ns/<namespace>/sa/<service-account>} using the character
-     * sets from the SPIFFE-ID specification:
+     * Matches {@code spiffe://<trust-domain>/<path>} using the character sets from the SPIFFE-ID
+     * specification:
      * <ul>
      *   <li>trust-domain: lowercase letters, digits, {@code .}, {@code -}, {@code _} (the spec requires
      *       the trust domain to be lowercase);
-     *   <li>namespace and service-account: ASCII letters (any case), digits, {@code .}, {@code -}, {@code _}
-     *       (SPIFFE path segments are case-sensitive).
+     *   <li>path: one or more {@code /}-separated segments of ASCII letters (any case), digits,
+     *       {@code .}, {@code -}, {@code _} (SPIFFE path segments are case-sensitive).
      * </ul>
-     * All segments must be non-empty. This is an allow-list of safe characters, so whitespace, control
-     * characters and {@code /} are excluded, keeping the extracted principal well-formed. Per the spec,
-     * the namespace and service-account path segments must not be the relative modifiers {@code .} or
-     * {@code ..} (enforced by the leading negative look-aheads). The service-account is exposed via the
-     * named group {@code sa}.
+     * At least one path segment is required and every segment must be non-empty. This is an allow-list
+     * of safe characters, so whitespace and control characters are excluded, keeping the principal
+     * well-formed. Per the spec, no path segment may be the relative modifier {@code .} or {@code ..}
+     * (enforced by the per-segment negative look-ahead).
      */
     private static final Pattern SPIFFE_ID_PATTERN = Pattern.compile(
-            "^spiffe://[a-z0-9._-]+/ns/(?!\\.{1,2}/)[A-Za-z0-9._-]+/sa/(?<sa>(?!\\.{1,2}$)[A-Za-z0-9._-]+)$");
+            "^spiffe://[a-z0-9._-]+(?:/(?!\\.{1,2}(?:/|$))[A-Za-z0-9._-]+)+$");
 
     private SpiffeIdUtil() {
         // to block instantiation
@@ -96,26 +97,11 @@ public static boolean isValidSpiffeId(String spiffeId) {
         return matchSpiffeId(spiffeId) != null;
     }
 
-    /**
-     * Extracts the service-account name from a SPIFFE ID such as
-     * {@code spiffe://my-cluster/ns/service-namespace/sa/service-sa}. The value must match the exact
-     * SPIFFE ID format (see {@link #isValidSpiffeId(String)}); otherwise {@code null} is returned.
-     *
-     * @param spiffeId the raw SPIFFE ID value from the request header
-     * @return the service-account segment (e.g. {@code service-sa}), or
-     *         {@code null} if the input is blank or not a well-formed SPIFFE ID
-     */
-    public static String extractServiceAccount(String spiffeId) {
-        Matcher matcher = matchSpiffeId(spiffeId);
-
-        return matcher != null ? matcher.group("sa") : null;
-    }
-
     /**
      * Matches the given value against the expected SPIFFE ID format, computing the {@link Matcher} once.
      *
      * @param spiffeId the raw SPIFFE ID value from the request header
-     * @return a matched {@link Matcher} (with groups available) if the value is a well-formed SPIFFE ID;
+     * @return a matched {@link Matcher} if the value is a well-formed SPIFFE ID;
      *         {@code null} if the value is blank or does not match
      */
     private static Matcher matchSpiffeId(String spiffeId) {
diff --git a/common-utils/src/test/java/org/apache/ranger/plugin/util/SpiffeIdUtilTest.java b/common-utils/src/test/java/org/apache/ranger/plugin/util/SpiffeIdUtilTest.java
index c9e76eb56..bb1ad4fb8 100644
--- a/common-utils/src/test/java/org/apache/ranger/plugin/util/SpiffeIdUtilTest.java
+++ b/common-utils/src/test/java/org/apache/ranger/plugin/util/SpiffeIdUtilTest.java
@@ -25,7 +25,6 @@
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 class SpiffeIdUtilTest {
@@ -44,14 +43,12 @@ void isValidSpiffeId_rejectsMalformed() {
         assertFalse(SpiffeIdUtil.isValidSpiffeId("   "));
         assertFalse(SpiffeIdUtil.isValidSpiffeId("not-a-spiffe-id"));
         assertFalse(SpiffeIdUtil.isValidSpiffeId("https://my-cluster/ns/n/sa/s"));   // wrong scheme
-        assertFalse(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster/sa/service-sa")); // missing /ns/
-        assertFalse(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster/ns/n"));       // missing /sa/
-        assertFalse(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster/ns//sa/s"));   // empty namespace
+        assertFalse(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster"));            // trust domain only, no path
+        assertFalse(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster/"));           // trailing slash, empty path
+        assertFalse(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster/ns//sa/s"));   // empty path segment
         assertFalse(SpiffeIdUtil.isValidSpiffeId("spiffe:///ns/n/sa/s"));            // empty trust domain
-        assertFalse(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster/ns/n/sa/"));   // empty service account
+        assertFalse(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster/ns/n/sa/"));   // empty trailing segment
         assertFalse(SpiffeIdUtil.isValidSpiffeId(VALID + "/"));                      // trailing slash
-        assertFalse(SpiffeIdUtil.isValidSpiffeId(VALID + "/extra"));                 // extra segment
-        assertFalse(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster/sa/s/ns/n"));  // wrong order
     }
 
     @Test
@@ -61,9 +58,6 @@ void isValidSpiffeId_rejectsUnsafeOrIllegalCharacters() {
         assertFalse(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster/ns/prod/sa/service\nsa"));  // embedded newline
         assertFalse(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster/ns/prod/sa/svc@admin"));    // illegal char '@'
         assertFalse(SpiffeIdUtil.isValidSpiffeId("spiffe://My-Cluster/ns/prod/sa/service-sa"));   // trust domain must be lowercase per spec
-
-        assertNull(SpiffeIdUtil.extractServiceAccount("spiffe://my-cluster/ns/prod/sa/service sa"));
-        assertNull(SpiffeIdUtil.extractServiceAccount("spiffe://my-cluster/ns/prod/sa/service\nsa"));
     }
 
     @Test
@@ -74,73 +68,47 @@ void isValidSpiffeId_rejectsRelativePathModifierSegments() {
         assertFalse(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster/ns/prod/sa/."));         // service-account '.'
         assertFalse(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster/ns/prod/sa/.."));        // service-account '..'
 
-        assertNull(SpiffeIdUtil.extractServiceAccount("spiffe://my-cluster/ns/prod/sa/."));
-        assertNull(SpiffeIdUtil.extractServiceAccount("spiffe://my-cluster/ns/../sa/service-sa"));
-
         // Dots inside a segment (not a bare '.'/'..') remain valid.
         assertTrue(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster/ns/prod/sa/svc.v1"));
-        assertEquals("svc.v1", SpiffeIdUtil.extractServiceAccount("spiffe://my-cluster/ns/..prod/sa/svc.v1"));
+        assertTrue(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster/ns/..prod/sa/svc.v1"));
     }
 
     @Test
     void isValidSpiffeId_acceptsSpecAllowedCharacters() {
         // SPIFFE path segments are case-sensitive and allow letters (any case), digits, '.', '-', '_'.
         assertTrue(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster.example.com/ns/service-namespace/sa/service-sa"));
-        assertEquals("Service-SA", SpiffeIdUtil.extractServiceAccount("spiffe://my-cluster/ns/Prod/sa/Service-SA")); // mixed case
-        assertEquals("svc_01", SpiffeIdUtil.extractServiceAccount("spiffe://cluster1/ns/ns_2/sa/svc_01"));           // underscore
-        assertEquals("svc-01", SpiffeIdUtil.extractServiceAccount("spiffe://cluster1/ns/ns-2/sa/svc-01"));           // hyphen/digits
+        assertTrue(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster/ns/Prod/sa/Service-SA")); // mixed case
+        assertTrue(SpiffeIdUtil.isValidSpiffeId("spiffe://cluster1/ns/ns_2/sa/svc_01"));       // underscore
+        assertTrue(SpiffeIdUtil.isValidSpiffeId("spiffe://cluster1/ns/ns-2/sa/svc-01"));       // hyphen/digits
     }
 
     @Test
-    void extractServiceAccount_returnsSaOnlyForValidId() {
-        assertEquals("service-sa", SpiffeIdUtil.extractServiceAccount(VALID));
-        assertEquals("service-sa", SpiffeIdUtil.extractServiceAccount("  " + VALID + "  "));
-    }
-
-    @Test
-    void extractServiceAccount_returnsNullForMalformedId() {
-        assertNull(SpiffeIdUtil.extractServiceAccount(null));
-        assertNull(SpiffeIdUtil.extractServiceAccount(""));
-        assertNull(SpiffeIdUtil.extractServiceAccount("not-a-spiffe-id"));
-        assertNull(SpiffeIdUtil.extractServiceAccount("spiffe://my-cluster/sa/service-sa"));
-        assertNull(SpiffeIdUtil.extractServiceAccount("spiffe://my-cluster/ns/n/sa/"));
-        assertNull(SpiffeIdUtil.extractServiceAccount(VALID + "/extra"));
-    }
-
-    @Test
-    void extractServiceAccount_handlesRealisticProductionIds() {
+    void isValidSpiffeId_handlesRealisticProductionIds() {
         // Kubernetes trust domain with a DNS-style cluster name and typical namespace/service-account names.
-        assertEquals("nginx-ingress",
-                SpiffeIdUtil.extractServiceAccount("spiffe://prod-cluster.k8s.example.com/ns/ingress-nginx/sa/nginx-ingress"));
+        assertTrue(SpiffeIdUtil.isValidSpiffeId("spiffe://prod-cluster.k8s.example.com/ns/ingress-nginx/sa/nginx-ingress"));
 
         // Cloud/mesh style trust domain (e.g. the header value from the PR description).
-        assertEquals("service-sa",
-                SpiffeIdUtil.extractServiceAccount("spiffe://my-cluster/ns/service-namespace/sa/service-sa"));
+        assertTrue(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster/ns/service-namespace/sa/service-sa"));
 
         // Istio-style default service account in an application namespace.
-        assertEquals("default",
-                SpiffeIdUtil.extractServiceAccount("spiffe://cluster.local/ns/payments/sa/default"));
+        assertTrue(SpiffeIdUtil.isValidSpiffeId("spiffe://cluster.local/ns/payments/sa/default"));
 
         // Namespace and service-account with digits and hyphens, as commonly generated by platforms.
-        assertEquals("spark-driver-01",
-                SpiffeIdUtil.extractServiceAccount("spiffe://data-plane.internal/ns/team-analytics-42/sa/spark-driver-01"));
-
-        assertTrue(SpiffeIdUtil.isValidSpiffeId("spiffe://prod-cluster.k8s.example.com/ns/ingress-nginx/sa/nginx-ingress"));
-        assertTrue(SpiffeIdUtil.isValidSpiffeId("spiffe://cluster.local/ns/payments/sa/default"));
+        assertTrue(SpiffeIdUtil.isValidSpiffeId("spiffe://data-plane.internal/ns/team-analytics-42/sa/spark-driver-01"));
     }
 
     @Test
-    void rejectsSpecValidButNonConformingSpiffeId() {
-        // These are well-formed SPIFFE IDs per the SPIFFE spec (valid scheme + trust domain + path),
-        // but they do not follow the expected spiffe://<trust-domain>/ns/<namespace>/sa/<service-account>
-        // layout, so Ranger must reject them.
-        String workloadPath = "spiffe://example.org/workload/frontend"; // arbitrary path, no /ns//sa/
-        String nsOnly       = "spiffe://example.org/ns/prod";           // has /ns/ but no /sa/ segment
-
-        assertFalse(SpiffeIdUtil.isValidSpiffeId(workloadPath));
-        assertFalse(SpiffeIdUtil.isValidSpiffeId(nsOnly));
-        assertNull(SpiffeIdUtil.extractServiceAccount(workloadPath));
-        assertNull(SpiffeIdUtil.extractServiceAccount(nsOnly));
+    void acceptsSpecValidNonSpireLayoutSpiffeId() {
+        // Well-formed SPIFFE IDs per the spec (valid scheme + trust domain + one or more path segments)
+        // that do not follow the SPIRE spiffe://<trust-domain>/ns/<namespace>/sa/<service-account>
+        // convention. Since the full SPIFFE ID is used as the principal, these must be accepted.
+        assertTrue(SpiffeIdUtil.isValidSpiffeId("spiffe://example.org/workload/frontend")); // arbitrary path
+        assertTrue(SpiffeIdUtil.isValidSpiffeId("spiffe://example.org/ns/prod"));           // /ns/ but no /sa/
+        assertTrue(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster/sa/service-sa"));      // /sa/ but no /ns/
+        assertTrue(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster/ns/n"));               // single-segment style
+        assertTrue(SpiffeIdUtil.isValidSpiffeId("spiffe://example.org/single"));            // single path segment
+        assertTrue(SpiffeIdUtil.isValidSpiffeId("spiffe://my-cluster/sa/s/ns/n"));          // segments in any order
+        assertTrue(SpiffeIdUtil.isValidSpiffeId(VALID + "/extra"));                         // additional trailing segment
     }
 
     @Test
diff --git a/pdp/conf.dist/ranger-pdp-site.xml b/pdp/conf.dist/ranger-pdp-site.xml
index 9d67b8eab..6f3152f41 100644
--- a/pdp/conf.dist/ranger-pdp-site.xml
+++ b/pdp/conf.dist/ranger-pdp-site.xml
@@ -149,7 +149,7 @@
   <property>
     <name>ranger.pdp.authn.header.spiffe</name>
     <value></value>
-    <description>Comma-separated list of HTTP header name(s) carrying a SPIFFE ID (e.g. spiffe://cluster/ns/&lt;ns&gt;/sa/&lt;service-account&gt;). A single header name is also valid. The trailing service-account segment is used as the authenticated principal. When multiple headers are configured, the first one carrying a well-formed SPIFFE ID is used. Consulted only when the username header is absent.</description>
+    <description>Comma-separated list of HTTP header name(s) carrying a SPIFFE ID (e.g. spiffe://cluster/ns/&lt;ns&gt;/sa/&lt;service-account&gt;). A single header name is also valid. The full SPIFFE ID is used as the authenticated principal. When multiple headers are configured, the first one carrying a well-formed SPIFFE ID is used. Consulted only when the username header is absent.</description>
   </property>
 
   <!-- JWT bearer token authentication for incoming REST requests -->
diff --git a/pdp/src/main/java/org/apache/ranger/pdp/security/HttpHeaderAuthNHandler.java b/pdp/src/main/java/org/apache/ranger/pdp/security/HttpHeaderAuthNHandler.java
index 1761a0938..a6ff8e147 100644
--- a/pdp/src/main/java/org/apache/ranger/pdp/security/HttpHeaderAuthNHandler.java
+++ b/pdp/src/main/java/org/apache/ranger/pdp/security/HttpHeaderAuthNHandler.java
@@ -71,13 +71,12 @@ public Result authenticate(HttpServletRequest request, HttpServletResponse respo
         }
 
         for (String spiffeHeader : spiffeHeaders) {
-            String spiffeId       = request.getHeader(spiffeHeader);
-            String serviceAccount = SpiffeIdUtil.extractServiceAccount(spiffeId);
+            String spiffeId = StringUtils.trimToNull(request.getHeader(spiffeHeader));
 
-            if (StringUtils.isNotBlank(serviceAccount)) {
-                LOG.debug("authenticate(): service-account={} (from SPIFFE header {})", serviceAccount, spiffeHeader);
+            if (SpiffeIdUtil.isValidSpiffeId(spiffeId)) {
+                LOG.debug("authenticate(): spiffeId={} (from SPIFFE header {})", spiffeId, spiffeHeader);
 
-                return Result.authenticated(serviceAccount, AUTH_TYPE_SPIFFE);
+                return Result.authenticated(spiffeId, AUTH_TYPE_SPIFFE);
             } else if (StringUtils.isNotBlank(spiffeId)) {
                 LOG.warn("SPIFFE header '{}' value is not a well-formed SPIFFE ID", spiffeHeader);
             }
diff --git a/pdp/src/main/resources/ranger-pdp-default.xml b/pdp/src/main/resources/ranger-pdp-default.xml
index 9d67b8eab..6f3152f41 100644
--- a/pdp/src/main/resources/ranger-pdp-default.xml
+++ b/pdp/src/main/resources/ranger-pdp-default.xml
@@ -149,7 +149,7 @@
   <property>
     <name>ranger.pdp.authn.header.spiffe</name>
     <value></value>
-    <description>Comma-separated list of HTTP header name(s) carrying a SPIFFE ID (e.g. spiffe://cluster/ns/&lt;ns&gt;/sa/&lt;service-account&gt;). A single header name is also valid. The trailing service-account segment is used as the authenticated principal. When multiple headers are configured, the first one carrying a well-formed SPIFFE ID is used. Consulted only when the username header is absent.</description>
+    <description>Comma-separated list of HTTP header name(s) carrying a SPIFFE ID (e.g. spiffe://cluster/ns/&lt;ns&gt;/sa/&lt;service-account&gt;). A single header name is also valid. The full SPIFFE ID is used as the authenticated principal. When multiple headers are configured, the first one carrying a well-formed SPIFFE ID is used. Consulted only when the username header is absent.</description>
   </property>
 
   <!-- JWT bearer token authentication for incoming REST requests -->
diff --git a/pdp/src/test/java/org/apache/ranger/pdp/security/HttpHeaderAuthNHandlerTest.java b/pdp/src/test/java/org/apache/ranger/pdp/security/HttpHeaderAuthNHandlerTest.java
index e122516bd..d48cb3e90 100644
--- a/pdp/src/test/java/org/apache/ranger/pdp/security/HttpHeaderAuthNHandlerTest.java
+++ b/pdp/src/test/java/org/apache/ranger/pdp/security/HttpHeaderAuthNHandlerTest.java
@@ -77,11 +77,12 @@ void testAuthenticate_usesSpiffeHeaderWhenUsernameAbsent() {
         handler.init(config);
 
         // Realistic production SPIFFE ID: DNS-style Kubernetes cluster trust domain + namespace/service-account.
-        HttpServletRequest     request = requestWithHeader("X-Spiffe-Id", "spiffe://prod-cluster.k8s.example.com/ns/ingress-nginx/sa/nginx-ingress");
-        PdpAuthNHandler.Result result  = handler.authenticate(request, null);
+        String                 spiffeId = "spiffe://prod-cluster.k8s.example.com/ns/ingress-nginx/sa/nginx-ingress";
+        HttpServletRequest     request  = requestWithHeader("X-Spiffe-Id", spiffeId);
+        PdpAuthNHandler.Result result   = handler.authenticate(request, null);
 
         assertEquals(PdpAuthNHandler.Result.Status.AUTHENTICATED, result.getStatus());
-        assertEquals("nginx-ingress", result.getUserName());
+        assertEquals(spiffeId, result.getUserName());
         assertEquals(HttpHeaderAuthNHandler.AUTH_TYPE_SPIFFE, result.getAuthType());
     }
 
@@ -118,13 +119,15 @@ void testAuthenticate_multipleSpiffeHeadersUsesFirstValid() {
 
         Map<String, String> headers = new HashMap<>();
 
+        String spiffeId = "spiffe://my-cluster/ns/service-namespace/sa/service-sa";
+
         headers.put("X-Spiffe-Id", "not-a-spiffe-id");
-        headers.put("X-Workload-Id", "spiffe://my-cluster/ns/service-namespace/sa/service-sa");
+        headers.put("X-Workload-Id", spiffeId);
 
         PdpAuthNHandler.Result result = handler.authenticate(requestWithHeaders(headers), null);
 
         assertEquals(PdpAuthNHandler.Result.Status.AUTHENTICATED, result.getStatus());
-        assertEquals("service-sa", result.getUserName());
+        assertEquals(spiffeId, result.getUserName());
         assertEquals(HttpHeaderAuthNHandler.AUTH_TYPE_SPIFFE, result.getAuthType());
     }
 
@@ -145,7 +148,7 @@ void testAuthenticate_malformedSpiffeHeaderSkips() {
     }
 
     @Test
-    void testAuthenticate_specValidButNonConformingSpiffeHeaderSkips() {
+    void testAuthenticate_specValidNonSpireLayoutSpiffeHeaderAuthenticatesFullId() {
         HttpHeaderAuthNHandler handler = new HttpHeaderAuthNHandler();
         Properties             config  = new Properties();
 
@@ -153,12 +156,15 @@ void testAuthenticate_specValidButNonConformingSpiffeHeaderSkips() {
 
         handler.init(config);
 
-        // Valid SPIFFE ID per the SPIFFE spec, but not in the expected /ns/<ns>/sa/<sa> layout.
-        HttpServletRequest     request = requestWithHeader("X-Spiffe-Id", "spiffe://example.org/workload/frontend");
-        PdpAuthNHandler.Result result  = handler.authenticate(request, null);
+        // Valid SPIFFE ID per the SPIFFE spec that does not use the SPIRE /ns/<ns>/sa/<sa> layout;
+        // the full SPIFFE ID is used as the authenticated principal.
+        String                 spiffeId = "spiffe://example.org/workload/frontend";
+        HttpServletRequest     request  = requestWithHeader("X-Spiffe-Id", spiffeId);
+        PdpAuthNHandler.Result result   = handler.authenticate(request, null);
 
-        assertEquals(PdpAuthNHandler.Result.Status.SKIP, result.getStatus());
-        assertNull(result.getUserName());
+        assertEquals(PdpAuthNHandler.Result.Status.AUTHENTICATED, result.getStatus());
+        assertEquals(spiffeId, result.getUserName());
+        assertEquals(HttpHeaderAuthNHandler.AUTH_TYPE_SPIFFE, result.getAuthType());
     }
 
     @Test
diff --git a/security-admin/src/main/java/org/apache/ranger/common/StringUtil.java b/security-admin/src/main/java/org/apache/ranger/common/StringUtil.java
index 275c883c5..87e100c3b 100644
--- a/security-admin/src/main/java/org/apache/ranger/common/StringUtil.java
+++ b/security-admin/src/main/java/org/apache/ranger/common/StringUtil.java
@@ -17,7 +17,11 @@
  * under the License.
  */
 
- package org.apache.ranger.common;
+package org.apache.ranger.common;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
 
 import java.io.Serializable;
 import java.io.UnsupportedEncodingException;
@@ -28,234 +32,267 @@
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.stereotype.Component;
-
 @Component
 public class StringUtil implements Serializable {
-	private static final Logger logger = LoggerFactory.getLogger(StringUtil.class);
+    private static final Logger logger = LoggerFactory.getLogger(StringUtil.class);
 
-	static final public String VALIDATION_CRED = "(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z]).{8,}";
+    private static final long serialVersionUID = -2102399594424760213L;
 
-	static final public String VALIDATION_NAME = "^([A-Za-z0-9_]|[\u00C0-\u017F])([a-zA-Z0-9\\s_. -@]|[\u00C0-\u017F])+$";
-	static final public String VALIDATION_TEXT = "[a-zA-Z0-9\\ \"!@#$%^&amp;*()-_=+;:'&quot;|~`&lt;&gt;?/{}\\.\\,\\-\\?<>\\x00-\\x7F\\p{L}-]*";
-	static final public String VALIDATION_LOGINID = "^([A-Za-z0-9_]|[\u00C0-\u017F])([a-z0-9,._\\-+/@= ]|[\u00C0-\u017F])+$";
+    public static final String VALIDATION_CRED       = "(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z]).{8,}";
+    public static final String VALIDATION_NAME       = "^([A-Za-z0-9_]|[\u00C0-\u017F])([a-zA-Z0-9\\s_. -@]|[\u00C0-\u017F])+$";
+    public static final String VALIDATION_TEXT       = "[a-zA-Z0-9\\ \"!@#$%^&amp;*()-_=+;:'&quot;|~`&lt;&gt;?/{}\\.\\,\\-\\?<>\\x00-\\x7F\\p{L}-]*";
+    public static final String VALIDATION_LOGINID    = "^([A-Za-z0-9_]|[\u00C0-\u017F])([a-z0-9,._\\-+/@= ]|[\u00C0-\u017F])+$";
+    public static final String VALIDATION_ALPHA      = "[a-z,A-Z]*";
+    public static final String VALIDATION_IP_ADDRESS = "[\\d\\.\\%\\:]*";
+    public static final String WILDCARD_ASTERISK     = "*";
 
-	static final public String VALIDATION_ALPHA = "[a-z,A-Z]*";
-	static final public String VALIDATION_IP_ADDRESS = "[\\d\\.\\%\\:]*";
-	static final public String WILDCARD_ASTERISK = "*";
+    // When enabled, ':' is treated as a valid login-id character so that SPIFFE IDs (e.g. spiffe://spiffe.example.com/ns/sales/sa/trino) can be used as usernames.
+    public static final String PROP_SPIFFE_AS_USERNAME_ENABLED = "ranger.admin.spiffe.as.username.enabled";
+    public static final String VALIDATION_LOGINID_SPIFFE       = "^([A-Za-z0-9_]|[\u00C0-\u017F])([a-z0-9,._\\-+/@=: ]|[\u00C0-\u017F])+$";
 
-	static HashMap<String, Pattern> compiledRegEx = new HashMap<String, Pattern>();
+    /**
+     *
+     */
+    static HashMap<String, Pattern> compiledRegEx = new HashMap<>();
+
+    String[] invalidNames;
+
+    public StringUtil() {
+        // Default constructor
+        invalidNames = PropertiesUtil.getPropertyStringList("xa.names.invalid");
+    }
 
-	String[] invalidNames = null;
+    public static String trim(String str) {
+        return str != null ? str.trim() : null;
+    }
 
-	/**
+    public static String getUTFEncodedString(String username) throws UnsupportedEncodingException {
+        return URLEncoder.encode(username, StandardCharsets.UTF_8.toString());
+    }
+
+    /**
+     * Checks if the string is null or empty string.
      *
+     * @param str
+     * @return true if it is empty string or null
      */
-	private static final long serialVersionUID = -2102399594424760213L;
-
-	public StringUtil() {
-		// Default constructor
-		invalidNames = PropertiesUtil.getPropertyStringList("xa.names.invalid");
-	}
-
-	/**
-	 * Checks if the string is null or empty string.
-	 *
-	 * @param str
-	 * @return true if it is empty string or null
-	 */
-	public boolean isEmpty(String str) {
-		if (str == null || str.trim().length() == 0) {
-			return true;
-		}
-		return false;
-	}
-
-	public boolean isEmptyOrWildcardAsterisk(String str) {
-		return isEmpty(str) || WILDCARD_ASTERISK.equals(str);
-	}
-
-	public boolean equals(String str1, String str2) {
-		if (str1 == str2) {
-			return true;
-		}
-
-		if (str1 == null || str2 == null) {
-			return false;
-		}
-
-		return str1.equals(str2);
-	}
-
-	public String toCamelCaseAllWords(String str) {
-		if (str == null) {
-			return null;
-		}
-		str = str.trim().toLowerCase();
-		StringBuilder result = new StringBuilder(str.length());
-		boolean makeUpper = true;
-		boolean lastCharSpace = true;
-		for (int c = 0; c < str.length(); c++) {
-			char ch = str.charAt(c);
-			if (lastCharSpace && ch == ' ') {
-				continue;
-			}
-
-			if (makeUpper) {
-				result.append(str.substring(c, c + 1).toUpperCase());
-				makeUpper = false;
-			} else {
-				result.append(ch);
-			}
-			if (ch == ' ') {
-				lastCharSpace = true;
-				makeUpper = true;
-			} else {
-				lastCharSpace = false;
-			}
-
-		}
-		return result.toString();
-	}
-
-	public boolean validatePassword(String password, String[] invalidValues) {
-		// For now let's make sure we have minimum 8 characters
-		if (password == null) {
-			return false;
-		}
-		password = password.trim();
-		boolean checkPassword = password.matches(VALIDATION_CRED);
-		if (!checkPassword) {
-			return false;
-		}
-
-		for (int i = 0; invalidValues != null && i < invalidValues.length; i++) {
-			if (password.equalsIgnoreCase(invalidValues[i])) {
-				return false;
-			}
-		}
-		return true;
-	}
-
-	public boolean validateEmail(String emailAddress) {
-		if (emailAddress == null || emailAddress.trim().length() > 128) {
-			return false;
-		}
-		emailAddress = emailAddress.trim();
-		String expression = "^[\\w]([\\-\\.\\w])+[\\w]+@[\\w]+[\\w\\-]+[\\w]*\\.([\\w]+[\\w\\-]+[\\w]*(\\.[a-z][a-z|0-9]*)?)$";
-		return regExPatternMatch(expression, emailAddress);
-
-	}
-
-	public boolean regExPatternMatch(String expression, String inputStr) {
-		boolean ret = false;
-
-		if (expression != null && inputStr != null) {
-			Pattern pattern = compiledRegEx.get(expression);
-
-			if (pattern == null) {
-				pattern = Pattern.compile(expression, Pattern.CASE_INSENSITIVE);
-				compiledRegEx.put(expression, pattern);
-			}
-
-			Matcher matcher = pattern.matcher(inputStr);
-			ret = matcher.matches();
-		}
-
-		return ret;
-	}
-
-	public boolean validateString(String regExStr, String str) {
-		try {
-			return regExPatternMatch(regExStr, str);
-		} catch (Throwable t) {
-			logger.info("Error validating string. str=" + str, t);
-			return false;
-		}
-	}
-
-	public String normalizeEmail(String email) {
-		// Make email address as lower case
-		if (email != null) {
-			return email.trim().toLowerCase();
-		}
-		return null;
-	}
-
-	public String[] split(String value) {
-		return split(value, ",");
-	}
-
-	public String[] split(String value, String delimiter) {
-		if (value != null) {
-			value = value.startsWith(delimiter) ? value.substring(1) : value;
-			String[] splitValues = value.split(delimiter);
-			String[] returnValues = new String[splitValues.length];
-			int c = -1;
-			for (String splitValue : splitValues) {
-				String str = splitValue.trim();
-				if (str.length() > 0) {
-					c++;
-					returnValues[c] = str;
-				}
-			}
-			return returnValues;
-		} else {
-			return new String[0];
-		}
-	}
-
-	public static String trim(String str) {
-		return str != null ? str.trim() : null;
-	}
-
-	/**
-	 * @param name
-	 * @return
-	 */
-	public boolean isValidName(String name) {
-		if (name == null || name.trim().length() < 1) {
-			return false;
-		}
-		for (String invalidName : invalidNames) {
-			if (name.toUpperCase().trim()
-					.startsWith(invalidName.toUpperCase().trim())) {
-				return false;
-			}
-		}
-		return validateString(VALIDATION_NAME, name);
-	}
-
-	/**
-	 * Checks if the list is null or empty list.
-	 *
-	 * @param list
-	 * @return true if it is empty list or null
-	 */
-	public boolean isEmpty(List<?> list) {
-		if (list == null || list.isEmpty()) {
-			return true;
-		}
-		return false;
-	}
-	
-	/**
-	 * Returns a valid user name from the passed string
-	 * @param str
-	 * @return
-	 */
-	public String getValidUserName(String str) {
-		return str.indexOf("/") >= 0 ?
-				 str.substring(0,str.indexOf("/"))
-				:	str.indexOf("@") >= 0 ?
-						str.substring(0,str.indexOf("@"))
-						: str;
-	}
-
-	public static String getUTFEncodedString(String username) throws UnsupportedEncodingException {
-		return URLEncoder.encode(username, StandardCharsets.UTF_8.toString());
-	}
+    public boolean isEmpty(String str) {
+        return str == null || str.trim().isEmpty();
+    }
+
+    public boolean isEmptyOrWildcardAsterisk(String str) {
+        return isEmpty(str) || WILDCARD_ASTERISK.equals(str);
+    }
+
+    public boolean equals(String str1, String str2) {
+        if (str1 == str2) {
+            return true;
+        }
+
+        if (str1 == null || str2 == null) {
+            return false;
+        }
+
+        return str1.equals(str2);
+    }
+
+    public String toCamelCaseAllWords(String str) {
+        if (str == null) {
+            return null;
+        }
+
+        str = str.trim().toLowerCase();
+
+        StringBuilder result        = new StringBuilder(str.length());
+        boolean       makeUpper     = true;
+        boolean       lastCharSpace = true;
+
+        for (int c = 0; c < str.length(); c++) {
+            char ch = str.charAt(c);
+
+            if (lastCharSpace && ch == ' ') {
+                continue;
+            }
+
+            if (makeUpper) {
+                result.append(str.substring(c, c + 1).toUpperCase());
+
+                makeUpper = false;
+            } else {
+                result.append(ch);
+            }
+
+            if (ch == ' ') {
+                lastCharSpace = true;
+                makeUpper     = true;
+            } else {
+                lastCharSpace = false;
+            }
+        }
+
+        return result.toString();
+    }
+
+    public boolean validatePassword(String password, String[] invalidValues) {
+        // For now let's make sure we have minimum 8 characters
+        if (password == null) {
+            return false;
+        }
+
+        password = password.trim();
+
+        boolean checkPassword = password.matches(VALIDATION_CRED);
+
+        if (!checkPassword) {
+            return false;
+        }
+
+        for (int i = 0; invalidValues != null && i < invalidValues.length; i++) {
+            if (password.equalsIgnoreCase(invalidValues[i])) {
+                return false;
+            }
+        }
+
+        return true;
+    }
 
+    public boolean validateEmail(String emailAddress) {
+        if (emailAddress == null || emailAddress.trim().length() > 128) {
+            return false;
+        }
+
+        emailAddress = emailAddress.trim();
+
+        String expression = "^[\\w]([\\-\\.\\w])+[\\w]+@[\\w]+[\\w\\-]+[\\w]*\\.([\\w]+[\\w\\-]+[\\w]*(\\.[a-z][a-z|0-9]*)?)$";
+
+        return regExPatternMatch(expression, emailAddress);
+    }
+
+    public boolean regExPatternMatch(String expression, String inputStr) {
+        boolean ret = false;
+
+        if (expression != null && inputStr != null) {
+            Pattern pattern = compiledRegEx.get(expression);
+
+            if (pattern == null) {
+                pattern = Pattern.compile(expression, Pattern.CASE_INSENSITIVE);
+                compiledRegEx.put(expression, pattern);
+            }
+
+            Matcher matcher = pattern.matcher(inputStr);
+            ret = matcher.matches();
+        }
+
+        return ret;
+    }
+
+    public boolean validateString(String regExStr, String str) {
+        try {
+            return regExPatternMatch(regExStr, str);
+        } catch (Throwable t) {
+            logger.info("Error validating string. str={}", str, t);
+
+            return false;
+        }
+    }
+
+    public String normalizeEmail(String email) {
+        // Make email address as lower case
+        if (email != null) {
+            return email.trim().toLowerCase();
+        }
+
+        return null;
+    }
+
+    public String[] split(String value) {
+        return split(value, ",");
+    }
+
+    public String[] split(String value, String delimiter) {
+        if (value != null) {
+            value = value.startsWith(delimiter) ? value.substring(1) : value;
+
+            String[] splitValues  = value.split(delimiter);
+            String[] returnValues = new String[splitValues.length];
+            int      c            = -1;
+
+            for (String splitValue : splitValues) {
+                String str = splitValue.trim();
+
+                if (!str.isEmpty()) {
+                    c++;
+
+                    returnValues[c] = str;
+                }
+            }
+
+            return returnValues;
+        } else {
+            return new String[0];
+        }
+    }
+
+    /**
+     * @param name
+     * @return
+     */
+    public boolean isValidName(String name) {
+        if (name == null || name.trim().isEmpty()) {
+            return false;
+        }
+
+        for (String invalidName : invalidNames) {
+            if (name.toUpperCase().trim().startsWith(invalidName.toUpperCase().trim())) {
+                return false;
+            }
+        }
+
+        return validateString(VALIDATION_NAME, name);
+    }
+
+    /**
+     * Checks if the list is null or empty list.
+     *
+     * @param list
+     * @return true if it is empty list or null
+     */
+    public boolean isEmpty(List<?> list) {
+        return list == null || list.isEmpty();
+    }
+
+    /**
+     * Returns a valid user name from the passed string
+     *
+     * @param str
+     * @return
+     */
+    public String getValidUserName(String str) {
+        return str.contains("/") ?
+                str.substring(0, str.indexOf("/"))
+                : str.contains("@") ?
+                str.substring(0, str.indexOf("@"))
+                : str;
+    }
+
+    /**
+     * Whether SPIFFE IDs are allowed as usernames. Controlled by the
+     * {@link #PROP_SPIFFE_AS_USERNAME_ENABLED} config (disabled by default). When enabled,
+     * the login-id validation allows the ':' character used in SPIFFE IDs.
+     *
+     * @return true if using SPIFFE IDs as usernames is enabled
+     */
+    public static boolean isSpiffeAsUsernameEnabled() {
+        return PropertiesUtil.getBooleanProperty(PROP_SPIFFE_AS_USERNAME_ENABLED, false);
+    }
+
+    /**
+     * Returns the login-id validation regex in effect. When using SPIFFE IDs as usernames
+     * is enabled, ':' is treated as a valid character (see {@link #VALIDATION_LOGINID_SPIFFE}).
+     *
+     * @return the login-id validation regex
+     */
+    public static String getLoginIdValidationRegEx() {
+        return isSpiffeAsUsernameEnabled() ? VALIDATION_LOGINID_SPIFFE : VALIDATION_LOGINID;
+    }
 }
diff --git a/security-admin/src/main/java/org/apache/ranger/rest/UserREST.java b/security-admin/src/main/java/org/apache/ranger/rest/UserREST.java
index 4708b8638..aeb4969ed 100644
--- a/security-admin/src/main/java/org/apache/ranger/rest/UserREST.java
+++ b/security-admin/src/main/java/org/apache/ranger/rest/UserREST.java
@@ -17,22 +17,7 @@
  * under the License.
  */
 
- package org.apache.ranger.rest;
-
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import javax.servlet.http.HttpServletRequest;
-import javax.ws.rs.Consumes;
-import javax.ws.rs.GET;
-import javax.ws.rs.POST;
-import javax.ws.rs.PUT;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.Produces;
-import javax.ws.rs.core.Context;
+package org.apache.ranger.rest;
 
 import org.apache.ranger.biz.UserMgr;
 import org.apache.ranger.biz.XUserMgr;
@@ -66,6 +51,20 @@
 import org.springframework.transaction.annotation.Propagation;
 import org.springframework.transaction.annotation.Transactional;
 
+import javax.servlet.http.HttpServletRequest;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.Context;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 
 @Path("users")
 @Component
@@ -73,293 +72,290 @@
 @RangerAnnotationJSMgrName("UserMgr")
 @Transactional(propagation = Propagation.REQUIRES_NEW)
 public class UserREST {
-	private static final Logger logger = LoggerFactory.getLogger(UserREST.class);
-
-	@Autowired
-	StringUtil stringUtil;
-
-	@Autowired
-	RangerDaoManager daoManager;
-
-	@Autowired
-	RangerConfigUtil configUtil;
-
-	@Autowired
-	RESTErrorUtil restErrorUtil;
-
-	@Autowired
-	SearchUtil searchUtil;
-
-	@Autowired
-	UserMgr userManager;
-
-	@Autowired
-	RangerRestUtil msRestUtil;
-	
-	@Autowired
-	XUserMgr xUserMgr;
-
-	private final static List<SortField> SORT_FIELDS = Arrays.asList(
-			new SortField("requestDate", "requestDate"),
-			new SortField("approvedDate", "approvedDate"),
-			new SortField("activationDate", "activationDate"),
-			new SortField("emailAddress", "emailAddress"),
-			new SortField("firstName", "firstName"),
-			new SortField("lastName", "lastName")
-		);
-	/**
-	 * Implements the traditional search functionalities for UserProfile
-	 *
-	 * @param request
-	 * @return
-	 */
-	@GET
-	@Produces({ "application/json" })
-	@PreAuthorize("@rangerPreAuthSecurityHandler.isAPIAccessible(\"" + RangerAPIList.SEARCH_USERS + "\")")
-	public VXPortalUserList searchUsers(@Context HttpServletRequest request) {
-		SearchCriteria searchCriteria = searchUtil.extractCommonCriterias(
-				request, SORT_FIELDS);
-
-		// userId
-		searchUtil.extractLong(request, searchCriteria, "userId", "User Id");
-
-		// loginId
-		searchUtil.extractString(request, searchCriteria, "loginId",
-				"Login Id", null);
-
-		// emailAddress
-		searchUtil.extractString(request, searchCriteria, "emailAddress",
-				"Email Address", null);
-
-		// firstName
-		searchUtil.extractString(request, searchCriteria, "firstName",
-				"First Name", StringUtil.VALIDATION_NAME);
-
-		// lastName
-		searchUtil.extractString(request, searchCriteria, "lastName",
-				"Last Name", StringUtil.VALIDATION_NAME);
-
-		// status
-		searchUtil.extractEnum(request, searchCriteria, "status", "Status",
-				"statusList", RangerConstants.ActivationStatus_MAX);
-
-		// publicScreenName
-		searchUtil.extractString(request, searchCriteria, "publicScreenName",
-				"Public Screen Name", StringUtil.VALIDATION_NAME);
-		// roles
-		searchUtil.extractStringList(request, searchCriteria, "role", "Role",
-				"roleList", configUtil.getRoles(), StringUtil.VALIDATION_NAME);
-
-		return userManager.searchUsers(searchCriteria);
-	}
-
-	/**
-	 * Return the VUserProfile for the given userId
-	 *
-	 * @param userId
-	 * @return
-	 */
-	@GET
-	@Path("{userId}")
-	@Produces({ "application/json" })
-	@PreAuthorize("@rangerPreAuthSecurityHandler.isAPIAccessible(\"" + RangerAPIList.GET_USER_PROFILE_FOR_USER + "\")")
-	public VXPortalUser getUserProfileForUser(@PathParam("userId") Long userId) {
-		try {
-			VXPortalUser userProfile = userManager.getUserProfile(userId);
-			if (userProfile != null) {
-				if (logger.isDebugEnabled()) {
-					logger.debug("getUserProfile() Found User userId=" + userId);
-				}
-			} else {
-				logger.debug("getUserProfile() Not found userId=" + userId);
-			}
-			return userProfile;
-		} catch (Throwable t) {
-			logger.error("getUserProfile() no user session. error="
-					+ t.toString());
-		}
-		return null;
-	}
-
-	@POST
-	@Consumes({ "application/json" })
-	@Produces({ "application/json" })
-	@PreAuthorize("@rangerPreAuthSecurityHandler.isAPIAccessible(\"" + RangerAPIList.CREATE + "\")")
-	public VXPortalUser create(VXPortalUser userProfile,
-			@Context HttpServletRequest servletRequest) {
-		logger.info("create:" + userProfile.getEmailAddress());
-
-		return userManager.createUser(userProfile);
-	}
-	
-	// API to add user with default account
-	@POST
-	@Path("/default")
-	@Consumes({ "application/json" })
-	@Produces({ "application/json" })
-	@PreAuthorize("@rangerPreAuthSecurityHandler.isAPIAccessible(\"" + RangerAPIList.CREATE_DEFAULT_ACCOUNT_USER + "\")")
-	public VXPortalUser createDefaultAccountUser(VXPortalUser userProfile,
-			@Context HttpServletRequest servletRequest) {
-		VXPortalUser vxPortalUser;
-		vxPortalUser=userManager.createDefaultAccountUser(userProfile);
-		if(vxPortalUser!=null)
-		{
-			xUserMgr.assignPermissionToUser(vxPortalUser, true);
-		}
-		 return vxPortalUser;
-	}
-
-
-	@PUT
-	@Consumes({ "application/json" })
-	@Produces({ "application/json" })
-	@RangerAnnotationRestAPI(updates_classes = "VUserProfile")
-	@PreAuthorize("@rangerPreAuthSecurityHandler.isAPIAccessible(\"" + RangerAPIList.UPDATE + "\")")
-	public VXPortalUser update(VXPortalUser userProfile,
-			@Context HttpServletRequest servletRequest) {
-		logger.info("update:" + userProfile.getEmailAddress());
-		XXPortalUser gjUser = daoManager.getXXPortalUser().getById(userProfile.getId());
-		userManager.checkAccess(gjUser);
-		if (gjUser != null) {
-			msRestUtil.validateVUserProfileForUpdate(gjUser, userProfile);
-			gjUser = userManager.updateUser(userProfile);
-			return userManager.mapXXPortalUserVXPortalUser(gjUser);
-		} else {
-			logger.info("update(): Invalid userId provided: userId="
-					+ userProfile.getId());
-			throw restErrorUtil.createRESTException("serverMsg.userRestUser",
-					MessageEnums.DATA_NOT_FOUND, null, null,
-					userProfile.toString());
-		}
-	}
-
-	@PUT
-	@Path("/{userId}/roles")
-	@Consumes({ "application/json" })
-	@Produces({ "application/json" })
-	@PreAuthorize("@rangerPreAuthSecurityHandler.isAPIAccessible(\"" + RangerAPIList.SET_USER_ROLES + "\")")
-	public VXResponse setUserRoles(@PathParam("userId") Long userId,
-			VXStringList roleList) {
-		userManager.checkAccess(userId);
-		userManager.setUserRoles(userId, roleList.getVXStrings());
-		VXResponse response = new VXResponse();
-		response.setStatusCode(VXResponse.STATUS_SUCCESS);
-		return response;
-	}
-
-	/**
-	 * Deactivate the user
-	 *
-	 * @param userId
-	 * @return
-	 */
-	@POST
-	@Path("{userId}/deactivate")
-	@Consumes({ "application/json" })
-	@Produces({ "application/json" })
-	@PreAuthorize("@rangerPreAuthSecurityHandler.isAPIAccessible(\"" + RangerAPIList.DEACTIVATE_USER + "\")")
-	@RangerAnnotationClassName(class_name = VXPortalUser.class)
-	public VXPortalUser deactivateUser(@PathParam("userId") Long userId) {
-		XXPortalUser gjUser = daoManager.getXXPortalUser().getById(userId);
-		if (gjUser == null) {
-			logger.info("update(): Invalid userId provided: userId=" + userId);
-			throw restErrorUtil.createRESTException("serverMsg.userRestUser",
-					MessageEnums.DATA_NOT_FOUND, null, null, "" + userId);
-		}
-		return userManager.deactivateUser(gjUser);
-	}
-
-	/**
-	 * This method returns the VUserProfile for the current session
-	 *
-	 * @param request
-	 * @return
-	 */
-	@GET
-	@Path("/profile")
-	@Produces({ "application/json" })
-	public VXPortalUser getUserProfile(@Context HttpServletRequest request) {
-		try {
-			logger.debug("getUserProfile(). httpSessionId="
-					+ request.getSession().getId());
-			Map<String, String> configProperties  = new HashMap<>();
-			Long inactivityTimeout = PropertiesUtil.getLongProperty("ranger.service.inactivity.timeout", 15*60);
-			configProperties.put("inactivityTimeout", Long.toString(inactivityTimeout));
-			VXPortalUser userProfile = userManager.getUserProfileByLoginId();
-			userProfile.setConfigProperties(configProperties);
-			return userProfile;
-		} catch (Throwable t) {
-			logger.error(
-					"getUserProfile() no user session. error=" + t.toString(),
-					t);
-		}
-		return null;
-	}
-
-	/**	
-	 * @param userId
-	 * @param changePassword
-	 * @return
-	 */
-	@POST
-	@Path("{userId}/passwordchange")
-	@Consumes({ "application/json" })
-	@Produces({ "application/json" })
-	public VXResponse changePassword(@PathParam("userId") Long userId,
-			VXPasswordChange changePassword) {
-		if(changePassword==null || stringUtil.isEmpty(changePassword.getLoginId())) {
-			logger.warn("SECURITY:changePassword(): Invalid loginId provided. loginId was empty or null");
-			throw restErrorUtil.createRESTException("serverMsg.userRestUser", MessageEnums.DATA_NOT_FOUND, null, null, "");
-		} else if (changePassword.getId() == null) {
-			changePassword.setId(userId);
-		} else if (!changePassword.getId().equals(userId) ) {
-			logger.warn("SECURITY:changePassword(): userId mismatch");
-			throw restErrorUtil.createRESTException("serverMsg.userRestUser",MessageEnums.DATA_NOT_FOUND, null, null,"");
-		}
-
-		XXPortalUser gjUser = daoManager.getXXPortalUser().findByLoginId(changePassword.getLoginId());
-		if (gjUser == null) {
-			logger.warn("SECURITY:changePassword(): Invalid loginId provided: loginId="+ changePassword.getLoginId());
-			throw restErrorUtil.createRESTException("serverMsg.userRestUser",MessageEnums.DATA_NOT_FOUND, null, null, changePassword.getLoginId());
-		}
-
-		changePassword.setId(gjUser.getId());
- 		VXResponse ret = userManager.changePassword(changePassword);
-		return ret;
-	}
-
-	/**	
-	 *
-	 * @param userId
-	 * @param changeEmail
-	 * @return
-	 */
-	@POST
-	@Path("{userId}/emailchange")
-	@Consumes({ "application/json" })
-	@Produces({ "application/json" })
-	public VXPortalUser changeEmailAddress(@PathParam("userId") Long userId,
-			VXPasswordChange changeEmail) {
-		if(changeEmail==null || stringUtil.isEmpty(changeEmail.getLoginId())) {
-			logger.warn("SECURITY:changeEmail(): Invalid loginId provided. loginId was empty or null");
-			throw restErrorUtil.createRESTException("serverMsg.userRestUser", MessageEnums.DATA_NOT_FOUND, null, null, "");
-		} else if (changeEmail.getId() == null) {
-			changeEmail.setId(userId);
-		} else if (!changeEmail.getId().equals(userId) ) {
-			logger.warn("SECURITY:changeEmail(): userId mismatch");
-			throw restErrorUtil.createRESTException("serverMsg.userRestUser",MessageEnums.DATA_NOT_FOUND, null, null,"");
-		}
-
-		logger.info("changeEmail:" + changeEmail.getLoginId());
-		XXPortalUser gjUser = daoManager.getXXPortalUser().findByLoginId(changeEmail.getLoginId());
-		if (gjUser == null) {
-			logger.warn("SECURITY:changeEmail(): Invalid loginId provided: loginId="+ changeEmail.getLoginId());
-			throw restErrorUtil.createRESTException("serverMsg.userRestUser",MessageEnums.DATA_NOT_FOUND, null, null, changeEmail.getLoginId());
-		}
-
-		changeEmail.setId(gjUser.getId());
-		VXPortalUser ret = userManager.changeEmailAddress(gjUser, changeEmail);
-		return ret;
-	}
-
+    private static final Logger logger = LoggerFactory.getLogger(UserREST.class);
+
+    private static final List<SortField> SORT_FIELDS = Arrays.asList(new SortField("requestDate", "requestDate"), new SortField("approvedDate", "approvedDate"), new SortField("activationDate", "activationDate"), new SortField("emailAddress", "emailAddress"), new SortField("firstName", "firstName"), new SortField("lastName", "lastName"));
+
+    @Autowired
+    StringUtil stringUtil;
+
+    @Autowired
+    RangerDaoManager daoManager;
+
+    @Autowired
+    RangerConfigUtil configUtil;
+
+    @Autowired
+    RESTErrorUtil restErrorUtil;
+
+    @Autowired
+    SearchUtil searchUtil;
+
+    @Autowired
+    UserMgr userManager;
+
+    @Autowired
+    RangerRestUtil msRestUtil;
+
+    @Autowired
+    XUserMgr xUserMgr;
+
+    /**
+     * Implements the traditional search functionalities for UserProfile
+     *
+     * @param request
+     * @return
+     */
+    @GET
+    @Produces("application/json")
+    @PreAuthorize("@rangerPreAuthSecurityHandler.isAPIAccessible(\"" + RangerAPIList.SEARCH_USERS + "\")")
+    public VXPortalUserList searchUsers(@Context HttpServletRequest request) {
+        SearchCriteria searchCriteria = searchUtil.extractCommonCriterias(request, SORT_FIELDS);
+
+        // userId
+        searchUtil.extractLong(request, searchCriteria, "userId", "User Id");
+
+        // loginId
+        searchUtil.extractString(request, searchCriteria, "loginId", "Login Id", null);
+
+        // emailAddress
+        searchUtil.extractString(request, searchCriteria, "emailAddress", "Email Address", null);
+
+        // firstName
+        searchUtil.extractString(request, searchCriteria, "firstName", "First Name", StringUtil.VALIDATION_NAME);
+
+        // lastName
+        searchUtil.extractString(request, searchCriteria, "lastName", "Last Name", StringUtil.VALIDATION_NAME);
+
+        // status
+        searchUtil.extractEnum(request, searchCriteria, "status", "Status", "statusList", RangerConstants.ActivationStatus_MAX);
+
+        // publicScreenName
+        searchUtil.extractString(request, searchCriteria, "publicScreenName", "Public Screen Name", StringUtil.VALIDATION_NAME);
+        // roles
+        searchUtil.extractStringList(request, searchCriteria, "role", "Role", "roleList", configUtil.getRoles(), StringUtil.VALIDATION_NAME);
+
+        return userManager.searchUsers(searchCriteria);
+    }
+
+    /**
+     * Return the VUserProfile for the given userId
+     *
+     * @param userId
+     * @return
+     */
+    @GET
+    @Path("{userId}")
+    @Produces("application/json")
+    @PreAuthorize("@rangerPreAuthSecurityHandler.isAPIAccessible(\"" + RangerAPIList.GET_USER_PROFILE_FOR_USER + "\")")
+    public VXPortalUser getUserProfileForUser(@PathParam("userId") Long userId) {
+        try {
+            VXPortalUser userProfile = userManager.getUserProfile(userId);
+
+            if (userProfile != null) {
+                logger.debug("getUserProfile() Found User userId={}", userId);
+            } else {
+                logger.debug("getUserProfile() Not found userId={}", userId);
+            }
+
+            return userProfile;
+        } catch (Throwable t) {
+            logger.error("getUserProfile() no user session. error={}", String.valueOf(t));
+        }
+
+        return null;
+    }
+
+    @POST
+    @Consumes("application/json")
+    @Produces("application/json")
+    @PreAuthorize("@rangerPreAuthSecurityHandler.isAPIAccessible(\"" + RangerAPIList.CREATE + "\")")
+    public VXPortalUser create(VXPortalUser userProfile, @Context HttpServletRequest servletRequest) {
+        logger.info("create:{}", userProfile.getEmailAddress());
+
+        return userManager.createUser(userProfile);
+    }
+
+    // API to add user with default account
+    @POST
+    @Path("/default")
+    @Consumes("application/json")
+    @Produces("application/json")
+    @PreAuthorize("@rangerPreAuthSecurityHandler.isAPIAccessible(\"" + RangerAPIList.CREATE_DEFAULT_ACCOUNT_USER + "\")")
+    public VXPortalUser createDefaultAccountUser(VXPortalUser userProfile, @Context HttpServletRequest servletRequest) {
+        VXPortalUser vxPortalUser = userManager.createDefaultAccountUser(userProfile);
+
+        if (vxPortalUser != null) {
+            xUserMgr.assignPermissionToUser(vxPortalUser, true);
+        }
+
+        return vxPortalUser;
+    }
+
+    @PUT
+    @Consumes("application/json")
+    @Produces("application/json")
+    @RangerAnnotationRestAPI(updates_classes = "VUserProfile")
+    @PreAuthorize("@rangerPreAuthSecurityHandler.isAPIAccessible(\"" + RangerAPIList.UPDATE + "\")")
+    public VXPortalUser update(VXPortalUser userProfile, @Context HttpServletRequest servletRequest) {
+        logger.info("update:{}", userProfile.getEmailAddress());
+
+        XXPortalUser gjUser = daoManager.getXXPortalUser().getById(userProfile.getId());
+
+        userManager.checkAccess(gjUser);
+
+        if (gjUser != null) {
+            msRestUtil.validateVUserProfileForUpdate(gjUser, userProfile);
+
+            gjUser = userManager.updateUser(userProfile);
+
+            return userManager.mapXXPortalUserVXPortalUser(gjUser);
+        } else {
+            logger.info("update(): Invalid userId provided: userId={}", userProfile.getId());
+
+            throw restErrorUtil.createRESTException("serverMsg.userRestUser", MessageEnums.DATA_NOT_FOUND, null, null, userProfile.toString());
+        }
+    }
+
+    @PUT
+    @Path("/{userId}/roles")
+    @Consumes("application/json")
+    @Produces("application/json")
+    @PreAuthorize("@rangerPreAuthSecurityHandler.isAPIAccessible(\"" + RangerAPIList.SET_USER_ROLES + "\")")
+    public VXResponse setUserRoles(@PathParam("userId") Long userId, VXStringList roleList) {
+        userManager.checkAccess(userId);
+        userManager.setUserRoles(userId, roleList.getVXStrings());
+
+        VXResponse response = new VXResponse();
+
+        response.setStatusCode(VXResponse.STATUS_SUCCESS);
+
+        return response;
+    }
+
+    /**
+     * Deactivate the user
+     *
+     * @param userId
+     * @return
+     */
+    @POST
+    @Path("{userId}/deactivate")
+    @Consumes("application/json")
+    @Produces("application/json")
+    @PreAuthorize("@rangerPreAuthSecurityHandler.isAPIAccessible(\"" + RangerAPIList.DEACTIVATE_USER + "\")")
+    @RangerAnnotationClassName(class_name = VXPortalUser.class)
+    public VXPortalUser deactivateUser(@PathParam("userId") Long userId) {
+        XXPortalUser gjUser = daoManager.getXXPortalUser().getById(userId);
+
+        if (gjUser == null) {
+            logger.info("update(): Invalid userId provided: userId={}", userId);
+
+            throw restErrorUtil.createRESTException("serverMsg.userRestUser", MessageEnums.DATA_NOT_FOUND, null, null, "" + userId);
+        }
+
+        return userManager.deactivateUser(gjUser);
+    }
+
+    /**
+     * This method returns the VUserProfile for the current session
+     *
+     * @param request
+     * @return
+     */
+    @GET
+    @Path("/profile")
+    @Produces("application/json")
+    public VXPortalUser getUserProfile(@Context HttpServletRequest request) {
+        try {
+            logger.debug("getUserProfile(). httpSessionId={}", request.getSession().getId());
+
+            Map<String, String> configProperties  = new HashMap<>();
+            long                inactivityTimeout = PropertiesUtil.getLongProperty("ranger.service.inactivity.timeout", 15 * 60);
+
+            configProperties.put("inactivityTimeout", Long.toString(inactivityTimeout));
+            configProperties.put("spiffeAsUsernameEnabled", Boolean.toString(StringUtil.isSpiffeAsUsernameEnabled()));
+
+            VXPortalUser userProfile = userManager.getUserProfileByLoginId();
+
+            userProfile.setConfigProperties(configProperties);
+
+            return userProfile;
+        } catch (Throwable t) {
+            logger.error("getUserProfile() no user session. error={}", t, t);
+        }
+        return null;
+    }
+
+    /**
+     * @param userId
+     * @param changePassword
+     * @return
+     */
+    @POST
+    @Path("{userId}/passwordchange")
+    @Consumes("application/json")
+    @Produces("application/json")
+    public VXResponse changePassword(@PathParam("userId") Long userId, VXPasswordChange changePassword) {
+        if (changePassword == null || stringUtil.isEmpty(changePassword.getLoginId())) {
+            logger.warn("SECURITY:changePassword(): Invalid loginId provided. loginId was empty or null");
+
+            throw restErrorUtil.createRESTException("serverMsg.userRestUser", MessageEnums.DATA_NOT_FOUND, null, null, "");
+        } else if (changePassword.getId() == null) {
+            changePassword.setId(userId);
+        } else if (!changePassword.getId().equals(userId)) {
+            logger.warn("SECURITY:changePassword(): userId mismatch");
+
+            throw restErrorUtil.createRESTException("serverMsg.userRestUser", MessageEnums.DATA_NOT_FOUND, null, null, "");
+        }
+
+        XXPortalUser gjUser = daoManager.getXXPortalUser().findByLoginId(changePassword.getLoginId());
+
+        if (gjUser == null) {
+            logger.warn("SECURITY:changePassword(): Invalid loginId provided: loginId={}", changePassword.getLoginId());
+
+            throw restErrorUtil.createRESTException("serverMsg.userRestUser", MessageEnums.DATA_NOT_FOUND, null, null, changePassword.getLoginId());
+        }
+
+        changePassword.setId(gjUser.getId());
+
+        return userManager.changePassword(changePassword);
+    }
+
+    /**
+     * @param userId
+     * @param changeEmail
+     * @return
+     */
+    @POST
+    @Path("{userId}/emailchange")
+    @Consumes("application/json")
+    @Produces("application/json")
+    public VXPortalUser changeEmailAddress(@PathParam("userId") Long userId, VXPasswordChange changeEmail) {
+        if (changeEmail == null || stringUtil.isEmpty(changeEmail.getLoginId())) {
+            logger.warn("SECURITY:changeEmail(): Invalid loginId provided. loginId was empty or null");
+
+            throw restErrorUtil.createRESTException("serverMsg.userRestUser", MessageEnums.DATA_NOT_FOUND, null, null, "");
+        } else if (changeEmail.getId() == null) {
+            changeEmail.setId(userId);
+        } else if (!changeEmail.getId().equals(userId)) {
+            logger.warn("SECURITY:changeEmail(): userId mismatch");
+
+            throw restErrorUtil.createRESTException("serverMsg.userRestUser", MessageEnums.DATA_NOT_FOUND, null, null, "");
+        }
+
+        logger.info("changeEmail:{}", changeEmail.getLoginId());
+
+        XXPortalUser gjUser = daoManager.getXXPortalUser().findByLoginId(changeEmail.getLoginId());
+
+        if (gjUser == null) {
+            logger.warn("SECURITY:changeEmail(): Invalid loginId provided: loginId={}", changeEmail.getLoginId());
+
+            throw restErrorUtil.createRESTException("serverMsg.userRestUser", MessageEnums.DATA_NOT_FOUND, null, null, changeEmail.getLoginId());
+        }
+
+        changeEmail.setId(gjUser.getId());
+
+        return userManager.changeEmailAddress(gjUser, changeEmail);
+    }
 }
diff --git a/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerHeaderPreAuthFilter.java b/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerHeaderPreAuthFilter.java
index 3cf984f7d..800bf92f5 100644
--- a/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerHeaderPreAuthFilter.java
+++ b/security-admin/src/main/java/org/apache/ranger/security/web/filter/RangerHeaderPreAuthFilter.java
@@ -110,8 +110,8 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha
 
     /**
      * Resolves the principal from trusted headers. The username header (user identity) takes
-     * precedence; when it is absent, the SPIFFE header (service identity) is used and
-     * the trailing service-account segment of the SPIFFE ID becomes the principal.
+     * precedence; when it is absent, the SPIFFE header (service identity) is used and the
+     * full SPIFFE ID becomes the principal (SPIFFE IDs are used as usernames in Ranger).
      */
     private String resolvePrincipal(HttpServletRequest httpRequest) {
         String username = StringUtils.isNotBlank(userNameHeaderName) ? StringUtils.trimToNull(httpRequest.getHeader(userNameHeaderName)) : null;
@@ -121,13 +121,12 @@ private String resolvePrincipal(HttpServletRequest httpRequest) {
         }
 
         for (String spiffeHeaderName : spiffeHeaderNames) {
-            String spiffeId       = StringUtils.trimToNull(httpRequest.getHeader(spiffeHeaderName));
-            String serviceAccount = SpiffeIdUtil.extractServiceAccount(spiffeId);
+            String spiffeId = StringUtils.trimToNull(httpRequest.getHeader(spiffeHeaderName));
 
-            if (StringUtils.isNotBlank(serviceAccount)) {
-                LOG.debug("Resolved service-account '{}' from SPIFFE header '{}'", serviceAccount, spiffeHeaderName);
+            if (SpiffeIdUtil.isValidSpiffeId(spiffeId)) {
+                LOG.debug("Resolved SPIFFE ID '{}' from header '{}'", spiffeId, spiffeHeaderName);
 
-                return serviceAccount;
+                return spiffeId;
             } else if (StringUtils.isNotBlank(spiffeId)) {
                 LOG.warn("SPIFFE header '{}' value is not a well-formed SPIFFE ID", spiffeHeaderName);
             }
diff --git a/security-admin/src/main/resources/conf.dist/ranger-admin-site.xml b/security-admin/src/main/resources/conf.dist/ranger-admin-site.xml
index 5493b69ba..1a9c40caf 100644
--- a/security-admin/src/main/resources/conf.dist/ranger-admin-site.xml
+++ b/security-admin/src/main/resources/conf.dist/ranger-admin-site.xml
@@ -326,12 +326,17 @@
 	<property>
 		<name>ranger.admin.authn.header.spiffe</name>
 		<value></value>
-		<description>Comma-separated list of HTTP header name(s) carrying a SPIFFE ID (e.g. spiffe://cluster/ns/&lt;ns&gt;/sa/&lt;service-account&gt;). A single header name is also valid. The trailing service-account segment is used as the authenticated principal. When multiple headers are configured, the first one carrying a well-formed SPIFFE ID is used. Consulted only when the username header is absent.</description>
+		<description>Comma-separated list of HTTP header name(s) carrying a SPIFFE ID (e.g. spiffe://cluster/ns/&lt;ns&gt;/sa/&lt;service-account&gt;). A single header name is also valid. The full SPIFFE ID is used as the authenticated principal. When multiple headers are configured, the first one carrying a well-formed SPIFFE ID is used. Consulted only when the username header is absent.</description>
 	</property>
 	<property>
 		<name>ranger.admin.authn.header.requestid</name>
 		<value></value>
 	</property>
+	<property>
+		<name>ranger.admin.spiffe.as.username.enabled</name>
+		<value>false</value>
+		<description>When enabled, ':' is accepted as a valid character in usernames so that SPIFFE IDs (e.g. spiffe://spiffe.example.com/ns/sales/sa/trino) can be used as service-account usernames. Disabled by default.</description>
+	</property>
 	<!-- Trusted header auth properties end -->
 	<!-- Kerberos Properties starts-->	
 	<property>
diff --git a/security-admin/src/main/webapp/login.jsp b/security-admin/src/main/webapp/login.jsp
index b25cb78ad..8ac693c7e 100644
--- a/security-admin/src/main/webapp/login.jsp
+++ b/security-admin/src/main/webapp/login.jsp
@@ -14,6 +14,7 @@
   See the License for the specific language governing permissions and
   limitations under the License.
 -->
+<%@ page import="org.apache.ranger.common.StringUtil" %>
 <!DOCTYPE html>
 <!--[if lt IE 7]><html class="no-js lt-ie9 lt-ie8 lt-ie7"><![endif]-->
 <!--[if IE 7]><html class="no-js lt-ie9 lt-ie8"><![endif]-->
@@ -32,6 +33,9 @@
         <link rel="stylesheet" href="styles/font-awesome.min.css">
         <link href="styles/xa.css" media="all" rel="stylesheet" type="text/css" >
         <script src="libs/bower/jquery/js/core-lib.js" ></script>
+        <script type="text/javascript">
+            window.rangerSpiffeAsUsernameEnabled = <%= StringUtil.isSpiffeAsUsernameEnabled() %>;
+        </script>
         <script src="scripts/prelogin/XAPrelogin.js" ></script>
         <script type="text/javascript">
             $(document).ready(function() {
diff --git a/security-admin/src/main/webapp/react-webapp/src/utils/XAEnums.js b/security-admin/src/main/webapp/react-webapp/src/utils/XAEnums.js
old mode 100644
new mode 100755
index b2cf730b6..b0f73fa90
--- a/security-admin/src/main/webapp/react-webapp/src/utils/XAEnums.js
+++ b/security-admin/src/main/webapp/react-webapp/src/utils/XAEnums.js
@@ -363,6 +363,54 @@ export const ClassTypes = {
     modelName: "VXRole",
     type: "vXRole",
     tt: "lbl.ClassTypes_CLASS_TYPE_RANGER_ROLE"
+  },
+  CLASS_TYPE_RANGER_DATASET: {
+    value: 1062,
+    label: "Ranger Dataset",
+    rbkey: "xa.enum.ClassTypes.CLASS_TYPE_RANGER_DATASET",
+    modelName: "RangerDataset",
+    type: "vDataset",
+    tt: "lbl.ClassTypes_CLASS_TYPE_RANGER_DATASET"
+  },
+  CLASS_TYPE_RANGER_PROJECT: {
+    value: 1063,
+    label: "Ranger Project",
+    rbkey: "xa.enum.ClassTypes.CLASS_TYPE_RANGER_PROJECT",
+    modelName: "RangerProject",
+    type: "vProject",
+    tt: "lbl.ClassTypes_CLASS_TYPE_RANGER_PROJECT"
+  },
+  CLASS_TYPE_RANGER_DATA_SHARE: {
+    value: 1064,
+    label: "Ranger Data Share",
+    rbkey: "xa.enum.ClassTypes.CLASS_TYPE_RANGER_DATA_SHARE",
+    modelName: "RangerDataShare",
+    type: "vDataShare",
+    tt: "lbl.ClassTypes_CLASS_TYPE_RANGER_DATA_SHARE"
+  },
+  CLASS_TYPE_RANGER_SHARED_RESOURCE: {
+    value: 1065,
+    label: "Ranger Shared Resource",
+    rbkey: "xa.enum.ClassTypes.CLASS_TYPE_RANGER_SHARED_RESOURCE",
+    modelName: "RangerSharedResource",
+    type: "vSharedResource",
+    tt: "lbl.ClassTypes_CLASS_TYPE_RANGER_SHARED_RESOURCE"
+  },
+  CLASS_TYPE_RANGER_DATA_SHARE_IN_DATASET: {
+    value: 1066,
+    label: "Ranger DataShare in Dataset",
+    rbkey: "xa.enum.ClassTypes.CLASS_TYPE_RANGER_DATA_SHARE_IN_DATASET",
+    modelName: "RangerDataShareInDataset",
+    type: "vDataShareInDataset",
+    tt: "lbl.ClassTypes_CLASS_TYPE_RANGER_DATA_SHARE_IN_DATASET"
+  },
+  CLASS_TYPE_RANGER_DATASET_IN_PROJECT: {
+    value: 1067,
+    label: "Ranger Dataset in Project",
+    rbkey: "xa.enum.ClassTypes.CLASS_TYPE_RANGER_DATASET_IN_PROJECT",
+    modelName: "RangerDatasetInProject",
+    type: "vDatasetInProject",
+    tt: "lbl.ClassTypes_CLASS_TYPE_RANGER_DATASET_IN_PROJECT"
   }
 };
 
@@ -536,9 +584,11 @@ export const RegexValidation = {
   NAME_VALIDATION: {
     regexExpressionForName:
       /^([A-Za-z0-9_]|[\u00C0-\u017F])([a-z0-9,._\-+/@= ]|[\u00C0-\u017F])+$/i,
+    regexExpressionForUserName:
+      /^([A-Za-z0-9_]|[\u00C0-\u017F])([a-z0-9,._\-+/@=: ]|[\u00C0-\u017F])+$/i,
     regexExpressionForFirstAndLastName:
       /^([A-Za-z0-9_]|[\u00C0-\u017F])([a-zA-Z0-9\s_. -@]|[\u00C0-\u017F])+$/i,
-    regexforNameValidation: /^[a-zA-Z0-9_-][a-zA-Z0-9\s_-]{0,254}$/,
+    regexForNameValidation: /^[a-zA-Z0-9_-][a-zA-Z0-9\s_-]{0,254}$/,
     regexExpressionForSecondaryName:
       /^([A-Za-z0-9_]|[\u00C0-\u017F])([a-zA-Z0-9\s_. -@]|[\u00C0-\u017F])+$/i,
     regexforServiceNameValidation: /^[a-zA-Z0-9_-][a-zA-Z0-9_-]{0,254}$/,
@@ -553,7 +603,16 @@ export const RegexValidation = {
         3. Name length should be greater than one.
       </>
     ),
-    regexforNameValidationMessage:
+    userNameValidationMessage: (
+      <>
+        1. Name should be start with alphabet / numeric / underscore / non-us
+        characters.
+        <br />
+        2. Allowed special character ,._-+/@=: and space. <br />
+        3. Name length should be greater than one.
+      </>
+    ),
+    regexForNameValidationMessage:
       "Name should not start with space, it should be less than 256 characters and special characters are not allowed(except _ - and space).",
     secondaryNameValidationMessage: (
       <>
@@ -621,6 +680,22 @@ export const PathAssociateWithModule = {
     "/roles/create",
     "/roles/:roleId"
   ],
+  "Governed Data Sharing": [
+    "/gds/mydatasetlisting",
+    "/gds/datasetlisting",
+    "/gds/create",
+    "/gds/dataset/:datasetId/detail",
+    "/gds/dataset/:datasetId/fullview",
+    "/gds/dataset/:datasetId/accessGrant",
+    "/gds/mydatasharelisting",
+    "/gds/datasharelisting",
+    "/gds/datashare/create",
+    "/gds/request/list",
+    "/gds/request/detail/:requestId",
+    "/gds/datashare/:datashareId/detail",
+    "/gds/datashare/:datashareId/fullview",
+    "/gds/datashare/resource/:datashareId"
+  ],
   Permission: ["/permissions/models", "/permissions/:permissionId/edit"],
   Profile: ["/userprofile"],
   KnoxSignOut: ["/knoxSSOWarning"],
@@ -628,7 +703,11 @@ export const PathAssociateWithModule = {
   PageNotFound: ["/pageNotFound"],
   localLogin: ["/locallogin"],
   slashPath: ["/"],
-  Forbidden: ["/forbidden"]
+  Forbidden: ["/forbidden"],
+  CreateNewPolicyForm: ["/policymanager/create-policy"],
+  EditNewPolicyForm: [
+    "/service/:serviceId/policies/:policyId/new-policy-form-edit"
+  ]
 };
 
 /* Access */
@@ -637,13 +716,13 @@ export const DefStatus = {
   RecursiveStatus: {
     STATUS_RECURSIVE: {
       value: true,
-      label: "recursive",
+      label: "Recursive",
       rbkey: "xa.enum.RecursiveStatus.RECURSIVE",
       tt: "lbl.RecursiveStatus_RECURSIVE"
     },
     STATUS_NONRECURSIVE: {
       value: false,
-      label: "nonrecursive",
+      label: "Non-recursive",
       rbkey: "xa.enum.RecursiveStatus.NONRECURSIVE",
       tt: "lbl.RecursiveStatus_NONRECURSIVE"
     }
@@ -854,9 +933,37 @@ export const pluginStatusColumnInfoMsg = {
       "Latest update in tags are not yet downloaded (sync-up with Ranger).",
     activationTimeDelayMsg:
       "Latest update in tags are not yet active for enforcement."
+  },
+  GDS: {
+    title: "GDS (Time details)",
+    lastUpdated: "Last update time of GDS info.",
+    downloadTime: "Last GDS info download time (sync-up with Ranger).",
+    activeTime:
+      "Last time the downloaded GDS info became active for enforcement.",
+    downloadTimeDelayMsg:
+      "Latest update in GDS info is not yet downloaded (sync-up with Ranger).",
+    activationTimeDelayMsg:
+      "Latest update in GDS info is not yet active for enforcement."
+  },
+  Role: {
+    title: "Role (Time details)",
+    lastUpdated: "Last updated time of roles.",
+    downloadTime: "Last roles download time (sync-up with Ranger).",
+    activeTime: "Last time the downloaded roles became active for enforcement.",
+    downloadTimeDelayMsg:
+      "Latest update in roles are not yet downloaded (sync-up with Ranger).",
+    activationTimeDelayMsg:
+      "Latest update in roles are not yet active for enforcement."
   }
 };
 
+export const statusClassMap = {
+  REQUESTED: "badge bg-warning",
+  GRANTED: "badge bg-success",
+  ACTIVE: "badge bg-primary",
+  DENIED: "badge bg-danger"
+};
+
 export const additionalServiceConfigs = [
   {
     label: "Policy Download Users",
diff --git a/security-admin/src/main/webapp/react-webapp/src/utils/XAMessages.js b/security-admin/src/main/webapp/react-webapp/src/utils/XAMessages.js
index ee7b010ea..8480f27bf 100644
--- a/security-admin/src/main/webapp/react-webapp/src/utils/XAMessages.js
+++ b/security-admin/src/main/webapp/react-webapp/src/utils/XAMessages.js
@@ -36,9 +36,21 @@ export const RegexMessage = {
         </p>
       </>
     ),
-    passwordvalidationinfomessage:
-      "Password should be minimum 8 characters ,atleast one uppercase letter, one lowercase letter and one numeric. For FIPS environment password should be minimum 14 characters with atleast one uppercase letter, one special characters, one lowercase letter and one numeric.",
-    emailvalidationinfomessage: (
+    userNameValidationMsgWithSpiffe: (
+      <>
+        <p className="pd-10 mb-0" style={{ fontSize: "small" }}>
+          1. User name should be start with alphabet / numeric / underscore /
+          non-us characters.
+          <br />
+          2. Allowed special character ,._-+/@=: and space.
+          <br />
+          3. Name length should be greater than one.
+        </p>
+      </>
+    ),
+    passwordValidationInfoMessage:
+      "Password should be minimum 8 characters ,at least one uppercase letter, one lowercase letter and one numeric. For FIPS environment password should be minimum 14 characters with atleast one uppercase letter, one special characters, one lowercase letter and one numeric.",
+    emailValidationInfoMessage: (
       <>
         <p className="pd-10 mb-0" style={{ fontSize: "small" }}>
           1. Email address should be start with alphabet / numeric / underscore
@@ -50,10 +62,16 @@ export const RegexMessage = {
         </p>
       </>
     ),
-    policyConditionInfoIcon:
-      "1. JavaScript Condition Examples :\
-                      country_code == 'USA', time_range >= 900 time_range <= 1800 etc.\
-                      2. Dragging bottom-right corner of javascript condition editor(Textarea) can resizable",
+    policyConditionInfoIcon:(
+      <>
+        <span className="mb-0" style={{ fontSize: "small" }}>
+          1. Boolean expression Examples:
+          <br />
+            country_code == 'USA', time_range &gt;= 900 time_range &lt;= 1800 etc.
+          <br />
+          2. The condition editor (textarea) can be resized by dragging its bottom-right corner.
+        </span>
+      </>),
     firstNameValidationMsg: (
       <>
         <p className="pd-10 mb-0" style={{ fontSize: "small" }}>
diff --git a/security-admin/src/main/webapp/react-webapp/src/views/UserGroupRoleListing/users_details/UserForm.jsx b/security-admin/src/main/webapp/react-webapp/src/views/UserGroupRoleListing/users_details/UserForm.jsx
index 2eb9e5a85..4b0a15ff3 100644
--- a/security-admin/src/main/webapp/react-webapp/src/views/UserGroupRoleListing/users_details/UserForm.jsx
+++ b/security-admin/src/main/webapp/react-webapp/src/views/UserGroupRoleListing/users_details/UserForm.jsx
@@ -23,7 +23,9 @@ import { Form, Field } from "react-final-form";
 import {
   BlockUi,
   scrollToError,
-  selectInputCustomStyles
+  selectInputCustomStyles,
+  trimInputValue,
+  ConfirmationClearIndicator
 } from "Components/CommonComponents";
 import AsyncSelect from "react-select/async";
 import Select from "react-select";
@@ -48,7 +50,11 @@ const INITIAL_STATE = {
   preventUnBlock: false
 };
 
-const PromtDialog = (props) => {
+// SPIFFE IDs (e.g. spiffe://spiffe.example.com/ns/sales/sa/trino) contain ':', which is only permitted in usernames when the admin config `ranger.admin.spiffe.as.username.enabled` is enabled.
+const isSpiffeAsUsernameEnabled = () =>
+  getUserProfile()?.configProperties?.spiffeAsUsernameEnabled === "true";
+
+const PromptDialog = (props) => {
   const { isDirtyField, isUnblock } = props;
   usePrompt("Are you sure you want to leave", isDirtyField && !isUnblock);
   return null;
@@ -241,19 +247,25 @@ function UserForm(props) {
           isEditView && userInfo && isExternalOrFederatedUser ? true : false
         }
         styles={selectInputCustomStyles}
+        tabSelectsValue={false}
+        placeholder="Select Groups"
+        clearConfirmMessage="Groups"
+        components={{
+          ClearIndicator: ConfirmationClearIndicator
+        }}
       />
     );
   };
 
   const disabledUserRoleField = () => {
     const userProps = getUserProfile();
-    let disabledUserRolefield;
+    let disabledUserRoleField;
     if (isEditView && userInfo) {
       if (userInfo.userSource == UserTypes.USER_EXTERNAL.value) {
-        disabledUserRolefield = true;
+        disabledUserRoleField = true;
       }
       if (userInfo.userSource == UserTypes.USER_FEDERATED.value) {
-        return (disabledUserRolefield = true);
+        return (disabledUserRoleField = true);
       }
       if (userProps.loginId != "admin") {
         if (userInfo.name != "admin") {
@@ -261,21 +273,21 @@ function UserForm(props) {
             userProps.userRoleList[0] == "ROLE_SYS_ADMIN" ||
             userProps.userRoleList[0] == "ROLE_KEY_ADMIN"
           ) {
-            disabledUserRolefield = false;
+            disabledUserRoleField = false;
           } else {
-            disabledUserRolefield = true;
+            disabledUserRoleField = true;
           }
         } else {
-          disabledUserRolefield = true;
+          disabledUserRoleField = true;
         }
       } else {
-        disabledUserRolefield = false;
+        disabledUserRoleField = false;
       }
       if (userInfo.name == userProps.loginId) {
-        disabledUserRolefield = true;
+        disabledUserRoleField = true;
       }
     }
-    return disabledUserRolefield;
+    return disabledUserRoleField;
   };
 
   const userRoleListData = () => {
@@ -294,10 +306,9 @@ function UserForm(props) {
     let formValueObj = {};
     if (isEditView && userInfo) {
       formValueObj.name = userInfo.name;
-      formValueObj.firstName = userInfo.firstName;
-      formValueObj.lastName = userInfo.lastName;
+      formValueObj.firstName = userInfo?.firstName?.trim();
+      formValueObj.lastName = userInfo?.lastName?.trim();
       formValueObj.emailAddress = userInfo.emailAddress;
-      formValueObj.firstName = userInfo.firstName;
     }
     if (userInfo && userInfo.userRoleList) {
       formValueObj.userRoleList = {
@@ -335,12 +346,14 @@ function UserForm(props) {
     if (!values.name) {
       errors.name = "Required";
     } else {
-      if (
-        !RegexValidation.NAME_VALIDATION.regexExpressionForName.test(
-          values.name
-        )
-      ) {
-        errors.name = RegexValidation.NAME_VALIDATION.nameValidationMessage;
+      const spiffeEnabled = isSpiffeAsUsernameEnabled();
+      const userNameRegex = spiffeEnabled
+        ? RegexValidation.NAME_VALIDATION.regexExpressionForUserName
+        : RegexValidation.NAME_VALIDATION.regexExpressionForName;
+      if (!userNameRegex.test(values.name)) {
+        errors.name = spiffeEnabled
+          ? RegexValidation.NAME_VALIDATION.userNameValidationMessage
+          : RegexValidation.NAME_VALIDATION.nameValidationMessage;
       }
     }
     if (!values.password && !isEditView) {
@@ -427,7 +440,7 @@ function UserForm(props) {
           dirty
         }) => (
           <div className="wrap user-role-grp-form">
-            <PromtDialog isDirtyField={dirty} isUnblock={preventUnBlock} />
+            <PromptDialog isDirtyField={dirty} isUnblock={preventUnBlock} />
             <form
               onSubmit={(event) => {
                 handleSubmit(event);
@@ -455,11 +468,16 @@ function UserForm(props) {
                         }
                         disabled={isEditView ? true : false}
                         data-cy="name"
+                        onBlur={(e) => trimInputValue(e, input)}
                       />
                       <InfoIcon
                         css="input-box-info-icon"
                         position="right"
-                        message={RegexMessage.MESSAGE.userNameValidationMsg}
+                        message={
+                          isSpiffeAsUsernameEnabled()
+                            ? RegexMessage.MESSAGE.userNameValidationMsgWithSpiffe
+                            : RegexMessage.MESSAGE.userNameValidationMsg
+                        }
                       />
 
                       {meta.error && meta.touched && (
@@ -505,7 +523,7 @@ function UserForm(props) {
                             >
                               {
                                 RegexMessage.MESSAGE
-                                  .passwordvalidationinfomessage
+                                  .passwordValidationInfoMessage
                               }
                             </p>
                           }
@@ -557,7 +575,7 @@ function UserForm(props) {
                             >
                               {
                                 RegexMessage.MESSAGE
-                                  .passwordvalidationinfomessage
+                                  .passwordValidationInfoMessage
                               }
                             </p>
                           }
@@ -598,6 +616,7 @@ function UserForm(props) {
                             : false
                         }
                         data-cy="firstName"
+                        onBlur={(e) => trimInputValue(e, input)}
                       />
                       <InfoIcon
                         css="input-box-info-icon"
@@ -635,6 +654,7 @@ function UserForm(props) {
                             : false
                         }
                         data-cy="lastName"
+                        onBlur={(e) => trimInputValue(e, input)}
                       />
                       <InfoIcon
                         css="input-box-info-icon"
@@ -683,7 +703,7 @@ function UserForm(props) {
                         css="input-box-info-icon"
                         position="right"
                         message={
-                          RegexMessage.MESSAGE.emailvalidationinfomessage
+                          RegexMessage.MESSAGE.emailValidationInfoMessage
                         }
                       />
 
@@ -710,6 +730,7 @@ function UserForm(props) {
                         options={userRoleListData()}
                         onChange={(e) => getUserRole(e, input)}
                         isDisabled={disabledUserRoleField()}
+                        tabSelectsValue={false}
                       ></Select>
                     )}
                   ></Field>
diff --git a/security-admin/src/main/webapp/scripts/prelogin/XAPrelogin.js b/security-admin/src/main/webapp/scripts/prelogin/XAPrelogin.js
index 28aab6523..e80c25ca7 100644
--- a/security-admin/src/main/webapp/scripts/prelogin/XAPrelogin.js
+++ b/security-admin/src/main/webapp/scripts/prelogin/XAPrelogin.js
@@ -45,7 +45,11 @@ function doLogin() {
 	}
 
 	var regexEmail = /^([a-zA-Z0-9_\.\-\+])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/;
-	var regexPlain = /^([A-Za-z0-9_]|[\u00C0-\u017F])([a-z0-9,._\-+/@= ]|[\u00C0-\u017F])+$/i;
+	// ':' is only accepted in usernames (for SPIFFE IDs) when the admin config ranger.admin.spiffe.as.username.enabled is turned on.
+	var spiffeAsUsername = (typeof window !== 'undefined' && window.rangerSpiffeAsUsernameEnabled === true);
+	var regexPlain = spiffeAsUsername
+		? /^([A-Za-z0-9_]|[\u00C0-\u017F])([a-z0-9,._\-+/@=: ]|[\u00C0-\u017F])+$/i
+		: /^([A-Za-z0-9_]|[\u00C0-\u017F])([a-z0-9,._\-+/@= ]|[\u00C0-\u017F])+$/i;
 	
 	if(!regexPlain.test(userName)){
 		if(!regexEmail.test(userName)){
diff --git a/security-admin/src/test/java/org/apache/ranger/common/TestStringUtil.java b/security-admin/src/test/java/org/apache/ranger/common/TestStringUtil.java
index 5514b1440..f8bc16aff 100644
--- a/security-admin/src/test/java/org/apache/ranger/common/TestStringUtil.java
+++ b/security-admin/src/test/java/org/apache/ranger/common/TestStringUtil.java
@@ -14,176 +14,206 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
- package org.apache.ranger.common;
+package org.apache.ranger.common;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.beans.factory.annotation.Autowired;
 
 import java.util.ArrayList;
 import java.util.List;
 
-import org.junit.Assert;
-import org.junit.Test;
-import org.springframework.beans.factory.annotation.Autowired;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
+@ExtendWith(MockitoExtension.class)
 public class TestStringUtil {
+    @Autowired
+    StringUtil stringUtil = new StringUtil();
+
+    @Test
+    public void testToCamelCaseAllWords() {
+        String camelcase      = "hello world";
+        String camelCaseWords = stringUtil.toCamelCaseAllWords(camelcase);
+        assertEquals("Hello World", camelCaseWords);
+    }
+
+    @Test
+    public void testNullValidatePassword() {
+        String[] invalidValues = {"aa", "bb", "aa12345dd"};
+        boolean  value         = stringUtil.validatePassword(null, invalidValues);
+        assertFalse(value);
+    }
+
+    @Test
+    public void testValidatePassword() {
+        String   password      = "Aa1234ddas12";
+        String[] invalidValues = {"aa", "bb", "aa12345dd"};
+        boolean  value         = stringUtil.validatePassword(password, invalidValues);
+        assertTrue(password.length() >= 8);
+        assertTrue(value);
+    }
+
+    @Test
+    public void testNotValidatePassword() {
+        String   password      = "aassasavcvcvc";
+        String[] invalidValues = {"aa", "bb", "aa12345dd"};
+        boolean  value         = stringUtil.validatePassword(password, invalidValues);
+        assertTrue(password.length() >= 8);
+        assertFalse(value);
+    }
+
+    @Test
+    public void testIsEmptyValue() {
+        String  str   = "";
+        boolean value = stringUtil.isEmpty(str);
+        assertTrue(value);
+    }
+
+    @Test
+    public void testIsNullValue() {
+        boolean value = stringUtil.isEmpty((String) null);
+        assertTrue(value);
+    }
+
+    @Test
+    public void testIsWithValue() {
+        String  str   = "test value";
+        boolean value = stringUtil.isEmpty(str);
+        assertFalse(value);
+    }
+
+    @Test
+    public void testEquals() {
+        String  str1  = "test";
+        String  str2  = "test";
+        boolean value = stringUtil.equals(str1, str2);
+        assertTrue(value);
+    }
+
+    @Test
+    public void testNormalizeEmail() {
+        String  email     = "test.Demo@test.COM";
+        String  lowercase = stringUtil.normalizeEmail(email);
+        String  emailId   = email.toLowerCase();
+        boolean value     = emailId.equals(lowercase);
+        assertTrue(value);
+    }
+
+    @Test
+    public void testNormalizeEmailIdNull() {
+        String lowercase = stringUtil.normalizeEmail(null);
+        assertNull(lowercase);
+    }
+
+    @Test
+    public void testSplit() {
+        String   str1        = "Test1";
+        String   str2        = "Test2";
+        String   str3        = "Test3";
+        String   value       = str1 + "," + str2 + "," + str3;
+        String[] stringArray = stringUtil.split(value);
+        assertEquals(3, stringArray.length);
+        assertEquals(str1, stringArray[0]);
+        assertEquals(str2, stringArray[1]);
+        assertEquals(str3, stringArray[2]);
+    }
+
+    @Test
+    public void testTrim() {
+        String str        = "test";
+        String dataString = StringUtil.trim(str);
+        assertEquals(str, dataString);
+    }
+
+    @Test
+    public void testValidateEmailId() {
+        String  email = "rangerqa@apache.org";
+        boolean value = stringUtil.validateEmail(email);
+        assertTrue(email.length() < 128);
+        assertTrue(value);
+    }
+
+    @Test
+    public void testNullEmailId() {
+        boolean value = stringUtil.validateEmail(null);
+        assertFalse(value);
+    }
+
+    @Test
+    public void testValidateString() {
+        String  regExStr = "^[\\w]([\\-\\.\\w])+[\\w]+@[\\w]+[\\w\\-]+[\\w]*\\.([\\w]+[\\w\\-]+[\\w]*(\\.[a-z][a-z|0-9]*)?)$";
+        String  str      = "test.test@gmail.com";
+        boolean value    = stringUtil.validateString(regExStr, str);
+        assertTrue(value);
+    }
+
+    @Test
+    public void testNotValidateString() {
+        String  regExStr = "^[\\w]([\\-\\.\\w])+[\\w]+[\\w]*\\.([\\w]+[\\w\\-]+[\\w]*(\\.[a-z][a-z|0-9]*)?)$";
+        String  str      = "test.test@gmail.com";
+        boolean value    = stringUtil.validateString(regExStr, str);
+        assertFalse(value);
+    }
+
+    @Test
+    public void testIsListEmpty() {
+        List<String> list      = new ArrayList<>();
+        boolean      listValue = stringUtil.isEmpty(list);
+        assertTrue(listValue);
+    }
+
+    @Test
+    public void testIsListNotEmpty() {
+        List<String> list = new ArrayList<>();
+        list.add("a");
+        list.add("b");
+        boolean listValue = stringUtil.isEmpty(list);
+        assertFalse(listValue);
+    }
+
+    @Test
+    public void testIsValidName() {
+        String  name  = "test";
+        boolean value = stringUtil.isValidName(name);
+        assertTrue(value);
+    }
+
+    @Test
+    public void testDefaultLoginIdRegExRejectsColon() {
+        // SPIFFE ID support is disabled by default, so ':' must not be a valid login-id character.
+        String spiffeId = "spiffe://spiffe.example.com/ns/sales/sa/trino";
+        assertFalse(stringUtil.validateString(StringUtil.VALIDATION_LOGINID, spiffeId));
+        assertFalse(stringUtil.validateString(StringUtil.VALIDATION_LOGINID, "user:name"));
+    }
+
+    @Test
+    public void testSpiffeLoginIdRegExAllowsSpiffeId() {
+        // SPIFFE IDs are used as service-account usernames in k8s deployments.
+        String spiffeId = "spiffe://spiffe.example.com/ns/sales/sa/trino";
+        assertTrue(stringUtil.validateString(StringUtil.VALIDATION_LOGINID_SPIFFE, spiffeId));
+        assertTrue(stringUtil.validateString(StringUtil.VALIDATION_LOGINID_SPIFFE, "user:name"));
+    }
+
+    @Test
+    public void testLoginIdRegExRejectsInvalidCharacter() {
+        assertFalse(stringUtil.validateString(StringUtil.VALIDATION_LOGINID, "user#name"));
+        assertFalse(stringUtil.validateString(StringUtil.VALIDATION_LOGINID_SPIFFE, "user#name"));
+    }
+
+    @Test
+    public void testGetLoginIdValidationRegExDefaultsToNonSpiffe() {
+        // With the config disabled (default), the effective regex is the non-SPIFFE one.
+        assertFalse(StringUtil.isSpiffeAsUsernameEnabled());
+        assertEquals(StringUtil.VALIDATION_LOGINID, StringUtil.getLoginIdValidationRegEx());
+    }
 
-	@Autowired
-	StringUtil stringUtil=new StringUtil();
-	
-	@Test
-	public void testToCamelCaseAllWords(){
-		String camelcase="hello world";
-		String camelCaseWords=stringUtil.toCamelCaseAllWords(camelcase);
-		Assert.assertEquals("Hello World",camelCaseWords);		
-	}
-	
-	@Test
-	public void testNullValidatePassword(){
-		String password=null;
-		String[] invalidValues={"aa","bb","aa12345dd"};
-		boolean value=stringUtil.validatePassword(password, invalidValues);
-		Assert.assertFalse(value);
-	}
-	
-	@Test
-	public void testValidatePassword(){
-		String password="Aa1234ddas12";
-		String[] invalidValues={"aa","bb","aa12345dd"};
-		boolean value=stringUtil.validatePassword(password, invalidValues);
-		Assert.assertTrue(password.length() >= 8);
-		Assert.assertTrue(value);
-	}
-	
-	@Test
-	public void testNotValidatePassword(){
-		String password="aassasavcvcvc";
-		String[] invalidValues={"aa","bb","aa12345dd"};
-		boolean value=stringUtil.validatePassword(password, invalidValues);
-		Assert.assertTrue(password.length() >= 8);
-		Assert.assertFalse(value);
-	}
-	
-	@Test
-	public void testIsEmptyValue(){		
-		String str="";
-		boolean value=stringUtil.isEmpty(str);
-		Assert.assertTrue(value);
-	}
-	@Test
-	public void testIsNullValue(){		
-		String str=null;
-		boolean value=stringUtil.isEmpty(str);
-		Assert.assertTrue(value);
-	}
-	
-	@Test
-	public void testIsWithValue(){		
-		String str="test value";
-		boolean value=stringUtil.isEmpty(str);
-		Assert.assertFalse(value);
-	}
-	
-	@Test
-	public void testEquals(){
-		String str1="test";
-		String str2="test";		
-		boolean value = stringUtil.equals(str1, str2);
-		Assert.assertTrue(value);
-	}
-	
-	@Test
-	public void testNormalizeEmail(){
-		String email="test.Demo@test.COM";
-		String lowercase=stringUtil.normalizeEmail(email);
-		String emailId=email.toLowerCase();
-		boolean value = emailId.equals(lowercase);
-		Assert.assertTrue(value);
-	}
-	
-	@Test
-	public void testNormalizeEmailIdNull(){
-		String email=null;
-		String lowercase=stringUtil.normalizeEmail(email);
-		Assert.assertEquals(lowercase,email);
-		Assert.assertNull(lowercase);
-	}
-	
-	@Test
-	public void testSplit(){
-		String str1 = "Test1";
-		String str2 = "Test2";
-		String str3 = "Test3";
-		String value = str1 + "," + str2 + "," + str3;
-		String[] stringArray = stringUtil.split(value);
-		 Assert.assertTrue(stringArray.length == 3);
-		 Assert.assertEquals(stringArray[0],str1);
-		 Assert.assertEquals(stringArray[1],str2);
-		 Assert.assertEquals(stringArray[2],str3);
-	}
-	
-	@Test
-	public void testTrim(){
-		String str="test";
-		String dataString = StringUtil.trim(str);
-		Assert.assertEquals(str,dataString);		
-	}
-	
-	@Test
-	public void testValidateEmailId(){
-		String email="rangerqa@apache.org";
-		boolean value=stringUtil.validateEmail(email);
-		Assert.assertTrue(email.length() < 128);
-		Assert.assertTrue(value);
-	}
-	
-	@Test
-	public void testNullEmailId(){
-		String email=null;				
-		boolean value=stringUtil.validateEmail(email);
-		Assert.assertFalse(value);
-	}
-	
-	@Test
-	public void testValidateString(){
-		String regExStr = "^[\\w]([\\-\\.\\w])+[\\w]+@[\\w]+[\\w\\-]+[\\w]*\\.([\\w]+[\\w\\-]+[\\w]*(\\.[a-z][a-z|0-9]*)?)$";
-		String str="test.test@gmail.com";
-		boolean value = stringUtil.validateString(regExStr, str);
-		Assert.assertTrue(value);
-	}	
-	
-	@Test
-	public void testNotValidateString(){
-		String regExStr = "^[\\w]([\\-\\.\\w])+[\\w]+[\\w]*\\.([\\w]+[\\w\\-]+[\\w]*(\\.[a-z][a-z|0-9]*)?)$";
-		String str="test.test@gmail.com";
-		boolean value = stringUtil.validateString(regExStr, str);
-		Assert.assertFalse(value);
-	}	
-	
-	@Test
-	public void testIsListEmpty(){
-		List<String> list=new ArrayList<String>();			
-		boolean listValue = stringUtil.isEmpty(list);
-		Assert.assertTrue(listValue);
-	}
-	
-	@Test
-	public void testIsListNotEmpty(){
-		List<String> list=new ArrayList<String>();
-		             list.add("a");
-		             list.add("b");
-		boolean listValue = stringUtil.isEmpty(list);
-		Assert.assertFalse(listValue);
-	}
-	
-	@Test
-	public void testIsValidName(){
-		String name="test";
-		boolean value = stringUtil.isValidName(name);
-		Assert.assertTrue(value);
-	}
-	
-	@Test
-	public void testIsValidNameNull(){
-		String name=null;
-		boolean value = stringUtil.isValidName(name);
-		Assert.assertFalse(value);
-	}
-}
\ No newline at end of file
+    @Test
+    public void testIsValidNameNull() {
+        boolean value = stringUtil.isValidName(null);
+        assertFalse(value);
+    }
+}
diff --git a/security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerHeaderPreAuthFilter.java b/security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerHeaderPreAuthFilter.java
index 675970dd2..5103d836b 100644
--- a/security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerHeaderPreAuthFilter.java
+++ b/security-admin/src/test/java/org/apache/ranger/security/web/filter/TestRangerHeaderPreAuthFilter.java
@@ -153,7 +153,7 @@ public void doFilter(ServletRequest req, ServletResponse res) {
     }
 
     @Test
-    public void testDoFilter_enabled_withSpiffeHeader_setsServiceAccountAuthentication() throws Exception {
+    public void testDoFilter_enabled_withSpiffeHeader_setsSpiffeIdAuthentication() throws Exception {
         PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_HEADER_AUTH_ENABLED, "true");
         PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_USERNAME_HEADER_NAME, "X-Forwarded-User");
         PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_SPIFFE_HEADER_NAME, "X-Spiffe-Id");
@@ -164,14 +164,17 @@ public void testDoFilter_enabled_withSpiffeHeader_setsServiceAccountAuthenticati
         filter.userMgr = userMgr;
         filter.initialize();
 
-        when(userMgr.getRolesByLoginId("nginx-ingress")).thenReturn(Collections.singletonList("ROLE_USER"));
+        // The full SPIFFE ID is the username in Ranger, so roles are looked up by the whole SPIFFE ID.
+        String spiffeId = "spiffe://prod-cluster.k8s.example.com/ns/ingress-nginx/sa/nginx-ingress";
+
+        when(userMgr.getRolesByLoginId(spiffeId)).thenReturn(Collections.singletonList("ROLE_USER"));
 
         HttpServletRequest  request  = mock(HttpServletRequest.class);
         HttpServletResponse response = mock(HttpServletResponse.class);
 
         when(request.getHeader("X-Forwarded-User")).thenReturn(null);
         // Realistic production SPIFFE ID: DNS-style Kubernetes cluster trust domain + namespace/service-account.
-        when(request.getHeader("X-Spiffe-Id")).thenReturn("spiffe://prod-cluster.k8s.example.com/ns/ingress-nginx/sa/nginx-ingress");
+        when(request.getHeader("X-Spiffe-Id")).thenReturn(spiffeId);
 
         FilterChain chain = new FilterChain() {
             @Override
@@ -182,7 +185,7 @@ public void doFilter(ServletRequest req, ServletResponse res) {
                 assertTrue(auth instanceof RangerAuthenticationToken);
                 RangerAuthenticationToken rangerAuth = (RangerAuthenticationToken) auth;
                 assertEquals(XXAuthSession.AUTH_TYPE_TRUSTED_PROXY, rangerAuth.getAuthType());
-                assertEquals("nginx-ingress", auth.getName());
+                assertEquals(spiffeId, auth.getName());
             }
         };
 
@@ -234,13 +237,15 @@ public void testDoFilter_enabled_multipleSpiffeHeaders_usesFirstValid() throws E
         filter.userMgr = userMgr;
         filter.initialize();
 
-        when(userMgr.getRolesByLoginId("service-sa")).thenReturn(Collections.singletonList("ROLE_USER"));
+        String spiffeId = "spiffe://my-cluster/ns/service-namespace/sa/service-sa";
+
+        when(userMgr.getRolesByLoginId(spiffeId)).thenReturn(Collections.singletonList("ROLE_USER"));
 
         HttpServletRequest  request  = mock(HttpServletRequest.class);
         HttpServletResponse response = mock(HttpServletResponse.class);
 
         when(request.getHeader("X-Spiffe-Id")).thenReturn("not-a-spiffe-id");
-        when(request.getHeader("X-Workload-Id")).thenReturn("spiffe://my-cluster/ns/service-namespace/sa/service-sa");
+        when(request.getHeader("X-Workload-Id")).thenReturn(spiffeId);
 
         FilterChain chain = new FilterChain() {
             @Override
@@ -248,7 +253,7 @@ public void doFilter(ServletRequest req, ServletResponse res) {
                 org.springframework.security.core.Authentication auth = SecurityContextHolder.getContext().getAuthentication();
 
                 assertNotNull(auth);
-                assertEquals("service-sa", auth.getName());
+                assertEquals(spiffeId, auth.getName());
             }
         };
 
@@ -280,7 +285,7 @@ public void testDoFilter_enabled_malformedSpiffeHeader_passesThrough() throws Ex
     }
 
     @Test
-    public void testDoFilter_enabled_specValidButNonConformingSpiffeHeader_passesThrough() throws Exception {
+    public void testDoFilter_enabled_specValidNonSpireLayoutSpiffeHeader_authenticatesFullId() throws Exception {
         PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_HEADER_AUTH_ENABLED, "true");
         PropertiesUtil.getPropertiesMap().put(RangerHeaderPreAuthFilter.PROP_SPIFFE_HEADER_NAME, "X-Spiffe-Id");
 
@@ -290,18 +295,31 @@ public void testDoFilter_enabled_specValidButNonConformingSpiffeHeader_passesThr
         filter.userMgr = userMgr;
         filter.initialize();
 
+        // Valid SPIFFE ID per the SPIFFE spec that does not use the SPIRE /ns/<ns>/sa/<sa> layout.
+        // The full SPIFFE ID is used as the authenticated principal.
+        String spiffeId = "spiffe://example.org/workload/frontend";
+
+        when(userMgr.getRolesByLoginId(spiffeId)).thenReturn(Collections.singletonList("ROLE_USER"));
+
         HttpServletRequest  request  = mock(HttpServletRequest.class);
         HttpServletResponse response = mock(HttpServletResponse.class);
-        FilterChain         chain    = mock(FilterChain.class);
 
-        // Valid SPIFFE ID per the SPIFFE spec, but not in the expected /ns/<ns>/sa/<sa> layout.
-        when(request.getHeader("X-Spiffe-Id")).thenReturn("spiffe://example.org/workload/frontend");
+        when(request.getHeader("X-Spiffe-Id")).thenReturn(spiffeId);
 
-        filter.doFilter(request, response, chain);
+        FilterChain chain = new FilterChain() {
+            @Override
+            public void doFilter(ServletRequest req, ServletResponse res) {
+                org.springframework.security.core.Authentication auth = SecurityContextHolder.getContext().getAuthentication();
 
-        verify(chain).doFilter(request, response);
-        verify(userMgr, never()).getRolesByLoginId(anyString());
-        assertNull(SecurityContextHolder.getContext().getAuthentication());
+                assertNotNull(auth);
+                assertTrue(auth instanceof RangerAuthenticationToken);
+                RangerAuthenticationToken rangerAuth = (RangerAuthenticationToken) auth;
+                assertEquals(XXAuthSession.AUTH_TYPE_TRUSTED_PROXY, rangerAuth.getAuthType());
+                assertEquals(spiffeId, auth.getName());
+            }
+        };
+
+        filter.doFilter(request, response, chain);
     }
 
     @Test