keycloak-memoizeit

Merge pull request #2295 from patriot1burke/master unsecure

2/29/2016 12:54:19 PM

Changes

Details

diff --git a/adapters/oidc/jetty/jetty-core/src/main/java/org/keycloak/adapters/jetty/core/AbstractKeycloakJettyAuthenticator.java b/adapters/oidc/jetty/jetty-core/src/main/java/org/keycloak/adapters/jetty/core/AbstractKeycloakJettyAuthenticator.java
index f8d7278..56d8f51 100755
--- a/adapters/oidc/jetty/jetty-core/src/main/java/org/keycloak/adapters/jetty/core/AbstractKeycloakJettyAuthenticator.java
+++ b/adapters/oidc/jetty/jetty-core/src/main/java/org/keycloak/adapters/jetty/core/AbstractKeycloakJettyAuthenticator.java
@@ -18,6 +18,8 @@
 package org.keycloak.adapters.jetty.core;
 
 import org.eclipse.jetty.security.DefaultUserIdentity;
+import org.eclipse.jetty.security.IdentityService;
+import org.eclipse.jetty.security.LoginService;
 import org.eclipse.jetty.security.ServerAuthException;
 import org.eclipse.jetty.security.UserAuthentication;
 import org.eclipse.jetty.security.authentication.DeferredAuthentication;
@@ -135,10 +137,44 @@ public abstract class AbstractKeycloakJettyAuthenticator extends LoginAuthentica
         return new DefaultUserIdentity(theSubject, principal, theRoles);
     }
 
+    private class DummyLoginService implements LoginService {
+        @Override
+        public String getName() {
+            return null;
+        }
+
+        @Override
+        public UserIdentity login(String username, Object credentials) {
+            return null;
+        }
+
+        @Override
+        public boolean validate(UserIdentity user) {
+            return false;
+        }
+
+        @Override
+        public IdentityService getIdentityService() {
+            return null;
+        }
+
+        @Override
+        public void setIdentityService(IdentityService service) {
+
+        }
+
+        @Override
+        public void logout(UserIdentity user) {
+
+        }
+    }
+
     @Override
     public void setConfiguration(AuthConfiguration configuration) {
         //super.setConfiguration(configuration);
         initializeKeycloak();
+        // need this so that getUserPrincipal does not throw NPE
+        _loginService = new DummyLoginService();
         String error = configuration.getInitParameter(FormAuthenticator.__FORM_ERROR_PAGE);
         setErrorPage(error);
     }
diff --git a/adapters/oidc/servlet-filter/src/main/java/org/keycloak/adapters/servlet/OIDCFilterSessionStore.java b/adapters/oidc/servlet-filter/src/main/java/org/keycloak/adapters/servlet/OIDCFilterSessionStore.java
index 8a3010d..70a67de 100755
--- a/adapters/oidc/servlet-filter/src/main/java/org/keycloak/adapters/servlet/OIDCFilterSessionStore.java
+++ b/adapters/oidc/servlet-filter/src/main/java/org/keycloak/adapters/servlet/OIDCFilterSessionStore.java
@@ -89,6 +89,7 @@ public class OIDCFilterSessionStore extends FilterSessionStore implements Adapte
 
     protected void cleanSession(HttpSession session) {
         session.removeAttribute(KeycloakAccount.class.getName());
+        session.removeAttribute(KeycloakSecurityContext.class.getName());
         clearSavedRequest(session);
     }
 
@@ -160,6 +161,7 @@ public class OIDCFilterSessionStore extends FilterSessionStore implements Adapte
         SerializableKeycloakAccount sAccount = new SerializableKeycloakAccount(roles, account.getPrincipal(), securityContext);
         HttpSession httpSession = request.getSession();
         httpSession.setAttribute(KeycloakAccount.class.getName(), sAccount);
+        httpSession.setAttribute(KeycloakSecurityContext.class.getName(), sAccount.getKeycloakSecurityContext());
         if (idMapper != null) idMapper.map(account.getKeycloakSecurityContext().getToken().getClientSession(),  account.getPrincipal().getName(), httpSession.getId());
         //String username = securityContext.getToken().getSubject();
         //log.fine("userSessionManagement.login: " + username);
diff --git a/adapters/oidc/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/tomcat/CatalinaSessionTokenStore.java b/adapters/oidc/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/tomcat/CatalinaSessionTokenStore.java
index 7734c2d..0a07e9e 100755
--- a/adapters/oidc/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/tomcat/CatalinaSessionTokenStore.java
+++ b/adapters/oidc/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/tomcat/CatalinaSessionTokenStore.java
@@ -69,12 +69,22 @@ public class CatalinaSessionTokenStore extends CatalinaAdapterSessionStore imple
         // just in case session got serialized
         if (session.getDeployment() == null) session.setCurrentRequestInfo(deployment, this);
 
-        if (session.isActive() && !session.getDeployment().isAlwaysRefreshToken()) return;
+        if (session.isActive() && !session.getDeployment().isAlwaysRefreshToken()) {
+            request.setAttribute(KeycloakSecurityContext.class.getName(), session);
+            request.setUserPrincipal(account.getPrincipal());
+            request.setAuthType("KEYCLOAK");
+            return;
+        }
 
         // FYI: A refresh requires same scope, so same roles will be set.  Otherwise, refresh will fail and token will
         // not be updated
         boolean success = session.refreshExpiredToken(false);
-        if (success && session.isActive()) return;
+        if (success && session.isActive()) {
+            request.setAttribute(KeycloakSecurityContext.class.getName(), session);
+            request.setUserPrincipal(account.getPrincipal());
+            request.setAuthType("KEYCLOAK");
+            return;
+        }
 
         // Refresh failed, so user is already logged out from keycloak. Cleanup and expire our session
         log.fine("Cleanup and expire session " + catalinaSession.getId() + " after failed refresh");
@@ -85,6 +95,8 @@ public class CatalinaSessionTokenStore extends CatalinaAdapterSessionStore imple
     }
 
     protected void cleanSession(Session catalinaSession) {
+        catalinaSession.getSession().removeAttribute(KeycloakSecurityContext.class.getName());
+        catalinaSession.getSession().removeAttribute(SerializableKeycloakAccount.class.getName());
         catalinaSession.getSession().removeAttribute(OidcKeycloakAccount.class.getName());
         catalinaSession.setPrincipal(null);
         catalinaSession.setAuthType(null);
@@ -164,6 +176,7 @@ public class CatalinaSessionTokenStore extends CatalinaAdapterSessionStore imple
         session.setPrincipal(principal);
         session.setAuthType("KEYCLOAK");
         session.getSession().setAttribute(SerializableKeycloakAccount.class.getName(), sAccount);
+        session.getSession().setAttribute(KeycloakSecurityContext.class.getName(), account.getKeycloakSecurityContext());
         String username = securityContext.getToken().getSubject();
         log.fine("userSessionManagement.login: " + username);
         this.sessionManagement.login(session);
diff --git a/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/ServletSessionTokenStore.java b/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/ServletSessionTokenStore.java
index 63c27d7..5db3ead 100755
--- a/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/ServletSessionTokenStore.java
+++ b/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/ServletSessionTokenStore.java
@@ -92,7 +92,8 @@ public class ServletSessionTokenStore implements AdapterTokenStore {
         } else {
             log.debug("Refresh failed. Account was not active. Returning null and invalidating Http session");
             try {
-                session.setAttribute(KeycloakUndertowAccount.class.getName(), null);
+                session.removeAttribute(KeycloakUndertowAccount.class.getName());
+                session.removeAttribute(KeycloakSecurityContext.class.getName());
                 session.invalidate();
             } catch (Exception e) {
                 log.debug("Failed to invalidate session, might already be invalidated");
@@ -106,6 +107,7 @@ public class ServletSessionTokenStore implements AdapterTokenStore {
         final ServletRequestContext servletRequestContext = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY);
         HttpSession session = getSession(true);
         session.setAttribute(KeycloakUndertowAccount.class.getName(), account);
+        session.setAttribute(KeycloakSecurityContext.class.getName(), account.getKeycloakSecurityContext());
         sessionManagement.login(servletRequestContext.getDeployment().getSessionManager());
     }
 
diff --git a/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowSessionTokenStore.java b/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowSessionTokenStore.java
index de57268..e578f85 100755
--- a/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowSessionTokenStore.java
+++ b/adapters/oidc/undertow/src/main/java/org/keycloak/adapters/undertow/UndertowSessionTokenStore.java
@@ -22,6 +22,7 @@ import io.undertow.server.HttpServerExchange;
 import io.undertow.server.session.Session;
 import io.undertow.util.Sessions;
 import org.jboss.logging.Logger;
+import org.keycloak.KeycloakSecurityContext;
 import org.keycloak.adapters.AdapterTokenStore;
 import org.keycloak.adapters.KeycloakDeployment;
 import org.keycloak.adapters.OidcKeycloakAccount;
@@ -101,6 +102,7 @@ public class UndertowSessionTokenStore implements AdapterTokenStore {
     public void saveAccountInfo(OidcKeycloakAccount account) {
         Session session = Sessions.getOrCreateSession(exchange);
         session.setAttribute(KeycloakUndertowAccount.class.getName(), account);
+        session.setAttribute(KeycloakSecurityContext.class.getName(), account.getKeycloakSecurityContext());
         sessionManagement.login(session.getSessionManager());
     }
 
@@ -111,6 +113,7 @@ public class UndertowSessionTokenStore implements AdapterTokenStore {
         KeycloakUndertowAccount account = (KeycloakUndertowAccount)session.getAttribute(KeycloakUndertowAccount.class.getName());
         if (account == null) return;
         session.removeAttribute(KeycloakUndertowAccount.class.getName());
+        session.removeAttribute(KeycloakSecurityContext.class.getName());
     }
 
     @Override
diff --git a/adapters/saml/jetty/jetty-core/src/main/java/org/keycloak/adapters/saml/jetty/AbstractSamlAuthenticator.java b/adapters/saml/jetty/jetty-core/src/main/java/org/keycloak/adapters/saml/jetty/AbstractSamlAuthenticator.java
index 07f2a40..eb17fee 100755
--- a/adapters/saml/jetty/jetty-core/src/main/java/org/keycloak/adapters/saml/jetty/AbstractSamlAuthenticator.java
+++ b/adapters/saml/jetty/jetty-core/src/main/java/org/keycloak/adapters/saml/jetty/AbstractSamlAuthenticator.java
@@ -18,6 +18,8 @@
 package org.keycloak.adapters.saml.jetty;
 
 import org.eclipse.jetty.security.DefaultUserIdentity;
+import org.eclipse.jetty.security.IdentityService;
+import org.eclipse.jetty.security.LoginService;
 import org.eclipse.jetty.security.ServerAuthException;
 import org.eclipse.jetty.security.UserAuthentication;
 import org.eclipse.jetty.security.authentication.DeferredAuthentication;
@@ -135,12 +137,46 @@ public abstract class AbstractSamlAuthenticator extends LoginAuthenticator {
 
     }
 
+    private class DummyLoginService implements LoginService {
+        @Override
+        public String getName() {
+            return null;
+        }
+
+        @Override
+        public UserIdentity login(String username, Object credentials) {
+            return null;
+        }
+
+        @Override
+        public boolean validate(UserIdentity user) {
+            return false;
+        }
+
+        @Override
+        public IdentityService getIdentityService() {
+            return null;
+        }
+
+        @Override
+        public void setIdentityService(IdentityService service) {
+
+        }
+
+        @Override
+        public void logout(UserIdentity user) {
+
+        }
+    }
+
 
 
     @Override
     public void setConfiguration(AuthConfiguration configuration) {
         //super.setConfiguration(configuration);
         initializeKeycloak();
+        // need this so that getUserPrincipal does not throw NPE
+        _loginService = new DummyLoginService();
         String error = configuration.getInitParameter(FormAuthenticator.__FORM_ERROR_PAGE);
         setErrorPage(error);
     }
diff --git a/adapters/saml/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/AbstractSamlAuthenticatorValve.java b/adapters/saml/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/AbstractSamlAuthenticatorValve.java
index 51bdb4b..aa75439 100755
--- a/adapters/saml/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/AbstractSamlAuthenticatorValve.java
+++ b/adapters/saml/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/saml/AbstractSamlAuthenticatorValve.java
@@ -167,9 +167,9 @@ public abstract class AbstractSamlAuthenticatorValve extends FormAuthenticator i
     @Override
     public void invoke(Request request, Response response) throws IOException, ServletException {
         log.fine("*********************** SAML ************");
+        CatalinaHttpFacade facade = new CatalinaHttpFacade(response, request);
+        SamlDeployment deployment = deploymentContext.resolveDeployment(facade);
         if (request.getRequestURI().substring(request.getContextPath().length()).endsWith("/saml")) {
-            CatalinaHttpFacade facade = new CatalinaHttpFacade(response, request);
-            SamlDeployment deployment = deploymentContext.resolveDeployment(facade);
             if (deployment != null && deployment.isConfigured()) {
                 SamlSessionStore tokenStore = getSessionStore(request, facade, deployment);
                 SamlAuthenticator authenticator = new CatalinaSamlEndpoint(facade, deployment, tokenStore);
@@ -180,6 +180,7 @@ public abstract class AbstractSamlAuthenticatorValve extends FormAuthenticator i
         }
 
         try {
+            getSessionStore(request, facade, deployment).isLoggedIn();  // sets request UserPrincipal if logged in.  we do this so that the UserPrincipal is available on unsecured, unconstrainted URLs
             super.invoke(request, response);
         } finally {
         }
diff --git a/core/src/main/java/org/keycloak/KeycloakSecurityContext.java b/core/src/main/java/org/keycloak/KeycloakSecurityContext.java
index 3806ffe..118fd1b 100755
--- a/core/src/main/java/org/keycloak/KeycloakSecurityContext.java
+++ b/core/src/main/java/org/keycloak/KeycloakSecurityContext.java
@@ -28,6 +28,9 @@ import java.io.ObjectOutputStream;
 import java.io.Serializable;
 
 /**
+ * Available in secured requests under HttpServlerRequest.getAttribute()
+ * Also available in HttpSession.getAttribute under the classname of this class
+ *
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
  */
diff --git a/docbook/auth-server-docs/reference/en/en-US/modules/adapter-context.xml b/docbook/auth-server-docs/reference/en/en-US/modules/adapter-context.xml
new file mode 100755
index 0000000..cfcf18f
--- /dev/null
+++ b/docbook/auth-server-docs/reference/en/en-US/modules/adapter-context.xml
@@ -0,0 +1,12 @@
+<chapter>
+    <title>KeycloakSecurityContext</title>
+    <para>
+        The <literal>KeycloakSecurityContext</literal> interface is available if you need to look at the access token directly.  This context is also useful if you need to
+        get the encoded access token so you can make additional REST invocations.  In servlet environments it is available in secured invocations as an attribute in HttpServletRequest.
+        Or, it is available in secure and insecure requests in the HttpSession for browser apps.
+        <programlisting>
+            httpServletRequest.getAttribute(KeycloakSecurityContext.class.getName());
+            httpServletRequest.getSession().getAttribute(KeycloakSecurityContext.class.getName());
+        </programlisting>
+    </para>
+</chapter>
\ No newline at end of file
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java
index f03b55a..7b0d9d3 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/AdapterTestStrategy.java
@@ -138,6 +138,12 @@ public class AdapterTestStrategy extends ExternalResource {
         String pageSource = driver.getPageSource();
         System.out.println(pageSource);
         Assert.assertTrue(pageSource.contains("parameter=hello"));
+        // test that user principal and KeycloakSecurityContext available
+        driver.navigate().to(APP_SERVER_BASE_URL + "/input-portal/insecure");
+        System.out.println("insecure: ");
+        System.out.println(driver.getPageSource());
+        Assert.assertTrue(driver.getPageSource().contains("Insecure Page"));
+        if (System.getProperty("insecure.user.principal.unsupported") == null) Assert.assertTrue(driver.getPageSource().contains("UserPrincipal"));
 
         // test logout
 
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/FilterAdapterTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/FilterAdapterTest.java
index 9637617..a533133 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/FilterAdapterTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/FilterAdapterTest.java
@@ -100,6 +100,7 @@ public class FilterAdapterTest {
 
     @Test
     public void testSavedPostRequest() throws Exception {
+        System.setProperty("insecure.user.principal.unsupported", "true");
         testStrategy.testSavedPostRequest();
     }
 
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/InputServlet.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/InputServlet.java
index c0135ef..b6a0bd5 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/InputServlet.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/adapter/InputServlet.java
@@ -17,6 +17,9 @@
 
 package org.keycloak.testsuite.adapter;
 
+import org.junit.Assert;
+import org.keycloak.KeycloakSecurityContext;
+
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
@@ -35,6 +38,17 @@ public class InputServlet extends HttpServlet {
     @Override
     protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
         String appBase = System.getProperty("app.server.base.url", "http://localhost:8081");
+        if (req.getRequestURI().endsWith("insecure")) {
+            if (System.getProperty("insecure.user.principal.unsupported") == null) Assert.assertNotNull(req.getUserPrincipal());
+            if (System.getProperty("insecure.user.principal.unsupported") == null) Assert.assertNotNull(req.getAttribute(KeycloakSecurityContext.class.getName()));
+            resp.setContentType("text/html");
+            PrintWriter pw = resp.getWriter();
+            pw.printf("<html><head><title>%s</title></head><body>", "Insecure Page");
+            if (req.getUserPrincipal() != null) pw.printf("UserPrincipal: " + req.getUserPrincipal().getName());
+            pw.print("</body></html>");
+            pw.flush();
+            return;
+        }
         String actionUrl = appBase + "/input-portal/secured/post";
 
 
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/ConcurrencyTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/ConcurrencyTest.java
index d47e78f..daaf5a7 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/ConcurrencyTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/ConcurrencyTest.java
@@ -48,8 +48,8 @@ public class ConcurrencyTest extends AbstractClientTest {
 
     private static final Logger log = Logger.getLogger(ConcurrencyTest.class);
 
-    private static final int DEFAULT_THREADS = 10;
-    private static final int DEFAULT_ITERATIONS = 100;
+    private static final int DEFAULT_THREADS = 5;
+    private static final int DEFAULT_ITERATIONS = 20;
 
     // If enabled only one request is allowed at the time. Useful for checking that test is working.
     private static final boolean SYNCHRONIZED = false;
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/InputServlet.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/InputServlet.java
index b6eb54a..57c8e48 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/InputServlet.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/InputServlet.java
@@ -17,6 +17,9 @@
 
 package org.keycloak.testsuite.keycloaksaml;
 
+import org.junit.Assert;
+import org.keycloak.KeycloakSecurityContext;
+
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
@@ -34,6 +37,16 @@ public class InputServlet extends HttpServlet {
     @Override
     protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
         String appBase = System.getProperty("app.server.base.url", "http://localhost:8081");
+        if (req.getRequestURI().endsWith("insecure")) {
+            if (System.getProperty("insecure.user.principal.unsupported") == null) Assert.assertNotNull(req.getUserPrincipal());
+            resp.setContentType("text/html");
+            PrintWriter pw = resp.getWriter();
+            pw.printf("<html><head><title>%s</title></head><body>", "Insecure Page");
+            if (req.getUserPrincipal() != null) pw.printf("UserPrincipal: " + req.getUserPrincipal().getName());
+            pw.print("</body></html>");
+            pw.flush();
+            return;
+        }
         String actionUrl = appBase + "/input-portal/secured/post";
 
 
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SamlAdapterTestStrategy.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SamlAdapterTestStrategy.java
index 948d9ca..a9e95bb 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SamlAdapterTestStrategy.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/keycloaksaml/SamlAdapterTestStrategy.java
@@ -152,6 +152,12 @@ public class SamlAdapterTestStrategy  extends ExternalResource {
         String pageSource = driver.getPageSource();
         System.out.println(pageSource);
         Assert.assertTrue(pageSource.contains("parameter=hello"));
+        // test that user principal and KeycloakSecurityContext available
+        driver.navigate().to(APP_SERVER_BASE_URL + "/input-portal/insecure");
+        System.out.println("insecure: ");
+        System.out.println(driver.getPageSource());
+        Assert.assertTrue(driver.getPageSource().contains("Insecure Page"));
+        if (System.getProperty("insecure.user.principal.unsupported") == null) Assert.assertTrue(driver.getPageSource().contains("UserPrincipal"));
 
         // test logout
 
diff --git a/testsuite/integration/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration/src/test/resources/META-INF/keycloak-server.json
index 950fd22..e9bb97f 100755
--- a/testsuite/integration/src/test/resources/META-INF/keycloak-server.json
+++ b/testsuite/integration/src/test/resources/META-INF/keycloak-server.json
@@ -86,7 +86,7 @@
     "connectionsInfinispan": {
         "default": {
             "clustered": "${keycloak.connectionsInfinispan.clustered:false}",
-            "async": "${keycloak.connectionsInfinispan.async:true}",
+            "async": "${keycloak.connectionsInfinispan.async:false}",
             "sessionsOwners": "${keycloak.connectionsInfinispan.sessionsOwners:2}"
         }
     }