From f135eaa9af8485638f8c43745b2bcc8081324b17 Mon Sep 17 00:00:00 2001
From: srosse <none@none>
Date: Wed, 26 Nov 2014 15:52:10 +0100
Subject: [PATCH] OO-1331,CL-338: add an externalId to identity, add a endpoint
 to search managed identities

---
 .../admin/user/imp/TransientIdentity.java     |  6 +-
 .../olat/admin/user/imp/UpdateIdentity.java   |  5 +
 .../org/olat/basesecurity/BaseSecurity.java   |  9 ++
 .../basesecurity/BaseSecurityManager.java     | 23 ++++-
 .../olat/basesecurity/IdentityImpl.hbm.xml    |  3 +-
 .../org/olat/basesecurity/IdentityImpl.java   | 13 +++
 .../basesecurity/SearchIdentityParams.java    |  9 ++
 src/main/java/org/olat/core/id/Identity.java  |  5 +
 .../olat/core/util/mail/ui/EMailIdentity.java |  5 +
 .../course/run/preview/PreviewIdentity.java   |  5 +
 .../org/olat/user/restapi/ManagedUserVO.java  | 84 ++++++++++++++++
 .../java/org/olat/user/restapi/UserVO.java    |  9 ++
 .../org/olat/user/restapi/UserVOFactory.java  |  8 ++
 .../org/olat/user/restapi/UserWebService.java | 28 +++++-
 .../database/mysql/alter_10_0_0_to_10_1_0.sql |  4 +
 .../database/mysql/setupDatabase.sql          |  1 +
 .../oracle/alter_10_0_0_to_10_1_0.sql         |  4 +
 .../database/oracle/setupDatabase.sql         |  1 +
 .../postgresql/alter_10_0_0_to_10_1_0.sql     |  4 +
 .../database/postgresql/setupDatabase.sql     |  1 +
 .../basesecurity/BaseSecurityManagerTest.java | 31 ++++++
 .../java/org/olat/restapi/UserMgmtTest.java   | 97 +++++++++++++++----
 22 files changed, 331 insertions(+), 24 deletions(-)
 create mode 100644 src/main/java/org/olat/user/restapi/ManagedUserVO.java

diff --git a/src/main/java/org/olat/admin/user/imp/TransientIdentity.java b/src/main/java/org/olat/admin/user/imp/TransientIdentity.java
index 868905facd4..f359b30bd7c 100644
--- a/src/main/java/org/olat/admin/user/imp/TransientIdentity.java
+++ b/src/main/java/org/olat/admin/user/imp/TransientIdentity.java
@@ -54,7 +54,11 @@ public class TransientIdentity implements Identity, User {
 	public Long getKey() {
 		return null;
 	}
-
+	
+	@Override
+	public String getExternalId() {
+		return null;
+	}
 
 	@Override
 	public String getName() {
diff --git a/src/main/java/org/olat/admin/user/imp/UpdateIdentity.java b/src/main/java/org/olat/admin/user/imp/UpdateIdentity.java
index 15dab54adee..37984c96d4f 100644
--- a/src/main/java/org/olat/admin/user/imp/UpdateIdentity.java
+++ b/src/main/java/org/olat/admin/user/imp/UpdateIdentity.java
@@ -97,6 +97,11 @@ public class UpdateIdentity implements Identity {
 		return identity.getName();
 	}
 	
+	@Override
+	public String getExternalId() {
+		return identity.getExternalId();
+	}
+
 	@Override
 	public void setName(String name) {
 		//
diff --git a/src/main/java/org/olat/basesecurity/BaseSecurity.java b/src/main/java/org/olat/basesecurity/BaseSecurity.java
index fb3b6077832..35f256c9243 100644
--- a/src/main/java/org/olat/basesecurity/BaseSecurity.java
+++ b/src/main/java/org/olat/basesecurity/BaseSecurity.java
@@ -592,6 +592,15 @@ public interface BaseSecurity {
 	 */
 	public Identity saveIdentityName(Identity identity, String newName);
 	
+	/**
+	 * Set an external id if the identity is managed by an external system.
+	 * 
+	 * @param identity
+	 * @param externalId
+	 * @return
+	 */
+	public Identity setExternalId(Identity identity, String externalId);
+	
 	/**
 	 * Check if identity is visible. Deleted or login-denied users are not visible.
 	 * @param identityName
diff --git a/src/main/java/org/olat/basesecurity/BaseSecurityManager.java b/src/main/java/org/olat/basesecurity/BaseSecurityManager.java
index 66030631a2c..55b1fe8a18a 100644
--- a/src/main/java/org/olat/basesecurity/BaseSecurityManager.java
+++ b/src/main/java/org/olat/basesecurity/BaseSecurityManager.java
@@ -1410,11 +1410,14 @@ public class BaseSecurityManager extends BasicManager implements BaseSecurity {
 		Date createdBefore = params.getCreatedBefore();
 		Integer status = params.getStatus();
 		Collection<Long> identityKeys = params.getIdentityKeys();
+		Boolean managed = params.getManaged();
 		
 		// complex where clause only when values are available
 		if (login != null || (userproperties != null && !userproperties.isEmpty())
 				|| (identityKeys != null && !identityKeys.isEmpty()) || createdAfter != null	|| createdBefore != null
-				|| hasAuthProviders || hasGroups || hasPermissionOnResources || status != null) {
+				|| hasAuthProviders || hasGroups || hasPermissionOnResources || status != null
+				|| managed != null) {
+			
 			sb.append(" where ");		
 			boolean needsAnd = false;
 			boolean needsUserPropertiesJoin = false;
@@ -1508,6 +1511,15 @@ public class BaseSecurityManager extends BasicManager implements BaseSecurity {
 				sb.append("ident.key in (:identityKeys)");
 			}
 			
+			if(managed != null) {
+				needsAnd = checkAnd(sb, needsAnd);
+				if(managed.booleanValue()) {
+					sb.append("ident.externalId is not null");
+				} else {
+					sb.append("ident.externalId is null");
+				}	
+			}
+			
 			// append query for named security groups
 			if (hasGroups) {
 				SecurityGroup[] groups = params.getGroups();
@@ -1765,6 +1777,15 @@ public class BaseSecurityManager extends BasicManager implements BaseSecurity {
 		return reloadedIdentity;
 	}
 	
+	@Override
+	public Identity setExternalId(Identity identity, String externalId) {
+		IdentityImpl reloadedIdentity = loadForUpdate(identity); 
+		reloadedIdentity.setExternalId(externalId);
+		reloadedIdentity = dbInstance.getCurrentEntityManager().merge(reloadedIdentity);
+		dbInstance.commit();
+		return reloadedIdentity;
+	}
+	
 	
 	/**
 	 * Don't forget to commit/roolback the transaction as soon as possible
diff --git a/src/main/java/org/olat/basesecurity/IdentityImpl.hbm.xml b/src/main/java/org/olat/basesecurity/IdentityImpl.hbm.xml
index 7fcc94d0f58..91b997fc5d6 100644
--- a/src/main/java/org/olat/basesecurity/IdentityImpl.hbm.xml
+++ b/src/main/java/org/olat/basesecurity/IdentityImpl.hbm.xml
@@ -9,8 +9,9 @@
     </id>
     
     <version name="version" access="field" column="version" type="int"/>
-	<property  name="creationDate" column="creationdate" type="timestamp" />   
+	<property  name="creationDate" column="creationdate" type="timestamp" />
  	<property  name="lastLogin" column="lastlogin" type="timestamp" />
+ 	<property  name="externalId" column="external_id" type="string" />
  	
     <property name="name" type="string">
     	<column name="name" unique="true" length="128" not-null="true" index="name_idx2" />
diff --git a/src/main/java/org/olat/basesecurity/IdentityImpl.java b/src/main/java/org/olat/basesecurity/IdentityImpl.java
index 750a12efe39..4abbad5c195 100644
--- a/src/main/java/org/olat/basesecurity/IdentityImpl.java
+++ b/src/main/java/org/olat/basesecurity/IdentityImpl.java
@@ -45,6 +45,7 @@ public class IdentityImpl extends PersistentObject implements Identity, Identity
 	private String name;
 	private User user;
 	private Date lastLogin;
+	private String externalId;
 	/** status=[activ|deleted|permanent] */
 	private int status;
 	
@@ -70,6 +71,7 @@ public class IdentityImpl extends PersistentObject implements Identity, Identity
 	/**
 	 * @return String
 	 */
+	@Override
 	public String getName() {
 		return name;
 	}
@@ -77,6 +79,7 @@ public class IdentityImpl extends PersistentObject implements Identity, Identity
 	/**
 	 * @return User
 	 */
+	@Override
 	public User getUser() {
 		return user;
 	}
@@ -84,15 +87,25 @@ public class IdentityImpl extends PersistentObject implements Identity, Identity
 	/**
 	 * @return lastLogin
 	 */
+	@Override
 	public Date getLastLogin() {
 		return lastLogin;
 	}
 
+	public String getExternalId() {
+		return externalId;
+	}
+
+	public void setExternalId(String externalId) {
+		this.externalId = externalId;
+	}
+
 	/**
 	 * for hibernate only Sets the name.
 	 * 
 	 * @param name The name to set
 	 */
+	@Override
 	public void setName(String name) {
 		if (name.length() > NAME_MAXLENGTH)
 			throw new AssertException("field name of table o_bs_identity too long");
diff --git a/src/main/java/org/olat/basesecurity/SearchIdentityParams.java b/src/main/java/org/olat/basesecurity/SearchIdentityParams.java
index b1f53ad5be6..50e3daa1dc6 100644
--- a/src/main/java/org/olat/basesecurity/SearchIdentityParams.java
+++ b/src/main/java/org/olat/basesecurity/SearchIdentityParams.java
@@ -40,6 +40,7 @@ public class SearchIdentityParams {
 	private Date userLoginBefore;
 	private Integer status;
 	private Collection<Long> identityKeys;
+	private Boolean managed;
 	
 	public SearchIdentityParams() {
 		//
@@ -109,6 +110,14 @@ public class SearchIdentityParams {
 		this.authProviders = authProviders;
 	}
 	
+	public Boolean getManaged() {
+		return managed;
+	}
+
+	public void setManaged(Boolean managed) {
+		this.managed = managed;
+	}
+
 	public Date getCreatedAfter() {
 		return createdAfter;
 	}
diff --git a/src/main/java/org/olat/core/id/Identity.java b/src/main/java/org/olat/core/id/Identity.java
index db2d747293b..16bdc2245f3 100644
--- a/src/main/java/org/olat/core/id/Identity.java
+++ b/src/main/java/org/olat/core/id/Identity.java
@@ -55,6 +55,11 @@ public interface Identity extends CreateInfo, IdentityRef, Persistable {
      * @return The username, (login name, nickname..)
      */
    public String getName();
+   
+   /**
+    * @return Reference to an identifier in an external system
+    */
+   public String getExternalId();
 
     /**
 	 * @return The user object associated with this identity. The user
diff --git a/src/main/java/org/olat/core/util/mail/ui/EMailIdentity.java b/src/main/java/org/olat/core/util/mail/ui/EMailIdentity.java
index e0582f68caa..8963cadf3f8 100644
--- a/src/main/java/org/olat/core/util/mail/ui/EMailIdentity.java
+++ b/src/main/java/org/olat/core/util/mail/ui/EMailIdentity.java
@@ -48,6 +48,11 @@ class EMailIdentity implements Identity {
 	public Long getKey() {
 		return null;
 	}
+	
+	@Override
+	public String getExternalId() {
+		return null;
+	}
 
 	@Override
 	public boolean equalsByPersistableKey(Persistable persistable) {
diff --git a/src/main/java/org/olat/course/run/preview/PreviewIdentity.java b/src/main/java/org/olat/course/run/preview/PreviewIdentity.java
index 4f58477183d..4b1a66366b2 100644
--- a/src/main/java/org/olat/course/run/preview/PreviewIdentity.java
+++ b/src/main/java/org/olat/course/run/preview/PreviewIdentity.java
@@ -61,6 +61,11 @@ final class PreviewIdentity implements Identity, User {
 		return 2l;
 	}
 	
+	@Override
+	public String getExternalId() {
+		return null;
+	}
+
 	/**
 	 * @see org.olat.core.id.Identity#getName()
 	 */
diff --git a/src/main/java/org/olat/user/restapi/ManagedUserVO.java b/src/main/java/org/olat/user/restapi/ManagedUserVO.java
new file mode 100644
index 00000000000..6ec69fd95f9
--- /dev/null
+++ b/src/main/java/org/olat/user/restapi/ManagedUserVO.java
@@ -0,0 +1,84 @@
+/**
+ * <a href="http://www.openolat.org">
+ * OpenOLAT - Online Learning and Training</a><br>
+ * <p>
+ * Licensed under the Apache License, Version 2.0 (the "License"); <br>
+ * you may not use this file except in compliance with the License.<br>
+ * You may obtain a copy of the License at the
+ * <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
+ * <p>
+ * Unless required by applicable law or agreed to in writing,<br>
+ * software distributed under the License is distributed on an "AS IS" BASIS, <br>
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
+ * See the License for the specific language governing permissions and <br>
+ * limitations under the License.
+ * <p>
+ * Initial code contributed and copyrighted by<br>
+ * frentix GmbH, http://www.frentix.com
+ * <p>
+ */
+package org.olat.user.restapi;
+
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlRootElement;
+
+/**
+ * 
+ * Initial date: 26.11.2014<br>
+ * @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
+ *
+ */
+@XmlAccessorType(XmlAccessType.FIELD)
+@XmlRootElement(name = "managedUserVO")
+public class ManagedUserVO {
+
+	private Long key;
+	private String login;
+	private String externalId;
+
+	public ManagedUserVO() {
+		//make JAXB happy
+	}
+
+	public Long getKey() {
+		return key;
+	}
+
+	public void setKey(Long key) {
+		this.key = key;
+	}
+
+	public String getLogin() {
+		return login;
+	}
+
+	public void setLogin(String login) {
+		this.login = login;
+	}
+
+	public String getExternalId() {
+		return externalId;
+	}
+
+	public void setExternalId(String externalId) {
+		this.externalId = externalId;
+	}
+
+	@Override
+	public String toString() {
+		return "ManagedUserVO[key=" + key + ":name=" + login + ":externalId=" + externalId + "]";
+	}
+	
+	@Override
+	public boolean equals(Object obj) {
+		if(this == obj) {
+			return true;
+		}
+		if(obj instanceof ManagedUserVO) {
+			ManagedUserVO vo = (ManagedUserVO)obj;
+			return key != null && key.equals(vo.key);
+		}
+		return false;
+	}
+}
\ No newline at end of file
diff --git a/src/main/java/org/olat/user/restapi/UserVO.java b/src/main/java/org/olat/user/restapi/UserVO.java
index e5dae4caca5..b30341c23f8 100644
--- a/src/main/java/org/olat/user/restapi/UserVO.java
+++ b/src/main/java/org/olat/user/restapi/UserVO.java
@@ -43,6 +43,7 @@ public class UserVO {
 
 	private Long key;
 	private String login;
+	private String externalId;
 	private String password;
 	private String firstName;
 	private String lastName;
@@ -73,6 +74,14 @@ public class UserVO {
 		this.login = login;
 	}
 
+	public String getExternalId() {
+		return externalId;
+	}
+
+	public void setExternalId(String externalId) {
+		this.externalId = externalId;
+	}
+
 	public String getPassword() {
 		return password;
 	}
diff --git a/src/main/java/org/olat/user/restapi/UserVOFactory.java b/src/main/java/org/olat/user/restapi/UserVOFactory.java
index dde5fdf4d4b..dc4e0056923 100644
--- a/src/main/java/org/olat/user/restapi/UserVOFactory.java
+++ b/src/main/java/org/olat/user/restapi/UserVOFactory.java
@@ -134,6 +134,14 @@ public class UserVOFactory {
 		}
 		return userVO;
 	}
+	
+	public static ManagedUserVO getManaged(Identity identity) {
+		ManagedUserVO managedUserVo = new ManagedUserVO();
+		managedUserVo.setKey(identity.getKey());
+		managedUserVo.setLogin(identity.getName());
+		managedUserVo.setExternalId(identity.getExternalId());
+		return managedUserVo;
+	}
 
 	public static void post(User dbUser, UserVO user, Locale locale) {
 		UserManager um = UserManager.getInstance();
diff --git a/src/main/java/org/olat/user/restapi/UserWebService.java b/src/main/java/org/olat/user/restapi/UserWebService.java
index 479a0c37d3c..c71bd96f6bb 100644
--- a/src/main/java/org/olat/user/restapi/UserWebService.java
+++ b/src/main/java/org/olat/user/restapi/UserWebService.java
@@ -25,6 +25,7 @@ import static org.olat.restapi.security.RestSecurityHelper.getUserRequest;
 import static org.olat.restapi.security.RestSecurityHelper.isUserManager;
 import static org.olat.user.restapi.UserVOFactory.formatDbUserProperty;
 import static org.olat.user.restapi.UserVOFactory.get;
+import static org.olat.user.restapi.UserVOFactory.getManaged;
 import static org.olat.user.restapi.UserVOFactory.parseUserProperty;
 import static org.olat.user.restapi.UserVOFactory.post;
 
@@ -65,6 +66,7 @@ import org.olat.basesecurity.Authentication;
 import org.olat.basesecurity.BaseSecurity;
 import org.olat.basesecurity.BaseSecurityManager;
 import org.olat.basesecurity.IdentityShort;
+import org.olat.basesecurity.SearchIdentityParams;
 import org.olat.core.gui.components.form.ValidationError;
 import org.olat.core.gui.translator.PackageTranslator;
 import org.olat.core.gui.translator.Translator;
@@ -195,19 +197,39 @@ public class UserWebService {
 		return Response.ok(userVOs).build();
 	}
 	
+	@GET
+	@Path("managed")
+	@Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
+	public Response getManagedUsers(@Context HttpServletRequest httpRequest) {
+		
+		if(!isUserManager(httpRequest)) {
+			return Response.serverError().status(Status.UNAUTHORIZED).build();
+		}
+		
+		SearchIdentityParams params = new SearchIdentityParams();
+		params.setManaged(Boolean.TRUE);
+		List<Identity> identities = BaseSecurityManager.getInstance().getIdentitiesByPowerSearch(params, 0, -1);
+		int count = 0;
+		ManagedUserVO[] userVOs = new ManagedUserVO[identities.size()];
+		for(Identity identity:identities) {
+			userVOs[count++] = getManaged(identity);
+		}
+		return Response.ok(userVOs).build();
+	}
+	
 	/**
 	 * Creates and persists a new user entity
 	 * @response.representation.qname {http://www.example.com}userVO
 	 * @response.representation.mediaType application/xml, application/json
 	 * @response.representation.doc The user to persist
-   * @response.representation.example {@link org.olat.user.restapi.Examples#SAMPLE_USERVO}
+	 * @response.representation.example {@link org.olat.user.restapi.Examples#SAMPLE_USERVO}
 	 * @response.representation.200.mediaType application/xml, application/json
 	 * @response.representation.200.doc The persisted user
-   * @response.representation.200.example {@link org.olat.user.restapi.Examples#SAMPLE_USERVO}
+	 * @response.representation.200.example {@link org.olat.user.restapi.Examples#SAMPLE_USERVO}
 	 * @response.representation.401.doc The roles of the authenticated user are not sufficient
 	 * @response.representation.406.mediaType application/xml, application/json
 	 * @response.representation.406.doc The list of errors
-   * @response.representation.406.example {@link org.olat.restapi.support.vo.Examples#SAMPLE_ERRORVOes}
+	 * @response.representation.406.example {@link org.olat.restapi.support.vo.Examples#SAMPLE_ERRORVOes}
 	 * @param user The user to persist
 	 * @param request The HTTP request
 	 * @return the new persisted <code>User</code>
diff --git a/src/main/resources/database/mysql/alter_10_0_0_to_10_1_0.sql b/src/main/resources/database/mysql/alter_10_0_0_to_10_1_0.sql
index 0d59add2f9f..10557cfb39f 100644
--- a/src/main/resources/database/mysql/alter_10_0_0_to_10_1_0.sql
+++ b/src/main/resources/database/mysql/alter_10_0_0_to_10_1_0.sql
@@ -36,6 +36,10 @@ create index cer_uuid_idx on o_cer_certificate (c_uuid);
 
 alter table o_gp_business add column allowtoleave boolean default 1;
 
+
+alter table o_bs_identity add column external_id varchar(64);
+
+
 -- coaching
 create or replace view o_as_eff_statement_identity_v as (
    select
diff --git a/src/main/resources/database/mysql/setupDatabase.sql b/src/main/resources/database/mysql/setupDatabase.sql
index b17d9a63f0a..d6f5249631c 100644
--- a/src/main/resources/database/mysql/setupDatabase.sql
+++ b/src/main/resources/database/mysql/setupDatabase.sql
@@ -144,6 +144,7 @@ create table if not exists o_bs_identity (
    creationdate datetime,
    lastlogin datetime,
    name varchar(128) not null unique,
+   external_id varchar(64),
    status integer,
    fk_user_id bigint unique,
    primary key (id)
diff --git a/src/main/resources/database/oracle/alter_10_0_0_to_10_1_0.sql b/src/main/resources/database/oracle/alter_10_0_0_to_10_1_0.sql
index 8400f10795c..9381d1ad95e 100644
--- a/src/main/resources/database/oracle/alter_10_0_0_to_10_1_0.sql
+++ b/src/main/resources/database/oracle/alter_10_0_0_to_10_1_0.sql
@@ -36,6 +36,10 @@ create index cer_uuid_idx on o_cer_certificate (c_uuid);
 
 alter table o_gp_business add allowtoleave number default 1 not null;
 
+
+alter table o_bs_identity add external_id varchar2(64 char);
+
+
 --coaching
 create or replace view o_as_eff_statement_identity_v as (
    select
diff --git a/src/main/resources/database/oracle/setupDatabase.sql b/src/main/resources/database/oracle/setupDatabase.sql
index 91e0ed12935..4f9deb22983 100644
--- a/src/main/resources/database/oracle/setupDatabase.sql
+++ b/src/main/resources/database/oracle/setupDatabase.sql
@@ -161,6 +161,7 @@ CREATE TABLE o_bs_identity (
   creationdate date,
   lastlogin date,
   name varchar2(128 char) NOT NULL,
+  external_id varchar2(64 char),
   status number(11),
   fk_user_id number(20),
   CONSTRAINT u_o_bs_identity UNIQUE (name),
diff --git a/src/main/resources/database/postgresql/alter_10_0_0_to_10_1_0.sql b/src/main/resources/database/postgresql/alter_10_0_0_to_10_1_0.sql
index f8ab1e3ea8b..af50a4acaf6 100644
--- a/src/main/resources/database/postgresql/alter_10_0_0_to_10_1_0.sql
+++ b/src/main/resources/database/postgresql/alter_10_0_0_to_10_1_0.sql
@@ -36,6 +36,10 @@ create index cer_uuid_idx on o_cer_certificate (c_uuid);
 
 alter table o_gp_business add column allowtoleave bool not null default true;
 
+
+alter table o_bs_identity add column external_id varchar(64);
+
+
 --coaching
 create view o_as_eff_statement_identity_v as (
    select
diff --git a/src/main/resources/database/postgresql/setupDatabase.sql b/src/main/resources/database/postgresql/setupDatabase.sql
index 2fb4603d1c5..16c0c3dc5f2 100644
--- a/src/main/resources/database/postgresql/setupDatabase.sql
+++ b/src/main/resources/database/postgresql/setupDatabase.sql
@@ -142,6 +142,7 @@ create table o_bs_identity (
    creationdate timestamp,
    lastlogin timestamp,
    name varchar(128) not null unique,
+   external_id varchar(64),
    status integer,
    fk_user_id int8 unique,
    primary key (id)
diff --git a/src/test/java/org/olat/basesecurity/BaseSecurityManagerTest.java b/src/test/java/org/olat/basesecurity/BaseSecurityManagerTest.java
index cd8a6591323..32c55c4e893 100644
--- a/src/test/java/org/olat/basesecurity/BaseSecurityManagerTest.java
+++ b/src/test/java/org/olat/basesecurity/BaseSecurityManagerTest.java
@@ -492,6 +492,37 @@ public class BaseSecurityManagerTest extends OlatTestCase {
 		Assert.assertEquals(id, ids.get(0));
 	}
 	
+	@Test
+	public void testGetIdentityByPowerSearch_managed() {
+		String login = "pow-6-" + UUID.randomUUID();
+		String externalId = UUID.randomUUID().toString();
+		Identity id = JunitTestHelper.createAndPersistIdentityAsUser(login);
+		dbInstance.commitAndCloseSession();
+		securityManager.setExternalId(id, externalId);
+		dbInstance.commitAndCloseSession();
+		
+		//search managed
+		SearchIdentityParams params = new SearchIdentityParams();
+		params.setManaged(Boolean.TRUE);
+		List<Identity> managedIds = securityManager.getIdentitiesByPowerSearch(params, 0, -1);
+		Assert.assertNotNull(managedIds);
+		Assert.assertFalse(managedIds.isEmpty());
+		Assert.assertTrue(managedIds.contains(id));
+		for(Identity managedId:managedIds) {
+			Assert.assertNotNull(managedId.getExternalId());
+		}
+		
+		//search not managed
+		params.setManaged(Boolean.FALSE);
+		List<Identity> naturalIds = securityManager.getIdentitiesByPowerSearch(params, 0, -1);
+		Assert.assertNotNull(naturalIds);
+		Assert.assertFalse(naturalIds.contains(id));
+		for(Identity naturalId:naturalIds) {
+			Assert.assertNull(naturalId.getExternalId());
+		}
+	}
+	
+	
 	@Test
 	public void testGetIdentitiesByPowerSearchWithGroups() {
 		Identity id = JunitTestHelper.createAndPersistIdentityAsUser("user-1-" + UUID.randomUUID().toString());
diff --git a/src/test/java/org/olat/restapi/UserMgmtTest.java b/src/test/java/org/olat/restapi/UserMgmtTest.java
index b7998c5cb7b..7f6db6c5aad 100644
--- a/src/test/java/org/olat/restapi/UserMgmtTest.java
+++ b/src/test/java/org/olat/restapi/UserMgmtTest.java
@@ -111,6 +111,7 @@ import org.olat.test.JunitTestHelper;
 import org.olat.test.OlatJerseyTestCase;
 import org.olat.user.DisplayPortraitManager;
 import org.olat.user.UserManager;
+import org.olat.user.restapi.ManagedUserVO;
 import org.olat.user.restapi.PreferencesVO;
 import org.olat.user.restapi.RolesVO;
 import org.olat.user.restapi.StatusVO;
@@ -191,25 +192,25 @@ public class UserMgmtTest extends OlatJerseyTestCase {
 		dbInstance.intermediateCommit();
 		
 		//create learn group	
-    // 1) context one: learning groups
-    RepositoryEntry c1 = JunitTestHelper.createAndPersistRepositoryEntry();
-    // create groups without waiting list
-    g1externalId = UUID.randomUUID().toString();
-    g1 = businessGroupService.createBusinessGroup(null, "user-rest-g1", null, g1externalId, "all", 0, 10, false, false, c1);
-    g2 = businessGroupService.createBusinessGroup(null, "user-rest-g2", null, 0, 10, false, false, c1);
-    // members g1
-    businessGroupRelationDao.addRole(id1, g1, GroupRoles.coach.name());
-    businessGroupRelationDao.addRole(id2, g1, GroupRoles.participant.name());
-    // members g2
-    businessGroupRelationDao.addRole(id2, g2, GroupRoles.coach.name());
-    businessGroupRelationDao.addRole(id1, g2, GroupRoles.participant.name());
+		// 1) context one: learning groups
+		RepositoryEntry c1 = JunitTestHelper.createAndPersistRepositoryEntry();
+		// create groups without waiting list
+		g1externalId = UUID.randomUUID().toString();
+		g1 = businessGroupService.createBusinessGroup(null, "user-rest-g1", null, g1externalId, "all", 0, 10, false, false, c1);
+		g2 = businessGroupService.createBusinessGroup(null, "user-rest-g2", null, 0, 10, false, false, c1);
+		// members g1
+		businessGroupRelationDao.addRole(id1, g1, GroupRoles.coach.name());
+		businessGroupRelationDao.addRole(id2, g1, GroupRoles.participant.name());
+		// members g2
+		businessGroupRelationDao.addRole(id2, g2, GroupRoles.coach.name());
+		businessGroupRelationDao.addRole(id1, g2, GroupRoles.participant.name());
 
-    // 2) context two: right groups
-    RepositoryEntry c2 = JunitTestHelper.createAndPersistRepositoryEntry();
-    // groups
-    g3ExternalId = UUID.randomUUID().toString();
-    g3 = businessGroupService.createBusinessGroup(null, "user-rest-g3", null, g3ExternalId, "all", -1, -1, false, false, c2);
-    g4 = businessGroupService.createBusinessGroup(null, "user-rest-g4", null, -1, -1, false, false, c2);
+		// 2) context two: right groups
+		RepositoryEntry c2 = JunitTestHelper.createAndPersistRepositoryEntry();
+		// groups
+		g3ExternalId = UUID.randomUUID().toString();
+		g3 = businessGroupService.createBusinessGroup(null, "user-rest-g3", null, g3ExternalId, "all", -1, -1, false, false, c2);
+		g4 = businessGroupService.createBusinessGroup(null, "user-rest-g4", null, -1, -1, false, false, c2);
 		// members
 		businessGroupRelationDao.addRole(id1, g3, GroupRoles.participant.name());
 		businessGroupRelationDao.addRole(id2, g4, GroupRoles.participant.name());
@@ -438,6 +439,56 @@ public class UserMgmtTest extends OlatJerseyTestCase {
 		assertTrue(vo.getProperties().isEmpty());
 		conn.shutdown();
 	}
+	
+	@Test
+	public void testGetManagedUser() throws IOException, URISyntaxException {
+		String externalId = UUID.randomUUID().toString();
+		Identity managedId = JunitTestHelper.createAndPersistIdentityAsRndUser("managed-1");
+		dbInstance.commitAndCloseSession();
+		securityManager.setExternalId(managedId, externalId);
+		dbInstance.commitAndCloseSession();
+
+		RestConnection conn = new RestConnection();
+		assertTrue(conn.login("administrator", "openolat"));
+		
+		URI request = UriBuilder.fromUri(getContextURI()).path("users").path("managed").build();
+		HttpGet method = conn.createGet(request, MediaType.APPLICATION_JSON, true);
+		HttpResponse response = conn.execute(method);
+		assertEquals(200, response.getStatusLine().getStatusCode());
+		List<ManagedUserVO> managedUsers = parseManagedUserArray(response.getEntity().getContent());
+
+		boolean found = false;
+		for(ManagedUserVO managedUser:managedUsers) {
+			if(managedUser.getKey().equals(managedId.getKey())) {
+				found = true;
+				Assert.assertEquals(externalId, managedUser.getExternalId());
+			}
+			Assert.assertNotNull(managedUser.getExternalId());
+		}
+		Assert.assertTrue(found);
+		
+		conn.shutdown();
+	}
+	
+	@Test
+	public void testGetManagedUser_onlyUserManagers() throws IOException, URISyntaxException {
+		String externalId = UUID.randomUUID().toString();
+		Identity managedId = JunitTestHelper.createAndPersistIdentityAsRndUser("managed-1");
+		dbInstance.commitAndCloseSession();
+		securityManager.setExternalId(managedId, externalId);
+		dbInstance.commitAndCloseSession();
+
+		RestConnection conn = new RestConnection();
+		assertTrue(conn.login(id1.getName(), "A6B7C8"));
+		
+		URI request = UriBuilder.fromUri(getContextURI()).path("users").path("managed").build();
+		HttpGet method = conn.createGet(request, MediaType.APPLICATION_JSON, true);
+		HttpResponse response = conn.execute(method);
+		assertEquals(401, response.getStatusLine().getStatusCode());
+		EntityUtils.consume(response.getEntity());
+		
+		conn.shutdown();
+	}
 		
 	/**
 	 * Only print out the raw body of the response
@@ -1325,6 +1376,16 @@ public class UserMgmtTest extends OlatJerseyTestCase {
 		}
 	}
 	
+	protected List<ManagedUserVO> parseManagedUserArray(InputStream body) {
+		try {
+			ObjectMapper mapper = new ObjectMapper(jsonFactory); 
+			return mapper.readValue(body, new TypeReference<List<ManagedUserVO>>(){/* */});
+		} catch (Exception e) {
+			e.printStackTrace();
+			return null;
+		}
+	}
+	
 	protected List<GroupVO> parseGroupArray(InputStream body) {
 		try {
 			ObjectMapper mapper = new ObjectMapper(jsonFactory); 
-- 
GitLab