diff --git a/scimgateway/src/main/java/com/asena/scimgateway/connector/AzureConnector.java b/scimgateway/src/main/java/com/asena/scimgateway/connector/AzureConnector.java index 78f12b9..da108e9 100644 --- a/scimgateway/src/main/java/com/asena/scimgateway/connector/AzureConnector.java +++ b/scimgateway/src/main/java/com/asena/scimgateway/connector/AzureConnector.java @@ -11,11 +11,13 @@ import com.asena.scimgateway.model.Attribute; import com.asena.scimgateway.model.ConnectionProperty; import com.asena.scimgateway.model.EntryTypeMapping; +import com.asena.scimgateway.model.ModificationStep; import com.asena.scimgateway.model.RemoteSystem; import com.asena.scimgateway.model.Script; import com.asena.scimgateway.model.ConnectionProperty.ConnectionPropertyType; import com.asena.scimgateway.utils.ConnectorUtil; import com.asena.scimgateway.utils.JSONUtil; +import com.asena.scimgateway.utils.ModificationUtil; import com.fasterxml.jackson.databind.ObjectMapper; import com.jayway.jsonpath.DocumentContext; import com.jayway.jsonpath.JsonPath; @@ -25,6 +27,8 @@ public class AzureConnector implements IConnector { private String oauthURL; private String oauthUser; private String oauthPassword; + private String userExpandProperties; + private String groupExpandProperties; @Override public RemoteSystem getRemoteSystemTemplate() { @@ -40,6 +44,8 @@ public RemoteSystem getRemoteSystemTemplate() { "OAuth user name", true, ConnectionPropertyType.STRING)); retSystem.addProperty(new ConnectionProperty("oauth.password", "adminpassword", "Oauth user password", false, ConnectionPropertyType.STRING)); + retSystem.addProperty(new ConnectionProperty("azure.user.expand", "memberOf", "Attributes for Graph API expand (user)", false, ConnectionPropertyType.STRING)); + retSystem.addProperty(new ConnectionProperty("azure.group.expand", "memberOf", "Attributes for Graph API expand (group)", false, ConnectionPropertyType.STRING)); retSystem.setType("Microsoft Azure Active Directory"); retSystem.addAttribute(new Attribute("displayName", "displayName", "Displayname")); @@ -112,6 +118,12 @@ public void setupConnector(RemoteSystem rs) { case "oauth.password": this.oauthPassword = cp.getValue(); break; + case "azure.user.expand": + this.userExpandProperties = cp.getValue(); + break; + case "azure.group.expand": + this.groupExpandProperties = cp.getValue(); + break; } } } @@ -134,12 +146,12 @@ public String createEntity(String entity, HashMap data) throws E } @Override - public String updateEntity(String entity, HashMap data) throws Exception { + public String updateEntity(String entity, ModificationStep ms) throws Exception { switch (entity) { case "Users": - return updateEntityInAzure(entity, data); + return updateEntityInAzure(entity, ms); case "Groups": - return updateEntityInAzure(entity, data); + return updateEntityInAzure(entity, ms); default: throw new InternalErrorException("No entity passed to Azure connector!"); } @@ -198,7 +210,9 @@ private HashMap getEntityFromAzure(String entity, HashMap map = new HashMap<>(); @@ -217,7 +231,7 @@ private List> getEntitiesFromAzure(String entity) throws hc.setUserName(this.oauthUser); hc.setPassword(this.oauthPassword); - String result = hc.get(this.baseURL + "/v1.0/" + entity); + String result = hc.get(getURL(entity, null, true)); ObjectMapper mapper = new ObjectMapper(); HashMap map = new HashMap<>(); @@ -239,7 +253,7 @@ private String createEntityInAzure(String entity, HashMap data) DocumentContext jsonContext = JsonPath.parse(data); - String retUser = hc.post(this.baseURL + "/v1.0/" + entity, jsonContext.jsonString()); + String retUser = hc.post(getURL(entity, null, false), jsonContext.jsonString()); ObjectMapper mapper = new ObjectMapper(); HashMap map = new HashMap<>(); @@ -264,16 +278,17 @@ private boolean deleteEntityInAzure(String entity, HashMap data) hc.setPassword(this.oauthPassword); hc.setExpectedResponseCode(204); - hc.delete(this.baseURL + "/v1.0/" + entity + "/" + userId); + hc.delete(getURL(entity, userId, false)); return true; } - private String updateEntityInAzure(String entity, HashMap data) throws Exception { - String userId = (String) ConnectorUtil.getAttributeValue(getNameId(), data); + private String updateEntityInAzure(String entity, ModificationStep ms) throws Exception { + String userId = (String) ms.findValueByAttribute(getNameId()); + HashMap data = ModificationUtil.collectSimpleModifications(ms); if (userId == null) { throw new InternalErrorException("UserID not found in read mapping!"); } - + OAuthInterceptor oi = new OAuthInterceptor(this.oauthUser, this.oauthPassword, this.oauthURL); oi.addBody("resource", this.baseURL); @@ -285,9 +300,30 @@ private String updateEntityInAzure(String entity, HashMap data) DocumentContext jsonContext = JsonPath.parse(data); - hc.patch(this.baseURL + "/v1.0/" + entity + "/" + userId, jsonContext.jsonString()); + hc.patch(getURL(entity, userId, false), jsonContext.jsonString()); return userId; } - + + private String getURL(String entity, String userId, boolean includeParams) { + String retURL = this.baseURL + "/v1.0/" + entity; + if (userId != null) { + retURL += "/" + userId; + } + if (includeParams) { + switch (entity) { + case "Users": + if ((this.userExpandProperties != null) && (!this.userExpandProperties.isEmpty())) { + retURL += "?$expand=" + this.userExpandProperties; + } + break; + case "Groups": + if ((this.groupExpandProperties != null) && (!this.groupExpandProperties.isEmpty())) { + retURL += "?$expand=" + this.groupExpandProperties; + } + break; + } + } + return retURL; + } } \ No newline at end of file diff --git a/scimgateway/src/main/java/com/asena/scimgateway/connector/IConnector.java b/scimgateway/src/main/java/com/asena/scimgateway/connector/IConnector.java index 79be547..65479ae 100644 --- a/scimgateway/src/main/java/com/asena/scimgateway/connector/IConnector.java +++ b/scimgateway/src/main/java/com/asena/scimgateway/connector/IConnector.java @@ -2,6 +2,8 @@ import java.util.HashMap; import java.util.List; + +import com.asena.scimgateway.model.ModificationStep; import com.asena.scimgateway.model.RemoteSystem; public interface IConnector { @@ -9,7 +11,7 @@ public interface IConnector { public void setupConnector(RemoteSystem rs); public String getNameId(); public String createEntity(String entity, HashMap data) throws Exception; - public String updateEntity(String entity, HashMap data) throws Exception; + public String updateEntity(String entity, ModificationStep ms) throws Exception; public boolean deleteEntity(String entity, HashMap data) throws Exception; public List> getEntities(String entity) throws Exception; public HashMap getEntity(String entity, HashMap data) throws Exception; diff --git a/scimgateway/src/main/java/com/asena/scimgateway/connector/LDAPConnector.java b/scimgateway/src/main/java/com/asena/scimgateway/connector/LDAPConnector.java index a6fddc6..ad4307f 100644 --- a/scimgateway/src/main/java/com/asena/scimgateway/connector/LDAPConnector.java +++ b/scimgateway/src/main/java/com/asena/scimgateway/connector/LDAPConnector.java @@ -13,6 +13,7 @@ import com.asena.scimgateway.model.Attribute; import com.asena.scimgateway.model.ConnectionProperty; import com.asena.scimgateway.model.EntryTypeMapping; +import com.asena.scimgateway.model.ModificationStep; import com.asena.scimgateway.model.RemoteSystem; import com.asena.scimgateway.model.ConnectionProperty.ConnectionPropertyType; import com.asena.scimgateway.utils.ConnectorUtil; @@ -198,24 +199,24 @@ private void upsertAttributeArray(LdapConnection conn, String dn, String attr, L conn.modify(dn, modAttr); } - private String updateEntity(HashMap data) { + private String updateEntity(ModificationStep ms) { LdapConnection connection = null; String retStr = null; try { connection = ldapConnect(); - retStr = (String)ConnectorUtil.getAttributeValue(nameId, data); - for (String key : data.keySet()) { - if (!key.equals(nameId)) { - Object objData = data.get(key); + retStr = (String) ms.findValueByAttribute(nameId); + for (com.asena.scimgateway.model.Modification m : ms.getModifications()) { + if (!m.getAttributeName().equals(nameId)) { + Object objData = m.getValue(); if (objData instanceof NativeArray) { NativeArray tempArr = (NativeArray) objData; List lstValues = new ArrayList(); for (int i = 0; i < tempArr.size(); i++) { lstValues.add((String) tempArr.get(i)); } - upsertAttributeArray(connection, retStr, key, lstValues); + upsertAttributeArray(connection, retStr, m.getAttributeName(), lstValues); } else { - upsertAttribute(connection, retStr, key, (String)data.get(key)); + upsertAttribute(connection, retStr, m.getAttributeName(), (String)objData); } } } @@ -229,8 +230,8 @@ private String updateEntity(HashMap data) { } @Override - public String updateEntity(String entity, HashMap data) throws Exception { - return updateEntity(data); + public String updateEntity(String entity, ModificationStep ms) throws Exception { + return updateEntity(ms); } @Override diff --git a/scimgateway/src/main/java/com/asena/scimgateway/connector/NoOpConnector.java b/scimgateway/src/main/java/com/asena/scimgateway/connector/NoOpConnector.java index 4fa5ed0..7f843df 100644 --- a/scimgateway/src/main/java/com/asena/scimgateway/connector/NoOpConnector.java +++ b/scimgateway/src/main/java/com/asena/scimgateway/connector/NoOpConnector.java @@ -4,8 +4,10 @@ import java.util.HashMap; import java.util.List; +import com.asena.scimgateway.exception.InternalErrorException; import com.asena.scimgateway.model.Attribute; import com.asena.scimgateway.model.ConnectionProperty; +import com.asena.scimgateway.model.ModificationStep; import com.asena.scimgateway.model.RemoteSystem; import com.asena.scimgateway.model.ConnectionProperty.ConnectionPropertyType; @@ -32,8 +34,10 @@ public String createEntity(String entity, HashMap data) throws E } @Override - public String updateEntity(String entity, HashMap data) throws Exception { - return (String) data.get(this.nameId); + public String updateEntity(String entity, ModificationStep ms) throws Exception { + // throw new InternalErrorException("NOT SUPPORTED!"); + + return ms.getId(); } @Override @@ -43,8 +47,8 @@ public boolean deleteEntity(String entity, HashMap data) { @Override public List> getEntities(String entity) throws Exception { - List> ret = new ArrayList<>(); - HashMap retObj = new HashMap<>(); + List> ret = new ArrayList<>(); + HashMap retObj = new HashMap<>(); retObj.put("noop", "test"); ret.add(retObj); return ret; @@ -52,7 +56,7 @@ public List> getEntities(String entity) throws Exception @Override public HashMap getEntity(String entity, HashMap data) throws Exception { - HashMap retObj = new HashMap<>(); + HashMap retObj = new HashMap<>(); retObj.put("noop", "test"); return retObj; } @@ -61,5 +65,5 @@ public HashMap getEntity(String entity, HashMap public String getNameId() { return nameId; } - + } \ No newline at end of file diff --git a/scimgateway/src/main/java/com/asena/scimgateway/connector/OneIdentityConnector.java b/scimgateway/src/main/java/com/asena/scimgateway/connector/OneIdentityConnector.java new file mode 100644 index 0000000..264fec0 --- /dev/null +++ b/scimgateway/src/main/java/com/asena/scimgateway/connector/OneIdentityConnector.java @@ -0,0 +1,190 @@ +package com.asena.scimgateway.connector; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Set; + +import com.asena.scimgateway.connector.utils.oneidentity.OneIdentityInterceptor; +import com.asena.scimgateway.exception.InternalErrorException; +import com.asena.scimgateway.http.BasicAuthInterceptor; +import com.asena.scimgateway.http.HTTPClient; +import com.asena.scimgateway.model.Attribute; +import com.asena.scimgateway.model.ConnectionProperty; +import com.asena.scimgateway.model.EntryTypeMapping; +import com.asena.scimgateway.model.ModificationStep; +import com.asena.scimgateway.model.RemoteSystem; +import com.asena.scimgateway.model.ConnectionProperty.ConnectionPropertyType; +import com.asena.scimgateway.utils.ConnectorUtil; +import com.asena.scimgateway.utils.JSONUtil; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.jayway.jsonpath.DocumentContext; +import com.jayway.jsonpath.JsonPath; + +public class OneIdentityConnector implements IConnector { + + private String host; + private String apiEndPoint; + private String authEndPoint; + private String userName; + private String password; + private String authString; + private String apiEntityEndPoint; + + @Override + public RemoteSystem getRemoteSystemTemplate() { + RemoteSystem retSystem = new RemoteSystem(); + retSystem.addProperty(new ConnectionProperty("host", "http://10.10.110.42", "Hostname of the app server", false, + ConnectionPropertyType.STRING)); + + retSystem.addProperty(new ConnectionProperty("apiendpoint", "/AppServer/api/entities/", + "API endpoint of the AppServer", false, ConnectionPropertyType.STRING)); + retSystem.addProperty(new ConnectionProperty("authstring", "Module=DialogUser;User=;Password=", + "Auth string", false, ConnectionPropertyType.STRING)); + retSystem.addProperty(new ConnectionProperty("authendpoint", "/AppServer/auth/apphost", + "Endpoint of the Auth API", true, ConnectionPropertyType.STRING)); + retSystem.addProperty(new ConnectionProperty("username", "viadmin", "Basic auth user name", false, + ConnectionPropertyType.STRING)); + retSystem.addProperty(new ConnectionProperty("password", "pass", "Basic auth user password", false, + ConnectionPropertyType.STRING)); + retSystem.addProperty(new ConnectionProperty("apientityendpoint", "/AppServer/api/entity/", + "API endpoint for single entitiy calls", false, ConnectionPropertyType.STRING)); + retSystem.setType("OneIdentity"); + + retSystem.addAttribute(new Attribute("display", "display", "display name")); + retSystem.addAttribute(new Attribute("longDisplay", "longDisplay", "long display name")); + retSystem.addAttribute(new Attribute("values.CentralAccount", "values.CentralAccount", "account name")); + retSystem.addAttribute(new Attribute("values.UID_Person", "values.UID_Person", "id")); + + EntryTypeMapping emUser = new EntryTypeMapping("Users"); + emUser.addWriteMapping(new Attribute("$.userName", "values.CentralAccount", "")); + emUser.addWriteMapping(new Attribute("$.userName", "uid", "")); + + emUser.addReadMapping(new Attribute("values.CentralAccount", "$.userName", "")); + emUser.addReadMapping(new Attribute("values.UID_Person", "$.id", "")); + retSystem.addEntryTypeMapping(emUser); + + return retSystem; + } + + @Override + public void setupConnector(RemoteSystem rs) { + Set conns = rs.getProperties(); + for (ConnectionProperty cp : conns) { + switch (cp.getKey()) { + case "host": + this.host = cp.getValue(); + break; + case "apiendpoint": + this.apiEndPoint = cp.getValue(); + break; + case "authstring": + this.authString = cp.getValue(); + break; + case "authendpoint": + this.authEndPoint = cp.getValue(); + break; + case "username": + this.userName = cp.getValue(); + break; + case "password": + this.password = cp.getValue(); + break; + case "apientityendpoint": + this.apiEntityEndPoint = cp.getValue(); + break; + } + } + } + + @Override + public String getNameId() { + return "id"; + } + + @Override + public String createEntity(String entity, HashMap data) throws Exception { + OneIdentityInterceptor oi = new OneIdentityInterceptor(this.host + this.authEndPoint, this.authString, + this.userName, this.password); + BasicAuthInterceptor bi = new BasicAuthInterceptor(this.userName, this.password); + + HTTPClient hc = new HTTPClient(); + hc.addInterceptor(oi); + hc.addInterceptor(bi); + hc.setUserName(this.userName); + hc.setPassword(this.password); + + HashMap valuesObj = new HashMap<>(); + valuesObj.put("values", data); + DocumentContext jsonContext = JsonPath.parse(valuesObj); + + String retUser = hc.post(this.host + this.apiEntityEndPoint + "Person", jsonContext.jsonString()); + ObjectMapper mapper = new ObjectMapper(); + + HashMap map = new HashMap<>(); + map = mapper.readValue(retUser, map.getClass()); + + return (String) JSONUtil.getFromJSONPath("$.uid", map); + } + + @Override + public String updateEntity(String entity, ModificationStep ms) throws Exception { + // TODO Auto-generated method stub + return null; + } + + @Override + public boolean deleteEntity(String entity, HashMap data) throws Exception { + // TODO Auto-generated method stub + return false; + } + + @Override + public List> getEntities(String entity) throws Exception { + OneIdentityInterceptor oi = new OneIdentityInterceptor(this.host + this.authEndPoint, this.authString, + this.userName, this.password); + BasicAuthInterceptor bi = new BasicAuthInterceptor(this.userName, this.password); + + HTTPClient hc = new HTTPClient(); + hc.addInterceptor(oi); + hc.addInterceptor(bi); + hc.setUserName(this.userName); + hc.setPassword(this.password); + + String result = hc.get(this.host + this.apiEndPoint + "Person"); + ObjectMapper mapper = new ObjectMapper(); + + List> map = new ArrayList<>(); + map = mapper.readValue(result, map.getClass()); + + return map; + } + + @Override + public HashMap getEntity(String entity, HashMap data) throws Exception { + String userId = (String) ConnectorUtil.getAttributeValue(getNameId(), data); + if (userId == null) { + throw new InternalErrorException("UserID not found in read mapping!"); + } + + OneIdentityInterceptor oi = new OneIdentityInterceptor(this.host + this.authEndPoint, this.authString, + this.userName, this.password); + BasicAuthInterceptor bi = new BasicAuthInterceptor(this.userName, this.password); + + HTTPClient hc = new HTTPClient(); + hc.addInterceptor(oi); + hc.addInterceptor(bi); + hc.setUserName(this.userName); + hc.setPassword(this.password); + + String result = hc.get(this.host + this.apiEntityEndPoint + "Person/" + userId); + ObjectMapper mapper = new ObjectMapper(); + + HashMap map = new HashMap<>(); + map = mapper.readValue(result, map.getClass()); + + return map; + + } + +} \ No newline at end of file diff --git a/scimgateway/src/main/java/com/asena/scimgateway/connector/SACConnector.java b/scimgateway/src/main/java/com/asena/scimgateway/connector/SACConnector.java index 795c8c4..cf89855 100644 --- a/scimgateway/src/main/java/com/asena/scimgateway/connector/SACConnector.java +++ b/scimgateway/src/main/java/com/asena/scimgateway/connector/SACConnector.java @@ -12,11 +12,13 @@ import com.asena.scimgateway.model.Attribute; import com.asena.scimgateway.model.ConnectionProperty; import com.asena.scimgateway.model.EntryTypeMapping; +import com.asena.scimgateway.model.ModificationStep; import com.asena.scimgateway.model.RemoteSystem; import com.asena.scimgateway.model.Script; import com.asena.scimgateway.model.ConnectionProperty.ConnectionPropertyType; import com.asena.scimgateway.utils.ConnectorUtil; import com.asena.scimgateway.utils.JSONUtil; +import com.asena.scimgateway.utils.ModificationUtil; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.jayway.jsonpath.DocumentContext; @@ -139,8 +141,8 @@ public String createEntity(String entity, HashMap data) throws E @SuppressWarnings("unchecked") @Override - public String updateEntity(String entity, HashMap data) throws Exception { - String userId = (String) ConnectorUtil.getAttributeValue(getNameId(), data); + public String updateEntity(String entity, ModificationStep ms) throws Exception { + String userId = (String) ms.findValueByAttribute(getNameId()); if (userId == null) { throw new InternalErrorException("UserID not found in read mapping!"); } @@ -148,7 +150,7 @@ public String updateEntity(String entity, HashMap data) throws E OAuthInterceptor oi = new OAuthInterceptor(this.oauthUser, this.oauthPassword, this.oauthURL); CSRFInterceptor ci = new CSRFInterceptor(this.csrfURL); - String s = transformEntityTo(data); + String s = transformEntityTo(ms); HTTPClient hc = new HTTPClient(); hc.addInterceptor(oi); hc.addInterceptor(ci); @@ -260,6 +262,11 @@ private HashMap transformEntityFrom(HashMap enti return tmpEntity; } + private String transformEntityTo(ModificationStep ms) throws JsonProcessingException { + HashMap entity = ModificationUtil.collectSimpleModifications(ms); + return transformEntityTo(entity); + } + private String transformEntityTo(HashMap entity) throws JsonProcessingException { DocumentContext jsonContext = JsonPath.parse("{}"); diff --git a/scimgateway/src/main/java/com/asena/scimgateway/connector/utils/oneidentity/OneIdentityInterceptor.java b/scimgateway/src/main/java/com/asena/scimgateway/connector/utils/oneidentity/OneIdentityInterceptor.java new file mode 100644 index 0000000..b04db40 --- /dev/null +++ b/scimgateway/src/main/java/com/asena/scimgateway/connector/utils/oneidentity/OneIdentityInterceptor.java @@ -0,0 +1,73 @@ +package com.asena.scimgateway.connector.utils.oneidentity; + +import java.io.IOException; +import java.util.List; + +import com.asena.scimgateway.http.BasicAuthInterceptor; +import com.asena.scimgateway.http.HTTPClient; + +import net.minidev.json.JSONObject; +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.Request.Builder; + +public class OneIdentityInterceptor implements Interceptor { + + private String authUrl; + private String authString; + private String userName; + private String password; + + public OneIdentityInterceptor(String authUrl, String authString, String userName, String password) { + this.authString = authString; + this.authUrl = authUrl; + this.userName = userName; + this.password = password; + } + + private String getAuthCookie(String url, String userName, String password, String authString) throws IOException { + HTTPClient hc = new HTTPClient(); + hc.setUserName(this.userName); + hc.setPassword(this.password); + hc.setExpectedResponseCode(200); + hc.addInterceptor(new BasicAuthInterceptor(this.userName, this.password)); + + JSONObject jo = new JSONObject(); + jo.put("authString", authString); + + Response resp = hc.postWithResponse(authUrl, jo.toJSONString()); + List cookies = resp.headers("Set-Cookie"); + return getAuthCookie(cookies, "ss-id="); + } + + private String getAuthCookie(List cookies, String cookie) throws IOException { + String retCookie = null; + if (cookies != null) { + for (String s : cookies) { + int authPos = s.indexOf(cookie); + int commaPos = s.indexOf(";"); + if (authPos >= 0) { + return s.substring(authPos, commaPos); + } + } + } + return retCookie; + } + + @Override + public Response intercept(Chain chain) throws IOException { + Request request = chain.request(); + String authCookie = getAuthCookie(this.authUrl, this.userName, this.password, this.authString); + if (authCookie != null) { + Builder b = request.newBuilder(); + b.addHeader("Cookie", authCookie); + + Request authenticatedRequest = b.build(); + return chain.proceed(authenticatedRequest); + } else { + throw new InternalError("Could not retrieve auth cookie"); + } + } + +} \ No newline at end of file diff --git a/scimgateway/src/main/java/com/asena/scimgateway/controller/SCIMEntityController.java b/scimgateway/src/main/java/com/asena/scimgateway/controller/SCIMEntityController.java index 238118b..94c4670 100644 --- a/scimgateway/src/main/java/com/asena/scimgateway/controller/SCIMEntityController.java +++ b/scimgateway/src/main/java/com/asena/scimgateway/controller/SCIMEntityController.java @@ -19,6 +19,7 @@ import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; @@ -80,6 +81,20 @@ public class SCIMEntityController { return o; } + @PreAuthorize("isTechnical() and isServiceUser(#systemid) and isRemoteSystemActive(#systemid)") + @PatchMapping("/{entity}/{id}") + public @ResponseBody Object scimUserPatch(@PathVariable String systemid, @PathVariable String entity, @PathVariable String id, @RequestBody HashMap params, HttpServletResponse response) { + RemoteSystem rs = remoteSystemService.findById(systemid).orElseThrow(() -> new NotFoundException(systemid)); + Object o = null; + try { + o = new SCIMProcessor(rs, entity).patchEntity(id, params); + response.setStatus(201); + } catch (Exception e) { + handleControllerError(e, params); + } + return o; + } + @PreAuthorize("isTechnical() and isServiceUser(#systemid) and isRemoteSystemActive(#systemid)") @PutMapping("/{entity}/{id}") public @ResponseBody Object scimUserUpdate(@PathVariable String systemid, @PathVariable String entity, @PathVariable String id, @RequestBody HashMap params, HttpServletResponse response) { diff --git a/scimgateway/src/main/java/com/asena/scimgateway/http/HTTPClient.java b/scimgateway/src/main/java/com/asena/scimgateway/http/HTTPClient.java index 917da64..8bc53d7 100644 --- a/scimgateway/src/main/java/com/asena/scimgateway/http/HTTPClient.java +++ b/scimgateway/src/main/java/com/asena/scimgateway/http/HTTPClient.java @@ -19,7 +19,6 @@ private enum HTTP_OPERATION { POST, PUT, PATCH } - private String mediaType = null; private String userName; private String password; @@ -77,7 +76,15 @@ public String post(String url, String obj) throws IOException { return write(url, obj, HTTP_OPERATION.POST); } + public Response postWithResponse(String url, String obj) throws IOException { + return writeWithResponse(url, obj, HTTP_OPERATION.POST); + } + private String write(String url, String obj, HTTP_OPERATION op) throws IOException { + return writeWithResponse(url, obj, op).body().string(); + } + + private Response writeWithResponse(String url, String obj, HTTP_OPERATION op) throws IOException { this.client = buildClient(); MediaType mt = MediaType.parse((this.mediaType != null) ? this.mediaType : "application/json; charset=utf-8"); RequestBody body = RequestBody.create(obj, mt); @@ -101,7 +108,7 @@ private String write(String url, String obj, HTTP_OPERATION op) throws IOExcepti if (response.code() != expectedResponseCode) { throw new IOException("Unexpected http code: " + response.code() + " - " + response.body().string()); } - return response.body().string(); + return response; } public String put(String url, String obj) throws IOException { diff --git a/scimgateway/src/main/java/com/asena/scimgateway/model/Modification.java b/scimgateway/src/main/java/com/asena/scimgateway/model/Modification.java new file mode 100644 index 0000000..56e55f0 --- /dev/null +++ b/scimgateway/src/main/java/com/asena/scimgateway/model/Modification.java @@ -0,0 +1,49 @@ +package com.asena.scimgateway.model; + +public class Modification { + public enum ModificationType { + COMPLEX_ADD, COMPLEX_REMOVE, SIMPLE + } + + private ModificationType type; + private String attributeName; + private Object value; + + public Modification(String attributeName, Object value, ModificationType type) { + setAttributeName(attributeName); + setValue(value); + setType(type); + } + + public Modification(String attributeName, Object value) { + setAttributeName(attributeName); + setValue(value); + setType(ModificationType.SIMPLE); + } + + public String getAttributeName() { + return attributeName; + } + + public ModificationType getType() { + return type; + } + + public void setType(ModificationType type) { + this.type = type; + } + + public Object getValue() { + return value; + } + + public void setValue(Object value) { + this.value = value; + } + + public void setAttributeName(String attributeName) { + this.attributeName = attributeName; + } + + +} \ No newline at end of file diff --git a/scimgateway/src/main/java/com/asena/scimgateway/model/ModificationStep.java b/scimgateway/src/main/java/com/asena/scimgateway/model/ModificationStep.java new file mode 100644 index 0000000..8b373de --- /dev/null +++ b/scimgateway/src/main/java/com/asena/scimgateway/model/ModificationStep.java @@ -0,0 +1,77 @@ +package com.asena.scimgateway.model; + +import java.util.ArrayList; +import java.util.List; + +public class ModificationStep { + private List modifications = new ArrayList<>(); + private String id; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public void addModification(Modification m) { + if ((m != null) && (findModificationByAttribute(m.getAttributeName()) == null)) { + modifications.add(m); + } + } + + public Modification findModificationByAttribute(String attributeName) { + if (attributeName == null) { + return null; + } + + for (Modification m : modifications) { + String attr = m.getAttributeName(); + if (attributeName.equals(attr)) { + return m; + } + } + + return null; + } + + public Object findValueByAttribute(String attributeName) { + Modification m = findModificationByAttribute(attributeName); + if (m != null) { + return m.getValue(); + } else { + return null; + } + } + + + public void setModificationValueByAttribute(String attributeName, Object o) { + if (attributeName == null) { + return; + } + + for (Modification m : modifications) { + String attr = m.getAttributeName(); + if (attributeName.equals(attr)) { + m.setValue(o); + } + } + } + + public void upsertModification(Modification m) { + if (m == null) { + return; + } + + if (findModificationByAttribute(m.getAttributeName()) == null) { + modifications.add(m); + } else { + setModificationValueByAttribute(m.getAttributeName(), m.getValue()); + } + } + + public List getModifications() { + return modifications; + } +} \ No newline at end of file diff --git a/scimgateway/src/main/java/com/asena/scimgateway/processor/ConnectorProcessor.java b/scimgateway/src/main/java/com/asena/scimgateway/processor/ConnectorProcessor.java index 7b73f6c..44dd9b7 100644 --- a/scimgateway/src/main/java/com/asena/scimgateway/processor/ConnectorProcessor.java +++ b/scimgateway/src/main/java/com/asena/scimgateway/processor/ConnectorProcessor.java @@ -7,6 +7,7 @@ import com.asena.scimgateway.connector.IConnector; import com.asena.scimgateway.connector.LDAPConnector; import com.asena.scimgateway.connector.NoOpConnector; +import com.asena.scimgateway.connector.OneIdentityConnector; import com.asena.scimgateway.connector.SACConnector; import com.asena.scimgateway.exception.InternalErrorException; import com.asena.scimgateway.model.RemoteSystem; @@ -17,17 +18,20 @@ public class ConnectorProcessor { private static Logger logger = LoggerFactory.getLogger(ConnectorProcessor.class); - private ConnectorProcessor() {} + private ConnectorProcessor() { + } public static Set getAvailableConnectors() { Set retSystems = new HashSet<>(); LDAPConnector ldap = new LDAPConnector(); SACConnector sac = new SACConnector(); AzureConnector az = new AzureConnector(); - + OneIdentityConnector oc = new OneIdentityConnector(); + retSystems.add(ldap.getRemoteSystemTemplate()); retSystems.add(sac.getRemoteSystemTemplate()); retSystems.add(az.getRemoteSystemTemplate()); + retSystems.add(oc.getRemoteSystemTemplate()); return retSystems; } @@ -49,13 +53,14 @@ public static IConnector getConnectorByType(String type) { NoOpConnector noop = new NoOpConnector(); SACConnector sac = new SACConnector(); AzureConnector az = new AzureConnector(); + OneIdentityConnector oi = new OneIdentityConnector(); logger.info("Reading connector type {}", type); - + if (type == null) { throw new InternalErrorException("No connector found with type null"); } - + switch (type) { case "LDAP": return csv; @@ -65,6 +70,8 @@ public static IConnector getConnectorByType(String type) { return az; case "NOOP": return noop; + case "OneIdentity": + return oi; default: throw new InternalErrorException("No connector found with type " + type); } diff --git a/scimgateway/src/main/java/com/asena/scimgateway/processor/ModificationProcessor.java b/scimgateway/src/main/java/com/asena/scimgateway/processor/ModificationProcessor.java new file mode 100644 index 0000000..4b1bc95 --- /dev/null +++ b/scimgateway/src/main/java/com/asena/scimgateway/processor/ModificationProcessor.java @@ -0,0 +1,62 @@ +package com.asena.scimgateway.processor; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import com.asena.scimgateway.exception.InternalErrorException; +import com.asena.scimgateway.model.Modification; +import com.asena.scimgateway.model.ModificationStep; +import com.asena.scimgateway.model.Modification.ModificationType; + +public class ModificationProcessor { + + @SuppressWarnings("unchecked") + public static List patch(HashMap obj) { + List schema = (List) obj.get("schemas"); + List> operations = (List>) obj.get("Operations"); + List modifications = new ArrayList<>(); + if (schema == null) { + throw new InternalErrorException("No schema found on PATCH request"); + } + + if (operations == null) { + throw new InternalErrorException("No operations attribute found on PATCH request"); + } + + if (!schema.contains("urn:ietf:params:scim:api:messages:2.0:PatchOp")) { + throw new InternalErrorException("Schema for PATCH request not supported!"); + } + + for (HashMap elem : operations) { + Object elemOp = elem.get("op"); + Object elemPath = elem.get("path"); + HashMap elemValue = (HashMap) elem.get("value"); + if ((elemOp == null) || (elemPath == null) || (elemPath == null)) { + continue; + } + + if (elemOp.equals("add")) { + modifications.add(new Modification((String) elemPath, elemValue.get("value"), ModificationType.COMPLEX_ADD)); + } else if (elemOp.equals("remove")) { + modifications.add(new Modification((String) elemPath, elemValue.get("value"), ModificationType.COMPLEX_REMOVE)); + } else { + modifications.add(new Modification((String) elemPath, elemValue.get("value"))); + } + } + + return modifications; + } + + public static ModificationStep update(HashMap obj) { + ModificationStep retModifications = new ModificationStep(); + + for (String elemKey : obj.keySet()) { + retModifications.addModification(new Modification(elemKey, obj.get(elemKey))); + } + + return retModifications; + } + + +} \ No newline at end of file diff --git a/scimgateway/src/main/java/com/asena/scimgateway/processor/SCIMProcessor.java b/scimgateway/src/main/java/com/asena/scimgateway/processor/SCIMProcessor.java index bd96394..3bf7901 100644 --- a/scimgateway/src/main/java/com/asena/scimgateway/processor/SCIMProcessor.java +++ b/scimgateway/src/main/java/com/asena/scimgateway/processor/SCIMProcessor.java @@ -9,7 +9,10 @@ import com.asena.scimgateway.exception.InternalErrorException; import com.asena.scimgateway.model.Attribute; import com.asena.scimgateway.model.EntryTypeMapping; +import com.asena.scimgateway.model.Modification; +import com.asena.scimgateway.model.ModificationStep; import com.asena.scimgateway.model.RemoteSystem; +import com.asena.scimgateway.model.Modification.ModificationType; import com.asena.scimgateway.utils.JSONUtil; import com.jayway.jsonpath.DocumentContext; import com.jayway.jsonpath.JsonPath; @@ -41,6 +44,22 @@ public HashMap getEntity(String userId) throws Exception { return data; } + public HashMap patchEntity(String entityId, HashMap obj) throws Exception { + IConnector conn = getConnector(); + List modifications = ModificationProcessor.patch(obj); + ModificationStep mStep = prepareDataFromRemoteSystem(modifications); + Modification mId = getWriteNameId(conn, entityId); + + mStep.upsertModification(mId); + mStep.setId((String) mId.getValue()); + + String id = transferUpdateToConnector(conn, mStep); + id = processId(id, getReadMappingNameId(conn)); + + SCIMResultProcessor.addMetaDataCreate(obj, remoteSystem, id, entity); + return null; + } + public HashMap createEntity(HashMap obj) throws Exception { IConnector conn = getConnector(); HashMap data = prepareDataToRemoteSystem(obj); @@ -55,9 +74,13 @@ public HashMap createEntity(HashMap obj) throws public HashMap updateEntity(String entityId, HashMap obj) throws Exception { IConnector conn = getConnector(); HashMap data = prepareDataToRemoteSystem(obj); - data = postPrepareDataToRemoteSystem(conn, remoteSystem, entityId, data); + Modification mId = getWriteNameId(conn, entityId); + ModificationStep mStep = ModificationProcessor.update(data); + + mStep.upsertModification(mId); + mStep.setId((String) mId.getValue()); - String id = transferUpdateToConnector(conn, data); + String id = transferUpdateToConnector(conn, mStep); id = processId(id, getReadMappingNameId(conn)); SCIMResultProcessor.addMetaDataCreate(obj, remoteSystem, id, entity); @@ -72,6 +95,47 @@ public boolean deleteEntity(String entityId) throws Exception { return transferDeleteToConnector(conn, data); } + private ModificationStep prepareDataFromRemoteSystem(List data) { + Set attrs = getWriteMappings(); + ModificationStep retModifications = new ModificationStep(); + + if (data == null) { + throw new InternalErrorException("No data for modification found!"); + } + + for (Attribute a : attrs) { + Object o = null; + Modification m = null; + if (((a.getSource() == null) || (a.getSource().length() < 1)) && (a.getDestination() != null)) { + o = null; + } else { + try { + m = findInModifications(data, a.getSource()); + o = m.getValue(); + } catch (Exception e) { + continue; + } + } + if (a.getTransformation() != null) { + o = ScriptProcessor.processTransformation(a, o, remoteSystem); + } + retModifications.addModification(new Modification(a.getDestination(), o, (m != null ? m.getType() : ModificationType.SIMPLE))); + } + + return retModifications; + } + + private Modification findInModifications(List data, String attr) { + String finalAttrName = attr.substring(2); + for (Modification m : data) { + if (finalAttrName.equals(m.getAttributeName())) { + return m; + } + } + + throw new InternalErrorException("Attribute not found!"); + } + private HashMap prepareDataFromRemoteSystem(HashMap entry) { Set attrs = getReadMappings(); DocumentContext jsonContext = JsonPath.parse("{}"); @@ -134,6 +198,12 @@ private Attribute getWriteMappingNameId(IConnector conn) { throw new InternalErrorException("No write mapping with nameId " + writeNameId + " found!"); } + private Modification getWriteNameId(IConnector conn, String id) { + String nameId = conn.getNameId(); + String newId = processId(id, getWriteMappingNameId(conn)); + return new Modification(nameId, newId); + } + private HashMap postPrepareDataToRemoteSystem(IConnector conn, RemoteSystem rs, String id, HashMap data) { String nameId = conn.getNameId(); String newId = processId(id, getWriteMappingNameId(conn)); @@ -224,10 +294,10 @@ private String transferCreateToConnector(IConnector conn, HashMap data) + private String transferUpdateToConnector(IConnector conn, ModificationStep ms) throws Exception { conn.setupConnector(remoteSystem); - return conn.updateEntity(entity, data); + return conn.updateEntity(entity, ms); } private boolean transferDeleteToConnector(IConnector conn, HashMap data) diff --git a/scimgateway/src/main/java/com/asena/scimgateway/script/GlobalScripts.java b/scimgateway/src/main/java/com/asena/scimgateway/script/GlobalScripts.java index 4ca33ac..a346438 100644 --- a/scimgateway/src/main/java/com/asena/scimgateway/script/GlobalScripts.java +++ b/scimgateway/src/main/java/com/asena/scimgateway/script/GlobalScripts.java @@ -36,6 +36,8 @@ private void createHooks() { List hooks = new ArrayList<>(); hooks.add("getSystemProperty"); hooks.add("getRemoteSystem"); + hooks.add("getSystemId"); + hooks.add("println"); String[] hookNames = new String[hooks.size()]; hookNames = hooks.toArray(hookNames); @@ -56,5 +58,18 @@ public String getSystemProperty(String searchKey) { } return null; } - + + public String getSystemId() { + if (remoteSystem != null) { + return remoteSystem.getId(); + } else { + return null; + } + } + + public String println(Object s) { + System.out.println(s); + return null; + } + } \ No newline at end of file diff --git a/scimgateway/src/main/java/com/asena/scimgateway/script/ScriptRunner.java b/scimgateway/src/main/java/com/asena/scimgateway/script/ScriptRunner.java index 96141bf..ca8c219 100644 --- a/scimgateway/src/main/java/com/asena/scimgateway/script/ScriptRunner.java +++ b/scimgateway/src/main/java/com/asena/scimgateway/script/ScriptRunner.java @@ -15,9 +15,10 @@ public class ScriptRunner { private Context context; private Logger logger = LoggerFactory.getLogger(ScriptRunner.class); - + public ScriptRunner(RemoteSystem rs) { this.context = Context.enter(); + this.context.getWrapFactory().setJavaPrimitiveWrap(false); this.scope = this.context.initStandardObjects(new GlobalScripts(this.context, rs)); } @@ -32,7 +33,7 @@ public Object execute(Script s, Object param) { if ((s != null) && (s.getName() != null) && (s.getContent() != null)) { Object obj = this.scope.get(s.getName(), this.scope); if (obj instanceof Function) { - Object[] funcParams = {param}; + Object[] funcParams = { param }; Function f = (Function) obj; retData = f.call(this.context, this.scope, this.scope, funcParams); } else { diff --git a/scimgateway/src/main/java/com/asena/scimgateway/utils/ModificationUtil.java b/scimgateway/src/main/java/com/asena/scimgateway/utils/ModificationUtil.java new file mode 100644 index 0000000..cafdde2 --- /dev/null +++ b/scimgateway/src/main/java/com/asena/scimgateway/utils/ModificationUtil.java @@ -0,0 +1,16 @@ +package com.asena.scimgateway.utils; + +import java.util.HashMap; + +import com.asena.scimgateway.model.Modification; +import com.asena.scimgateway.model.ModificationStep; + +public class ModificationUtil { + public static HashMap collectSimpleModifications(ModificationStep ms) { + HashMap retData = new HashMap(); + for (Modification m : ms.getModifications()) { + retData.put(m.getAttributeName(), m.getValue()); + } + return retData; + } +} \ No newline at end of file diff --git a/scimgateway/src/main/resources/application.properties b/scimgateway/src/main/resources/application.properties index 580399a..7c0b977 100644 --- a/scimgateway/src/main/resources/application.properties +++ b/scimgateway/src/main/resources/application.properties @@ -4,7 +4,7 @@ spring.datasource.password=example spring.datasource.driverClassName=org.postgresql.Driver # The SQL dialect makes Hibernate generate better SQL for the chosen database -spring.jpa.properties.hibernate.dialect =org.hibernate.dialect.PostgreSQL95Dialect +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQL95Dialect # Hibernate ddl auto (create, create-drop, validate, update) spring.jpa.hibernate.ddl-auto = validate