diff --git a/pom.xml b/pom.xml index fb4a68b..01694ad 100644 --- a/pom.xml +++ b/pom.xml @@ -84,11 +84,6 @@ 1.5.2.0 - 2.5.1 - 1.36.0 - 1.25.0 - 1.23.0 - 2.17.1 2.7.18 @@ -112,38 +107,27 @@ com.google.oauth-client google-oauth-client - ${google-oauth-client.version} + 1.36.0 com.google.auth google-auth-library-oauth2-http - ${google-auth-library-oauth2-http.version} - - - com.google.oauth-client - google-oauth-client-java6 - ${google-oauth-client.version} + 1.23.0 com.google.api-client google-api-client - ${google-api-client.version} + 2.5.1 com.google.apis google-api-services-admin-directory - directory_v1-rev118-${google-api-services.version} + directory_v1-rev20240509-2.0.0 com.google.apis google-api-services-licensing - v1-rev62-${google-api-services.version} - - - - com.fasterxml.jackson.core - jackson-databind - ${jackson.version} + v1-rev20220430-2.0.0 @@ -151,7 +135,7 @@ spring-boot-starter-web ${spring-boot.version} - + net.tirasa.connid diff --git a/src/main/java/net/tirasa/connid/bundles/googleapps/GoogleApiExecutor.java b/src/main/java/net/tirasa/connid/bundles/googleapps/GoogleApiExecutor.java new file mode 100644 index 0000000..c4908f2 --- /dev/null +++ b/src/main/java/net/tirasa/connid/bundles/googleapps/GoogleApiExecutor.java @@ -0,0 +1,134 @@ +/* + * ==================== + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright (c) 2024 ConnId. All Rights Reserved + * + * The contents of this file are subject to the terms of the Common Development + * and Distribution License("CDDL") (the "License"). You may not use this file + * except in compliance with the License. + * + * You can obtain a copy of the License at + * http://opensource.org/licenses/cddl1.php + * See the License for the specific language governing permissions and limitations + * under the License. + * + * When distributing the Covered Code, include this CDDL Header Notice in each file + * and include the License file at http://opensource.org/licenses/cddl1.php. + * If applicable, add the following below this CDDL Header, with the fields + * enclosed by brackets [] replaced by your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * ==================== + */ +package net.tirasa.connid.bundles.googleapps; + +import com.google.api.client.googleapis.json.GoogleJsonError; +import com.google.api.client.googleapis.json.GoogleJsonResponseException; +import com.google.api.client.googleapis.services.json.AbstractGoogleJsonClientRequest; +import com.google.api.client.http.HttpStatusCodes; +import java.io.IOException; +import java.security.SecureRandom; +import org.identityconnectors.common.Assertions; +import org.identityconnectors.common.logging.Log; +import org.identityconnectors.framework.common.exceptions.ConnectorException; +import org.identityconnectors.framework.common.exceptions.RetryableException; + +public final class GoogleApiExecutor { + + private static final Log LOG = Log.getLog(GoogleApiExecutor.class); + + private static final SecureRandom RANDOM = new SecureRandom(); + + public static , T, R> R execute( + final G request, final RequestResultHandler handler) { + + return execute( + Assertions.nullChecked(request, "Google Json ClientRequest"), + Assertions.nullChecked(handler, "handler"), -1); + } + + public static , T, R> R execute( + final G request, final RequestResultHandler handler, final int retry) { + + try { + if (retry >= 0) { + long sleep = (long) ((1000 * Math.pow(2, retry)) + nextLong(1000)); + try { + Thread.sleep(sleep); + } catch (InterruptedException e) { + throw ConnectorException.wrap(e); + } + } + return handler.handleResult(request, request.execute()); + } catch (GoogleJsonResponseException e) { + GoogleJsonError details = e.getDetails(); + if (null != details && null != details.getErrors()) { + GoogleJsonError.ErrorInfo errorInfo = details.getErrors().get(0); + // error: 403 + LOG.error("Unable to execute request {0} - {1} - {2}", + e.getStatusCode(), e.getStatusMessage(), errorInfo.getReason()); + switch (e.getStatusCode()) { + case HttpStatusCodes.STATUS_CODE_FORBIDDEN: + if ("userRateLimitExceeded".equalsIgnoreCase(errorInfo.getReason()) + || "rateLimitExceeded".equalsIgnoreCase(errorInfo.getReason())) { + return handler.handleError(e); + } + break; + case HttpStatusCodes.STATUS_CODE_NOT_FOUND: + if ("notFound".equalsIgnoreCase(errorInfo.getReason())) { + return handler.handleNotFound(e); + } + break; + case 409: + if ("duplicate".equalsIgnoreCase(errorInfo.getReason())) { + // Already Exists + handler.handleDuplicate(e); + } + break; + case 400: + if ("invalid".equalsIgnoreCase(errorInfo.getReason())) { + // Already Exists "Invalid Ou Id" + } + break; + case HttpStatusCodes.STATUS_CODE_SERVICE_UNAVAILABLE: + if ("backendError".equalsIgnoreCase(errorInfo.getReason())) { + throw RetryableException.wrap(e.getMessage(), e); + } + break; + default: + break; + } + } + + if (e.getStatusCode() == HttpStatusCodes.STATUS_CODE_FORBIDDEN) { + LOG.error("Forbidden request"); + handler.handleError(e); + } else if (e.getStatusCode() == HttpStatusCodes.STATUS_CODE_NOT_FOUND) { + LOG.error("Endpoint not found for request"); + return handler.handleNotFound(e); + } + throw ConnectorException.wrap(e); + } catch (IOException e) { + // https://developers.google.com/admin-sdk/directory/v1/limits + // rateLimitExceeded or userRateLimitExceeded + if (retry < 5) { + return execute(request, handler, retry + 1); + } else { + return handler.handleError(e); + } + } + } + + private static long nextLong(final long n) { + long bits, val; + do { + bits = (RANDOM.nextLong() << 1) >>> 1; + val = bits % n; + } while (bits - val + (n - 1) < 0L); + return val; + } + + private GoogleApiExecutor() { + // private constructor for static utility class + } +} diff --git a/src/main/java/net/tirasa/connid/bundles/googleapps/GoogleAppsConfiguration.java b/src/main/java/net/tirasa/connid/bundles/googleapps/GoogleAppsConfiguration.java index a2154a5..b3402f9 100644 --- a/src/main/java/net/tirasa/connid/bundles/googleapps/GoogleAppsConfiguration.java +++ b/src/main/java/net/tirasa/connid/bundles/googleapps/GoogleAppsConfiguration.java @@ -29,8 +29,8 @@ import com.google.api.client.http.javanet.NetHttpTransport; import com.google.api.client.json.JsonFactory; import com.google.api.client.json.gson.GsonFactory; -import com.google.api.services.admin.directory.Directory; -import com.google.api.services.admin.directory.DirectoryScopes; +import com.google.api.services.directory.Directory; +import com.google.api.services.directory.DirectoryScopes; import com.google.api.services.licensing.Licensing; import com.google.auth.http.HttpCredentialsAdapter; import com.google.auth.oauth2.GoogleCredentials; @@ -240,7 +240,6 @@ private void initGoogleCredentials() { DirectoryScopes.ADMIN_DIRECTORY_USERSCHEMA, DirectoryScopes.ADMIN_DIRECTORY_ORGUNIT, DirectoryScopes.ADMIN_DIRECTORY_DOMAIN, - DirectoryScopes.ADMIN_DIRECTORY_NOTIFICATIONS, DirectoryScopes.ADMIN_DIRECTORY_GROUP, DirectoryScopes.ADMIN_DIRECTORY_GROUP_MEMBER)); diff --git a/src/main/java/net/tirasa/connid/bundles/googleapps/GoogleAppsConnector.java b/src/main/java/net/tirasa/connid/bundles/googleapps/GoogleAppsConnector.java index 2cedd10..3854abf 100644 --- a/src/main/java/net/tirasa/connid/bundles/googleapps/GoogleAppsConnector.java +++ b/src/main/java/net/tirasa/connid/bundles/googleapps/GoogleAppsConnector.java @@ -23,79 +23,32 @@ */ package net.tirasa.connid.bundles.googleapps; -import com.google.api.client.googleapis.json.GoogleJsonError; -import com.google.api.client.googleapis.json.GoogleJsonResponseException; -import com.google.api.client.googleapis.services.json.AbstractGoogleJsonClientRequest; -import com.google.api.client.http.HttpStatusCodes; -import com.google.api.services.admin.directory.Directory; -import com.google.api.services.admin.directory.model.Alias; -import com.google.api.services.admin.directory.model.Group; -import com.google.api.services.admin.directory.model.Groups; -import com.google.api.services.admin.directory.model.Member; -import com.google.api.services.admin.directory.model.Members; -import com.google.api.services.admin.directory.model.OrgUnit; -import com.google.api.services.admin.directory.model.OrgUnits; -import com.google.api.services.admin.directory.model.User; -import com.google.api.services.admin.directory.model.UserMakeAdmin; -import com.google.api.services.admin.directory.model.UserPhoto; -import com.google.api.services.admin.directory.model.Users; -import com.google.api.services.licensing.Licensing; -import com.google.api.services.licensing.LicensingRequest; -import com.google.api.services.licensing.model.LicenseAssignment; -import com.google.api.services.licensing.model.LicenseAssignmentList; -import java.io.IOException; -import java.security.SecureRandom; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; +import java.util.Optional; import java.util.Set; -import java.util.regex.Matcher; -import org.identityconnectors.common.Assertions; import org.identityconnectors.common.CollectionUtil; -import org.identityconnectors.common.StringUtil; -import org.identityconnectors.common.logging.Log; import org.identityconnectors.framework.common.exceptions.ConnectorException; -import org.identityconnectors.framework.common.exceptions.InvalidAttributeValueException; -import org.identityconnectors.framework.common.exceptions.RetryableException; -import org.identityconnectors.framework.common.exceptions.UnknownUidException; import org.identityconnectors.framework.common.objects.Attribute; -import org.identityconnectors.framework.common.objects.AttributeBuilder; -import org.identityconnectors.framework.common.objects.AttributeUtil; -import org.identityconnectors.framework.common.objects.AttributesAccessor; -import org.identityconnectors.framework.common.objects.ConnectorObject; -import org.identityconnectors.framework.common.objects.ConnectorObjectBuilder; -import org.identityconnectors.framework.common.objects.Name; +import org.identityconnectors.framework.common.objects.AttributeDelta; import org.identityconnectors.framework.common.objects.ObjectClass; import org.identityconnectors.framework.common.objects.ObjectClassInfo; import org.identityconnectors.framework.common.objects.OperationOptionInfo; import org.identityconnectors.framework.common.objects.OperationOptionInfoBuilder; import org.identityconnectors.framework.common.objects.OperationOptions; -import org.identityconnectors.framework.common.objects.OperationalAttributes; -import org.identityconnectors.framework.common.objects.PredefinedAttributes; import org.identityconnectors.framework.common.objects.ResultsHandler; import org.identityconnectors.framework.common.objects.Schema; import org.identityconnectors.framework.common.objects.SchemaBuilder; -import org.identityconnectors.framework.common.objects.SearchResult; -import org.identityconnectors.framework.common.objects.SortKey; import org.identityconnectors.framework.common.objects.Uid; -import org.identityconnectors.framework.common.objects.filter.AndFilter; -import org.identityconnectors.framework.common.objects.filter.AttributeFilter; -import org.identityconnectors.framework.common.objects.filter.EqualsFilter; import org.identityconnectors.framework.common.objects.filter.Filter; import org.identityconnectors.framework.common.objects.filter.FilterTranslator; -import org.identityconnectors.framework.common.objects.filter.StartsWithFilter; import org.identityconnectors.framework.spi.Configuration; import org.identityconnectors.framework.spi.ConnectorClass; import org.identityconnectors.framework.spi.PoolableConnector; -import org.identityconnectors.framework.spi.SearchResultsHandler; import org.identityconnectors.framework.spi.operations.CreateOp; import org.identityconnectors.framework.spi.operations.DeleteOp; import org.identityconnectors.framework.spi.operations.SchemaOp; import org.identityconnectors.framework.spi.operations.SearchOp; import org.identityconnectors.framework.spi.operations.TestOp; +import org.identityconnectors.framework.spi.operations.UpdateDeltaOp; import org.identityconnectors.framework.spi.operations.UpdateOp; /** @@ -105,14 +58,7 @@ public class GoogleAppsConnector implements PoolableConnector, TestOp, SchemaOp, SearchOp, - CreateOp, UpdateOp, DeleteOp { - - /** - * Setup logging for the {@link GoogleAppsConnector}. - */ - private static final Log LOG = Log.getLog(GoogleAppsConnector.class); - - private static final SecureRandom RANDOM = new SecureRandom(); + CreateOp, UpdateOp, UpdateDeltaOp, DeleteOp { /** * Place holder for the {@link Configuration} passed into the init() method @@ -152,9 +98,24 @@ public void init(final Configuration configuration) { */ @Override public void dispose() { + Optional.ofNullable(configuration).ifPresent(GoogleAppsConfiguration::release); configuration = null; } + @Override + public void test() { + try { + configuration.test(); + } catch (Exception e) { + throw new ConnectorException(e); + } + } + + @Override + public void checkAlive() { + test(); + } + /** **************** * SPI Operations * @@ -164,317 +125,6 @@ public void dispose() { /** * {@inheritDoc} */ - @Override - public Uid create( - final ObjectClass objectClass, - final Set createAttributes, - final OperationOptions options) { - - final AttributesAccessor accessor = new AttributesAccessor(createAttributes); - - if (ObjectClass.ACCOUNT.equals(objectClass)) { - Uid uid = execute(UserHandler.createUser( - configuration.getDirectory().users(), accessor, configuration.getCustomSchemasJSON()), - new RequestResultHandler() { - - @Override - public Uid handleResult(final Directory.Users.Insert request, final User value) { - LOG.ok("New User is created: {0}", value.getId()); - return new Uid(value.getId(), value.getEtag()); - } - }); - - List aliases = accessor.findList(GoogleAppsUtil.ALIASES_ATTR); - if (null != aliases) { - final Directory.Users.Aliases aliasesService = configuration.getDirectory().users().aliases(); - for (Object member : aliases) { - if (member instanceof String) { - String id = execute(UserHandler.createUserAlias(aliasesService, uid.getUidValue(), - (String) member), - new RequestResultHandler() { - - @Override - public String handleResult( - final Directory.Users.Aliases.Insert request, final Alias value) { - - return value == null ? null : value.getId(); - } - }); - - if (null == id) { - // TODO make warn about failed update - } - } else if (null != member) { - // Delete user and Error or - RetryableException e = - RetryableException.wrap("Invalid attribute value: " + String.valueOf(member), uid); - e.initCause(new InvalidAttributeValueException("Attribute 'aliases' must be a String list")); - throw e; - } - } - } - - Attribute photo = accessor.find(GoogleAppsUtil.PHOTO_ATTR); - if (null != photo) { - Object photoObject = AttributeUtil.getSingleValue(photo); - if (photoObject instanceof byte[]) { - String id = execute(UserHandler.createUpdateUserPhoto( - configuration.getDirectory().users().photos(), uid.getUidValue(), (byte[]) photoObject), - new RequestResultHandler() { - - @Override - public String handleResult(final Directory.Users.Photos.Update request, final UserPhoto value) { - return value == null ? null : value.getId(); - } - }); - - if (null == id) { - // TODO make warn about failed update - } - } else if (null != photoObject) { - // Delete group and Error or - RetryableException e = RetryableException.wrap( - "Invalid attribute value: " + String.valueOf(photoObject), uid); - e.initCause(new InvalidAttributeValueException("Attribute 'photo' must be a single byte[] value")); - throw e; - } - } - - Attribute isAdmin = accessor.find(GoogleAppsUtil.IS_ADMIN_ATTR); - if (null != isAdmin) { - try { - Boolean isAdminValue = AttributeUtil.getBooleanValue(isAdmin); - if (null != isAdminValue && isAdminValue) { - UserMakeAdmin content = new UserMakeAdmin(); - content.setStatus(isAdminValue); - - execute(configuration.getDirectory().users().makeAdmin(uid.getUidValue(), content), - new RequestResultHandler() { - - @Override - public Void handleResult(final Directory.Users.MakeAdmin request, final Void value) { - return null; - } - }); - } - } catch (final Exception e) { - // TODO Delete user and throw Exception - throw ConnectorException.wrap(e); - } - } - - Attribute groups = accessor.find(PredefinedAttributes.GROUPS_NAME); - if (null != groups && null != groups.getValue()) { - final Directory.Members service = configuration.getDirectory().members(); - if (!groups.getValue().isEmpty()) { - final List addGroups = new ArrayList<>(); - - for (Object member : groups.getValue()) { - if (member instanceof String) { - String email = accessor.getName().getNameValue(); - addGroups.add(MembersHandler.create(service, (String) member, email, null)); - } else if (null != member) { - // throw error/revert? - throw new InvalidAttributeValueException("Attribute '__GROUPS__' must be a String list"); - } - } - - // Add new Member object - for (Directory.Members.Insert insert : addGroups) { - execute(insert, new RequestResultHandler() { - - @Override - public Object handleResult(final Directory.Members.Insert request, final Member value) { - return null; - } - - @Override - public Object handleDuplicate(final IOException e) { - // Do nothing - return null; - } - }); - } - } - } - - return uid; - } else if (ObjectClass.GROUP.equals(objectClass)) { - // @formatter:off - /* AlreadyExistsException - * { - * "code" : 409, - * "errors" : [ { - * "domain" : "global", - * "message" : "Entity already exists.", - * "reason" : "duplicate" - * } ], - * "message" : "Entity already exists." - * } - */ - // @formatter:on - Uid uid = execute(GroupHandler.create(configuration.getDirectory().groups(), accessor), - new RequestResultHandler() { - - @Override - public Uid handleResult(final Directory.Groups.Insert request, - final Group value) { - LOG.ok("New Group is created:{0}", value.getEmail()); - return new Uid(value.getEmail(), value.getEtag()); - } - }); - List members = accessor.findList(GoogleAppsUtil.MEMBERS_ATTR); - if (null != members) { - final Directory.Members membersService = configuration.getDirectory().members(); - for (Object member : members) { - if (member instanceof Map) { - String email = (String) ((Map) member).get(GoogleAppsUtil.EMAIL_ATTR); - String role = (String) ((Map) member).get(GoogleAppsUtil.ROLE_ATTR); - - String id = execute(MembersHandler.create(membersService, uid.getUidValue(), email, role), - new RequestResultHandler() { - - @Override - public String handleResult(final Directory.Members.Insert request, final Member value) { - - return value == null ? null : value.getId(); - } - }); - - if (null == id) { - // TODO make warn about failed update - } - } else if (null != member) { - // Delete group and Error or - RetryableException e = - RetryableException.wrap("Invalid attribute value: " + String.valueOf(member), uid); - e.initCause(new InvalidAttributeValueException("Attribute 'members' must be a Map list")); - throw e; - } - } - } - - return uid; - } else if (GoogleAppsUtil.MEMBER.equals(objectClass)) { - return execute(MembersHandler.create(configuration.getDirectory().members(), accessor), - new RequestResultHandler() { - - @Override - public Uid handleResult(final Directory.Members.Insert request, final Member value) { - LOG.ok("New Member is created:{0}/{1}", request.getGroupKey(), value.getEmail()); - return MembersHandler.generateUid(request.getGroupKey(), value); - } - }); - } else if (GoogleAppsUtil.ORG_UNIT.equals(objectClass)) { - return execute(OrgunitsHandler.create(configuration.getDirectory().orgunits(), accessor), - new RequestResultHandler() { - - @Override - public Uid handleResult(final Directory.Orgunits.Insert request, final OrgUnit value) { - LOG.ok("New OrgUnit is created:{0}", value.getName()); - return OrgunitsHandler.generateUid(value); - } - }); - } else if (GoogleAppsUtil.LICENSE_ASSIGNMENT.equals(objectClass)) { - // @formatter:off - /* AlreadyExistsException - * { - * "code" : 400, - * "errors" : [ { - * "domain" : "global", - * "message" : "Invalid Ou Id", - * "reason" : "invalid" - * } ], - * "message" : "Invalid Ou Id" - * } - */ - // @formatter:on - - return execute( - LicenseAssignmentsHandler.create( - configuration.getLicensing().licenseAssignments(), accessor), - new RequestResultHandler() { - - @Override - public Uid handleResult( - final Licensing.LicenseAssignments.Insert request, - final LicenseAssignment value) { - - LOG.ok("LicenseAssignment is Created:{0}/{1}/{2}", - value.getProductId(), value.getSkuId(), value.getUserId()); - return LicenseAssignmentsHandler.generateUid(value); - } - }); - } else { - LOG.warn("Create of type {0} is not supported", configuration.getConnectorMessages() - .format(objectClass.getDisplayNameKey(), objectClass.getObjectClassValue())); - throw new UnsupportedOperationException("Create of type" - + objectClass.getObjectClassValue() + " is not supported"); - } - - } - - @Override - public void delete(final ObjectClass objectClass, final Uid uid, final OperationOptions options) { - AbstractGoogleJsonClientRequest request = null; - - try { - if (ObjectClass.ACCOUNT.equals(objectClass)) { - request = configuration.getDirectory().users().delete(uid.getUidValue()); - } else if (ObjectClass.GROUP.equals(objectClass)) { - request = configuration.getDirectory().groups().delete(uid.getUidValue()); - } else if (GoogleAppsUtil.MEMBER.equals(objectClass)) { - // @formatter:off - /* Already deleted - * { - * "code" : 400, - * "errors" : [ { - * "domain" : "global", - * "message" : "Missing required field: memberKey", - * "reason" : "required" - * } ], - * "message" : "Missing required field: memberKey" - * } - */ - // @formatter:on - String[] ids = uid.getUidValue().split("/"); - if (ids.length == 2) { - request = configuration.getDirectory().members().delete(ids[0], ids[1]); - } else { - throw new UnknownUidException("Invalid ID format"); - } - } else if (GoogleAppsUtil.ORG_UNIT.equals(objectClass)) { - request = configuration.getDirectory().orgunits(). - delete(GoogleAppsUtil.MY_CUSTOMER_ID, CollectionUtil.newList(uid.getUidValue())); - } else if (GoogleAppsUtil.LICENSE_ASSIGNMENT.equals(objectClass)) { - request = LicenseAssignmentsHandler.delete( - configuration.getLicensing().licenseAssignments(), uid.getUidValue()); - } - } catch (IOException e) { - throw ConnectorException.wrap(e); - } - - if (null == request) { - LOG.warn("Delete of type {0} is not supported", configuration.getConnectorMessages() - .format(objectClass.getDisplayNameKey(), objectClass.getObjectClassValue())); - throw new UnsupportedOperationException("Delete of type" - + objectClass.getObjectClassValue() + " is not supported"); - } - - execute(request, new RequestResultHandler, Void, Void>() { - - @Override - public Void handleResult(final AbstractGoogleJsonClientRequest request, final Void value) { - return null; - } - - @Override - public Void handleNotFound(final IOException e) { - throw new UnknownUidException(uid, objectClass); - } - }); - } - @Override @SuppressWarnings("unchecked") public Schema schema() { @@ -514,7 +164,8 @@ public Schema schema() { @Override public FilterTranslator createFilterTranslator( - final ObjectClass objectClass, final OperationOptions options) { + final ObjectClass objectClass, + final OperationOptions options) { return CollectionUtil::newList; } @@ -526,714 +177,16 @@ public void executeQuery( final ResultsHandler handler, final OperationOptions options) { - final Set attributesToGet = getAttributesToGet(objectClass, options); - Attribute key = getKeyFromFilter(objectClass, query); - - if (ObjectClass.ACCOUNT.equals(objectClass)) { - if (null == key || null == key.getValue() || key.getValue().isEmpty() || null == key.getValue().get(0)) { - // Search request - try { - Directory.Users.List request = configuration.getDirectory().users().list(); - if (null != query) { - StringBuilder queryBuilder = query.accept(new UserHandler(), request); - if (null != queryBuilder) { - String queryString = queryBuilder.toString(); - LOG.ok("Executing Query: {0}", queryString); - request.setQuery(queryString); - } - if (null == request.getDomain() && null == request.getCustomer()) { - request.setCustomer(GoogleAppsUtil.MY_CUSTOMER_ID); - } - } else { - request.setCustomer(GoogleAppsUtil.MY_CUSTOMER_ID); - } - - // Implementation to support the 'OP_PAGE_SIZE' - boolean paged = false; - if (options.getPageSize() != null && 0 < options.getPageSize()) { - if (options.getPageSize() >= 1 && options.getPageSize() <= 500) { - request.setMaxResults(options.getPageSize()); - paged = true; - } else { - throw new IllegalArgumentException( - "Invalid pageSize value. Default is 100. Max allowed is 500 (integer, 1-500)"); - } - } - // Implementation to support the 'OP_PAGED_RESULTS_COOKIE' - request.setPageToken(options.getPagedResultsCookie()); - request.setProjection(configuration.getProjection()); - - // Implementation to support the 'OP_ATTRIBUTES_TO_GET' - String fields = getFields(options, GoogleAppsUtil.ID_ATTR, - GoogleAppsUtil.ETAG_ATTR, GoogleAppsUtil.PRIMARY_EMAIL_ATTR); - if (null != fields) { - request.setFields("nextPageToken,users(" + fields + ")"); - } - - if (options.getOptions().get(GoogleAppsUtil.SHOW_DELETED_PARAM) instanceof Boolean) { - request.setShowDeleted(options.getOptions().get(GoogleAppsUtil.SHOW_DELETED_PARAM).toString()); - } - - // Implementation to support the 'OP_SORT_KEYS' - if (null != options.getSortKeys()) { - for (SortKey sortKey : options.getSortKeys()) { - String orderBy; - if (sortKey.getField().equalsIgnoreCase(GoogleAppsUtil.EMAIL_ATTR) - || sortKey.getField().equalsIgnoreCase(GoogleAppsUtil.PRIMARY_EMAIL_ATTR) - || sortKey.getField().equalsIgnoreCase(GoogleAppsUtil.ALIASES_ATTR) - || sortKey.getField().equalsIgnoreCase(GoogleAppsUtil.ALIAS_ATTR)) { - orderBy = GoogleAppsUtil.EMAIL_ATTR; - } else if (sortKey.getField().equalsIgnoreCase(GoogleAppsUtil.GIVEN_NAME_ATTR)) { - orderBy = GoogleAppsUtil.GIVEN_NAME_ATTR; - } else if (sortKey.getField().equalsIgnoreCase(GoogleAppsUtil.FAMILY_NAME_ATTR)) { - orderBy = GoogleAppsUtil.FAMILY_NAME_ATTR; - } else { - LOG.ok("Unsupported SortKey:{0}", sortKey); - continue; - } - - request.setOrderBy(orderBy); - if (sortKey.isAscendingOrder()) { - request.setSortOrder(GoogleAppsUtil.ASCENDING_ORDER); - } else { - request.setSortOrder(GoogleAppsUtil.DESCENDING_ORDER); - } - break; - } - } - - String nextPageToken = null; - do { - nextPageToken = execute(request, - new RequestResultHandler() { - - @Override - public String handleResult(final Directory.Users.List request, final Users value) { - if (null != value.getUsers()) { - for (User user : value.getUsers()) { - handler.handle(fromUser( - user, attributesToGet, configuration.getDirectory().groups())); - } - } - return value.getNextPageToken(); - } - }); - request.setPageToken(nextPageToken); - } while (!paged && StringUtil.isNotBlank(nextPageToken)); - - if (paged && StringUtil.isNotBlank(nextPageToken)) { - LOG.info("Paged Search was requested and next token is:{0}", nextPageToken); - ((SearchResultsHandler) handler).handleResult(new SearchResult(nextPageToken, 0)); - } - } catch (IOException e) { - LOG.warn(e, "Failed to initialize Groups#List"); - throw ConnectorException.wrap(e); - } - - } else { - // Read request - try { - Directory.Users.Get request = - configuration.getDirectory().users().get((String) key.getValue().get(0)); - request.setFields(getFields(options, GoogleAppsUtil.ID_ATTR, - GoogleAppsUtil.ETAG_ATTR, GoogleAppsUtil.PRIMARY_EMAIL_ATTR)); - request.setProjection(configuration.getProjection()); - - execute(request, - new RequestResultHandler() { - - @Override - public Boolean handleResult(final Directory.Users.Get request, final User user) { - return handler.handle( - fromUser(user, attributesToGet, configuration.getDirectory().groups())); - } - - @Override - public Boolean handleNotFound(final IOException e) { - // Do nothing if not found - return true; - } - }); - } catch (IOException e) { - LOG.warn(e, "Failed to initialize Users#Get"); - throw ConnectorException.wrap(e); - } - } - } else if (ObjectClass.GROUP.equals(objectClass)) { - if (null == key) { - // Search request - try { - // userKey excludes the customer and domain!! - Directory.Groups.List request = configuration.getDirectory().groups().list(); - if (null != query) { - StringBuilder queryBuilder = query.accept(new GroupHandler(), request); - if (null != queryBuilder) { - String queryString = queryBuilder.toString(); - LOG.ok("Executing Query: {0}", queryString); - request.setQuery(queryString); - } - if (null == request.getDomain() && null == request.getCustomer()) { - request.setCustomer(GoogleAppsUtil.MY_CUSTOMER_ID); - } - } else { - request.setCustomer(GoogleAppsUtil.MY_CUSTOMER_ID); - } - - boolean paged = false; - // Groups - if (options.getPageSize() != null && 0 < options.getPageSize()) { - request.setMaxResults(options.getPageSize()); - paged = true; - } - request.setPageToken(options.getPagedResultsCookie()); - - // Implementation to support the 'OP_ATTRIBUTES_TO_GET' - String fields = getFields(options, GoogleAppsUtil.ID_ATTR, - GoogleAppsUtil.ETAG_ATTR, GoogleAppsUtil.EMAIL_ATTR); - if (null != fields) { - request.setFields("nextPageToken,groups(" + fields + ")"); - } - - String nextPageToken = null; - do { - nextPageToken = execute(request, - new RequestResultHandler() { - - @Override - public String handleResult(final Directory.Groups.List request, final Groups value) { - if (null != value.getGroups()) { - for (Group group : value.getGroups()) { - handler.handle(fromGroup( - group, attributesToGet, configuration.getDirectory().members())); - } - } - return value.getNextPageToken(); - } - }); - request.setPageToken(nextPageToken); - } while (!paged && StringUtil.isNotBlank(nextPageToken)); - - if (paged && StringUtil.isNotBlank(nextPageToken)) { - LOG.info("Paged Search was requested"); - ((SearchResultsHandler) handler).handleResult(new SearchResult(nextPageToken, 0)); - } - } catch (IOException e) { - LOG.warn(e, "Failed to initialize Groups#List"); - throw ConnectorException.wrap(e); - } - } else { - // Read request - try { - Directory.Groups.Get request = - configuration.getDirectory().groups().get((String) key.getValue().get(0)); - request.setFields(getFields(options, GoogleAppsUtil.ID_ATTR, - GoogleAppsUtil.ETAG_ATTR, GoogleAppsUtil.EMAIL_ATTR)); - - execute(request, new RequestResultHandler() { - - @Override - public Boolean handleResult(final Directory.Groups.Get request, final Group value) { - return handler.handle(fromGroup( - value, attributesToGet, configuration.getDirectory().members())); - } - - @Override - public Boolean handleNotFound(final IOException e) { - // Do nothing if not found - return true; - } - }); - } catch (IOException e) { - LOG.warn(e, "Failed to initialize Groups#Get"); - throw ConnectorException.wrap(e); - } - } - } else if (GoogleAppsUtil.MEMBER.equals(objectClass)) { - if (null == key) { - // Search request - // TODO support AND role - try { - String groupKey = null; - - if (query instanceof EqualsFilter - && ((EqualsFilter) query).getAttribute().is(GoogleAppsUtil.GROUP_KEY_ATTR)) { - - groupKey = AttributeUtil.getStringValue(((AttributeFilter) query).getAttribute()); - } else { - throw new UnsupportedOperationException("Only EqualsFilter('groupKey') is supported"); - } - - if (StringUtil.isBlank(groupKey)) { - throw new InvalidAttributeValueException("The 'groupKey' can not be blank."); - } - Directory.Members.List request = configuration.getDirectory().members().list(groupKey); - - boolean paged = false; - // Groups - if (options.getPageSize() != null && 0 < options.getPageSize()) { - request.setMaxResults(options.getPageSize()); - paged = true; - } - request.setPageToken(options.getPagedResultsCookie()); - - String nextPageToken = null; - do { - nextPageToken = execute(request, - new RequestResultHandler() { - - @Override - public String handleResult( - final Directory.Members.List request, - final Members value) { - if (null != value.getMembers()) { - for (Member group : value.getMembers()) { - handler.handle(MembersHandler.from(request.getGroupKey(), group)); - } - } - return value.getNextPageToken(); - } - }); - request.setPageToken(nextPageToken); - } while (!paged && StringUtil.isNotBlank(nextPageToken)); - - if (paged && StringUtil.isNotBlank(nextPageToken)) { - LOG.info("Paged Search was requested"); - ((SearchResultsHandler) handler).handleResult(new SearchResult(nextPageToken, 0)); - } - } catch (IOException e) { - LOG.warn(e, "Failed to initialize Groups#List"); - throw ConnectorException.wrap(e); - } - } else { - // Read request - try { - String[] ids = ((Uid) key).getUidValue().split("/"); - if (ids.length != 2) { - // TODO fix the exception - throw new InvalidAttributeValueException("Unrecognised UID format"); - } - - Directory.Members.Get request = configuration.getDirectory().members().get(ids[0], ids[1]); - - execute(request, - new RequestResultHandler() { - - @Override - public Boolean handleResult(final Directory.Members.Get request, final Member value) { - return handler.handle(MembersHandler.from(request.getGroupKey(), value)); - } - - @Override - public Boolean handleNotFound(final IOException e) { - // Do nothing if not found - return true; - } - }); - } catch (IOException e) { - LOG.warn(e, "Failed to initialize Groups#Get"); - throw ConnectorException.wrap(e); - } - } - } else if (GoogleAppsUtil.ORG_UNIT.equals(objectClass)) { - if (null == key) { - // Search request - try { - Directory.Orgunits.List request = configuration.getDirectory().orgunits(). - list(GoogleAppsUtil.MY_CUSTOMER_ID); - if (null != query) { - if (query instanceof StartsWithFilter - && AttributeUtil.namesEqual(GoogleAppsUtil.ORG_UNIT_PATH_ATTR, - ((StartsWithFilter) query).getName())) { - - request.setOrgUnitPath(((StartsWithFilter) query).getValue()); - } else { - throw new UnsupportedOperationException( - "Only StartsWithFilter('orgUnitPath') is supported"); - } - } else { - request.setOrgUnitPath("/"); - } - - String scope = options.getScope(); - if (OperationOptions.SCOPE_OBJECT.equalsIgnoreCase(scope) - || OperationOptions.SCOPE_ONE_LEVEL.equalsIgnoreCase(scope)) { - - request.setType("children"); - } else { - request.setType("all"); - } - - // Implementation to support the 'OP_ATTRIBUTES_TO_GET' - String fields = getFields(options, GoogleAppsUtil.ORG_UNIT_PATH_ATTR, - GoogleAppsUtil.ETAG_ATTR, GoogleAppsUtil.NAME_ATTR); - if (null != fields) { - request.setFields("organizationUnits(" + fields + ")"); - } - - execute(request, - new RequestResultHandler() { - - @Override - public Void handleResult(final Directory.Orgunits.List request, - final OrgUnits value) { - if (null != value.getOrganizationUnits()) { - for (OrgUnit group : value.getOrganizationUnits()) { - handler.handle(OrgunitsHandler.from(group, attributesToGet)); - } - } - return null; - } - }); - } catch (IOException e) { - LOG.warn(e, "Failed to initialize OrgUnits#List"); - throw ConnectorException.wrap(e); - } - } else { - // Read request - try { - Directory.Orgunits.Get request = configuration.getDirectory().orgunits(). - get(GoogleAppsUtil.MY_CUSTOMER_ID, Arrays.asList((String) key.getValue().get(0))); - request.setFields(getFields(options, GoogleAppsUtil.ORG_UNIT_PATH_ATTR, - GoogleAppsUtil.ETAG_ATTR, GoogleAppsUtil.NAME_ATTR)); - - execute(request, - new RequestResultHandler() { - - @Override - public Boolean handleResult(final Directory.Orgunits.Get request, final OrgUnit value) { - return handler.handle(OrgunitsHandler.from(value, attributesToGet)); - } - - @Override - public Boolean handleNotFound(final IOException e) { - // Do nothing if not found - return true; - } - }); - } catch (IOException e) { - LOG.warn(e, "Failed to initialize OrgUnits#Get"); - throw ConnectorException.wrap(e); - } - } - } else if (GoogleAppsUtil.LICENSE_ASSIGNMENT.equals(objectClass)) { - if (null == key) { - // Search request - try { - String productId = ""; - String skuId = ""; - - boolean paged = false; - - LicensingRequest request = null; - - if (StringUtil.isBlank(productId)) { - // TODO iterate over the three productids - throw new ConnectorException("productId is required"); - } else if (StringUtil.isBlank(skuId)) { - Licensing.LicenseAssignments.ListForProduct r = - configuration.getLicensing().licenseAssignments(). - listForProduct(productId, GoogleAppsUtil.MY_CUSTOMER_ID); - - if (options.getPageSize() != null && 0 < options.getPageSize()) { - r.setMaxResults(Long.valueOf(options.getPageSize())); - paged = true; - } - r.setPageToken(options.getPagedResultsCookie()); - request = r; - } else { - Licensing.LicenseAssignments.ListForProductAndSku r = - configuration.getLicensing().licenseAssignments(). - listForProductAndSku(productId, skuId, GoogleAppsUtil.MY_CUSTOMER_ID); - - if (options.getPageSize() != null && 0 < options.getPageSize()) { - r.setMaxResults(Long.valueOf(options.getPageSize())); - paged = true; - } - r.setPageToken(options.getPagedResultsCookie()); - request = r; - } - - String nextPageToken = null; - do { - nextPageToken = execute(request, - new RequestResultHandler< - LicensingRequest< - LicenseAssignmentList>, LicenseAssignmentList, String>() { - - @Override - public String handleResult( - final LicensingRequest request, - final LicenseAssignmentList value) { - - if (null != value.getItems()) { - for (LicenseAssignment resource : value.getItems()) { - handler.handle(LicenseAssignmentsHandler.from(resource)); - } - } - return value.getNextPageToken(); - } - }); - if (request instanceof Licensing.LicenseAssignments.ListForProduct) { - ((Licensing.LicenseAssignments.ListForProduct) request).setPageToken(nextPageToken); - } else { - ((Licensing.LicenseAssignments.ListForProductAndSku) request).setPageToken(nextPageToken); - } - } while (!paged && StringUtil.isNotBlank(nextPageToken)); - - if (paged && StringUtil.isNotBlank(nextPageToken)) { - LOG.info("Paged Search was requested"); - ((SearchResultsHandler) handler).handleResult(new SearchResult(nextPageToken, 0)); - } - - } catch (IOException e) { - LOG.warn(e, "Failed to initialize Groups#List"); - throw ConnectorException.wrap(e); - } - } else { - // Read request - try { - Matcher name = LicenseAssignmentsHandler.LICENSE_NAME_PATTERN.matcher(((Uid) key).getUidValue()); - if (!name.matches()) { - return; - } - - String productId = name.group(0); - String skuId = name.group(1); - String userId = name.group(2); - - Licensing.LicenseAssignments.Get request = - configuration.getLicensing().licenseAssignments().get(productId, skuId, userId); - - execute(request, - new RequestResultHandler() { - - @Override - public Boolean handleResult( - final Licensing.LicenseAssignments.Get request, - final LicenseAssignment value) { - - return handler.handle(LicenseAssignmentsHandler.from(value)); - } - - @Override - public Boolean handleNotFound(final IOException e) { - // Do nothing if not found - return true; - } - }); - } catch (IOException e) { - LOG.warn(e, "Failed to initialize Groups#Get"); - throw ConnectorException.wrap(e); - } - } - } else { - LOG.warn("Search of type {0} is not supported", configuration.getConnectorMessages() - .format(objectClass.getDisplayNameKey(), objectClass.getObjectClassValue())); - throw new UnsupportedOperationException("Search of type" - + objectClass.getObjectClassValue() + " is not supported"); - } - - } - - protected static Attribute getKeyFromFilter(final ObjectClass objectClass, final Filter filter) { - Attribute key = null; - if (filter instanceof EqualsFilter) { - // Account, Group, OrgUnit object classes - Attribute filterAttr = ((EqualsFilter) filter).getAttribute(); - if (filterAttr instanceof Uid) { - key = filterAttr; - } else if (ObjectClass.ACCOUNT.equals(objectClass) || ObjectClass.GROUP.equals(objectClass) - && (filterAttr instanceof Name - || filterAttr.getName().equalsIgnoreCase(GoogleAppsUtil.ALIASES_ATTR))) { - key = filterAttr; - } else if (GoogleAppsUtil.ORG_UNIT.equals(objectClass) && filterAttr.getName().equalsIgnoreCase( - GoogleAppsUtil.ORG_UNIT_PATH_ATTR)) { - key = filterAttr; - } else if (ObjectClass.GROUP.equals(objectClass) && filterAttr.is(GoogleAppsUtil.EMAIL_ATTR)) { - key = filterAttr; - } - } else if (filter instanceof AndFilter) { - // Member object class - if (GoogleAppsUtil.MEMBER.equals(objectClass)) { - Attribute groupKey = null; - Attribute memberKey = null; - StringBuilder memberId = new StringBuilder(); - - Collection filters = ((AndFilter) filter).getFilters(); - for (Filter f : filters) { - if (f instanceof EqualsFilter) { - Attribute filterAttr = ((EqualsFilter) f).getAttribute(); - if (filterAttr.getName().equalsIgnoreCase(GoogleAppsUtil.GROUP_KEY_ATTR)) { - groupKey = filterAttr; - } else if (filterAttr.getName().equalsIgnoreCase(GoogleAppsUtil.EMAIL_ATTR) - || filterAttr.getName().equalsIgnoreCase(GoogleAppsUtil.ALIAS_ATTR) - || filterAttr instanceof Uid) { - memberKey = filterAttr; - } else { - throw new UnsupportedOperationException( - "Only AndFilter('groupKey','memberKey') is supported"); - } - } else { - throw new UnsupportedOperationException( - "Only AndFilter('groupKey','memberKey') is supported"); - } - } - if (memberKey != null && groupKey != null) { - memberId.append(groupKey.getValue().get(0)); - memberId.append("/"); - memberId.append(memberKey.getValue().get(0)); - key = new Uid(memberId.toString()); - } - } - } - return key; - } - - protected static Set getAttributesToGet(final ObjectClass objectClass, final OperationOptions options) { - Set attributesToGet = null; - if (null != options.getAttributesToGet()) { - attributesToGet = CollectionUtil.newCaseInsensitiveSet(); - if (GoogleAppsUtil.ORG_UNIT.equals(objectClass)) { - attributesToGet.add(GoogleAppsUtil.ORG_UNIT_PATH_ATTR); - } else { - attributesToGet.add(GoogleAppsUtil.ID_ATTR); - } - attributesToGet.add(GoogleAppsUtil.ETAG_ATTR); - for (String attribute : options.getAttributesToGet()) { - int i = attribute.indexOf('/'); - if (i == 0) { - // Strip off the leading '/' - attribute = attribute.substring(1); - i = attribute.indexOf('/'); - } - int j = attribute.indexOf('('); - if (i < 0 && j < 0) { - attributesToGet.add(attribute); - } else if (i == 0 || j == 0) { - throw new IllegalArgumentException("Invalid attribute name to get:/" + attribute); - } else { - int l = attribute.length(); - if (i > 0) { - l = Math.min(l, i); - } - if (j > 0) { - l = Math.min(l, j); - } - attributesToGet.add(attribute.substring(0, l)); - } - } - } - return attributesToGet; - } - - protected static String googleName(final ObjectClass objectClass, final String attributeName) { - if (AttributeUtil.namesEqual(Name.NAME, attributeName)) { - if (ObjectClass.ACCOUNT.equals(objectClass)) { - return GoogleAppsUtil.PRIMARY_EMAIL_ATTR; - } else if (ObjectClass.GROUP.equals(objectClass)) { - return GoogleAppsUtil.EMAIL_ATTR; - } else { - return GoogleAppsUtil.NAME_ATTR; - } - } - - if (AttributeUtil.namesEqual(GoogleAppsUtil.DESCRIPTION_ATTR, attributeName)) { - return GoogleAppsUtil.DESCRIPTION_ATTR; - } - - if (AttributeUtil.namesEqual(GoogleAppsUtil.FAMILY_NAME_ATTR, attributeName)) { - return "name/familyName"; - } - if (AttributeUtil.namesEqual(GoogleAppsUtil.GIVEN_NAME_ATTR, attributeName)) { - return "name/givenName"; - } - if (AttributeUtil.namesEqual(GoogleAppsUtil.FULL_NAME_ATTR, attributeName)) { - return "name/fullName"; - } - return attributeName; //__GROUPS__ //__PASSWORD__ - } - - protected static Set getAttributesToGet(final OperationOptions options) { - Set attributesToGet = null; - if (null != options.getAttributesToGet()) { - attributesToGet = CollectionUtil.newCaseInsensitiveSet(); - for (String attribute : options.getAttributesToGet()) { - StringBuilder builder = new StringBuilder(); - loop: - for (int i = 0; i < attribute.length(); i++) { - char c = attribute.charAt(i); - switch (c) { - case '/': - if (i == 0) { - // Strip off the leading '/' - break; - } else if (i == 1) { - throw new IllegalArgumentException("Invalid attribute name to get:" - + attribute); - } - break loop; - - case '(': - if (i == 0) { - throw new IllegalArgumentException("Invalid attribute name to get:" - + attribute); - } - break loop; - - default: - builder.append(c); - } - } - attributesToGet.add(builder.toString()); - } - } - return attributesToGet; - } - - protected String getFields(final OperationOptions options, final String... nameAttribute) { - if (null != options.getAttributesToGet()) { - Set attributes = CollectionUtil.newCaseInsensitiveSet(); - attributes.addAll(Arrays.asList(nameAttribute)); - final boolean notBlankCustomSchemas = StringUtil.isNotBlank(configuration.getCustomSchemasJSON()); - final List customSchemaNames = notBlankCustomSchemas - ? customSchemaNames(configuration.getCustomSchemasJSON()) - : new ArrayList<>(); - for (String attribute : options.getAttributesToGet()) { - if (AttributeUtil.namesEqual(GoogleAppsUtil.DESCRIPTION_ATTR, attribute)) { - attributes.add(GoogleAppsUtil.DESCRIPTION_ATTR); - } else if (AttributeUtil.isSpecialName(attribute)) { - // nothing to do - } else if (AttributeUtil.namesEqual(GoogleAppsUtil.FAMILY_NAME_ATTR, attribute)) { - attributes.add("name/familyName"); - } else if (AttributeUtil.namesEqual(GoogleAppsUtil.GIVEN_NAME_ATTR, attribute)) { - attributes.add("name/givenName"); - } else if (AttributeUtil.namesEqual(GoogleAppsUtil.FULL_NAME_ATTR, attribute)) { - attributes.add("name/fullName"); - } else if (!customSchemaNames.contains(attribute)) { - attributes.add(attribute); - } - // return also customSchemas according to configuration - if ("full".equals(configuration.getProjection()) && notBlankCustomSchemas) { - attributes.add(GoogleAppsUtil.CUSTOM_SCHEMAS); - } - } - return StringUtil.join(attributes, GoogleAppsUtil.COMMA); - } - return null; + new GoogleAppsSearch(configuration, objectClass, query, handler, options).execute(); } @Override - public void test() { - try { - configuration.test(); - } catch (Exception e) { - throw new ConnectorException(e); - } - } + public Uid create( + final ObjectClass objectClass, + final Set createAttributes, + final OperationOptions options) { - @Override - public void checkAlive() { - test(); + return new GoogleAppsCreate(configuration, objectClass, createAttributes).execute(); } @Override @@ -1243,738 +196,21 @@ public Uid update( final Set replaceAttributes, final OperationOptions options) { - final AttributesAccessor accessor = new AttributesAccessor(replaceAttributes); - - Uid uidAfterUpdate = uid; - if (ObjectClass.ACCOUNT.equals(objectClass)) { - Directory.Users.Patch patch = UserHandler.updateUser( - configuration.getDirectory().users(), - uid.getUidValue(), - accessor, - configuration.getCustomSchemasJSON()); - if (null != patch) { - uidAfterUpdate = execute(patch, new RequestResultHandler() { - - @Override - public Uid handleResult(final Directory.Users.Patch request, final User value) { - LOG.ok("User is Updated:{0}", value.getId()); - return new Uid(value.getId(), value.getEtag()); - } - }); - } - Attribute groups = accessor.find(PredefinedAttributes.GROUPS_NAME); - if (null != groups && null != groups.getValue()) { - final Directory.Members service = configuration.getDirectory().members(); - if (groups.getValue().isEmpty()) { - // Remove all membership - for (String groupKey : listGroups( - configuration.getDirectory().groups(), - uidAfterUpdate.getUidValue(), - configuration.getDomain())) { - - execute(MembersHandler.delete(service, groupKey, uidAfterUpdate.getUidValue()), - new RequestResultHandler() { - - @Override - public Object handleResult(final Directory.Members.Delete request, final Void value) { - return null; - } - - @Override - public Object handleNotFound(final IOException e) { - // It may be an indirect membership, - // not able to delete - return null; - } - }); - } - } else { - final Set activeGroups = listGroups( - configuration.getDirectory().groups(), - uidAfterUpdate.getUidValue(), - configuration.getDomain()); - - final List addGroups = new ArrayList<>(); - final Set keepGroups = CollectionUtil.newCaseInsensitiveSet(); - - for (Object member : groups.getValue()) { - if (member instanceof String) { - if (activeGroups.contains((String) member)) { - keepGroups.add((String) member); - } else { - String email = accessor.getName().getNameValue(); - addGroups.add(MembersHandler.create(service, (String) member, email, null)); - } - } else if (null != member) { - // throw error/revert? - throw new InvalidAttributeValueException("Attribute '__GROUPS__' must be a String list"); - } - } - - // Add new Member object - for (Directory.Members.Insert insert : addGroups) { - execute(insert, new RequestResultHandler() { - - @Override - public Object handleResult(final Directory.Members.Insert request, final Member value) { - return null; - } - - @Override - public Object handleDuplicate(final IOException e) { - // Do nothing - return null; - } - }); - } - - // Delete existing Member object - if (activeGroups.removeAll(keepGroups)) { - for (String groupKey : activeGroups) { - execute(MembersHandler.delete(service, groupKey, uidAfterUpdate.getUidValue()), - new RequestResultHandler() { - - @Override - public Object handleResult(final Directory.Members.Delete request, final Void value) { - return null; - } - - @Override - public Object handleNotFound(final IOException e) { - // It may be an indirect membership, not able to delete - return null; - } - }); - } - } - } - } - // GOOGLEAPPS-9 - // license management: if remove license param is true and __ENABLE__ is false perform delete license - // license read must be performed with the user primaryEmail, userId is not allowed - if (configuration.getRemoveLicenseOnDisable() - && accessor.hasAttribute(OperationalAttributes.ENABLE_NAME) - && !accessor.findBoolean(OperationalAttributes.ENABLE_NAME) - && StringUtil.isNotBlank(accessor.findString(GoogleAppsUtil.PRIMARY_EMAIL_ATTR))) { - for (String skuId : configuration.getSkuIds()) { - // 1. retrieve user license - try { - // use email as key - Licensing.LicenseAssignments.Get request = - configuration.getLicensing().licenseAssignments().get( - configuration.getProductId(), - skuId, - accessor.findString(GoogleAppsUtil.PRIMARY_EMAIL_ATTR)); - execute(request, - new RequestResultHandler< - Licensing.LicenseAssignments.Get, LicenseAssignment, Boolean>() { - - @Override - public Boolean handleResult( - final Licensing.LicenseAssignments.Get request, - final LicenseAssignment value) { - - try { - // 2. remove license - delete(GoogleAppsUtil.LICENSE_ASSIGNMENT, - new Uid(GoogleAppsUtil.generateLicenseId( - value.getProductId(), - value.getSkuId(), - value.getUserId())), null); - } catch (Exception e) { - LOG.error(e, "Failed to delete license for user {0}", value.getUserId()); - throw ConnectorException.wrap(e); - } - return true; - } - - @Override - public Boolean handleNotFound(final IOException e) { - // Do nothing if not found - return true; - } - }); - } catch (IOException e) { - LOG.error(e, "Unable to find license for {0}-{1}-{2}", configuration.getProductId(), skuId, - accessor.findString(GoogleAppsUtil.PRIMARY_EMAIL_ATTR)); - } - } - } - } else if (ObjectClass.GROUP.equals(objectClass)) { - final Directory.Groups.Patch patch = GroupHandler.update( - configuration.getDirectory().groups(), - uid.getUidValue(), - accessor); - if (null != patch) { - uidAfterUpdate = execute(patch, new RequestResultHandler() { - - @Override - public Uid handleResult(final Directory.Groups.Patch request, final Group value) { - LOG.ok("Group is Updated:{0}", value.getEmail()); - return new Uid(value.getEmail(), value.getEtag()); - } - }); - } - Attribute members = accessor.find(GoogleAppsUtil.MEMBERS_ATTR); - if (null != members && null != members.getValue()) { - final Directory.Members service = configuration.getDirectory().members(); - if (members.getValue().isEmpty()) { - // Remove all membership - for (Map member : listMembers(service, uidAfterUpdate.getUidValue(), null)) { - execute(MembersHandler.delete( - service, uidAfterUpdate.getUidValue(), member.get(GoogleAppsUtil.EMAIL_ATTR)), - new RequestResultHandler() { - - @Override - public Object handleResult(final Directory.Members.Delete request, final Void value) { - return null; - } - - @Override - public Object handleNotFound(final IOException e) { - // Do nothing - return null; - } - }); - } - } else { - final List> activeMembership = - listMembers(service, uidAfterUpdate.getUidValue(), null); - - final List addMembership = new ArrayList<>(); - final List patchMembership = new ArrayList<>(); - - for (Object member : members.getValue()) { - if (member instanceof Map) { - String email = (String) ((Map) member).get(GoogleAppsUtil.EMAIL_ATTR); - if (null == email) { - continue; - } - String role = (String) ((Map) member).get(GoogleAppsUtil.ROLE_ATTR); - if (null == role) { - role = "MEMBER"; - } - - boolean notMember = true; - for (Map a : activeMembership) { - // How to handle ROLE update? - // OWNER -> MANAGER -> MEMBER - if (email.equalsIgnoreCase(a.get(GoogleAppsUtil.EMAIL_ATTR))) { - a.put("keep", null); - if (!role.equalsIgnoreCase(a.get(GoogleAppsUtil.ROLE_ATTR))) { - patchMembership.add(MembersHandler.update( - service, uidAfterUpdate.getUidValue(), email, role)); - } - notMember = false; - break; - } - } - if (notMember) { - addMembership.add(MembersHandler.create( - service, uidAfterUpdate.getUidValue(), email, role)); - } - } else if (null != member) { - // throw error/revert? - throw new InvalidAttributeValueException( - "Attribute 'members' must be a Map list"); - } - } - - // Add new Member object - for (Directory.Members.Insert insert : addMembership) { - execute(insert, new RequestResultHandler() { - - @Override - public Object handleResult(final Directory.Members.Insert request, final Member value) { - return null; - } - - @Override - public Object handleDuplicate(final IOException e) { - // Do nothing - return null; - } - }); - } - - // Update existing Member object - for (Directory.Members.Patch request : patchMembership) { - execute(request, new RequestResultHandler() { - - @Override - public Object handleResult(final Directory.Members.Patch request, final Member value) { - return null; - } - }); - } - - // Delete existing Member object - for (Map am : activeMembership) { - if (!am.containsKey("keep")) { - execute(MembersHandler.delete( - service, uidAfterUpdate.getUidValue(), am.get(GoogleAppsUtil.EMAIL_ATTR)), - new RequestResultHandler() { - - @Override - public Object handleResult(final Directory.Members.Delete request, final Void value) { - return null; - } - - @Override - public Object handleNotFound(final IOException e) { - // Do nothing - return null; - } - }); - } - } - } - } - } else if (GoogleAppsUtil.MEMBER.equals(objectClass)) { - String role = accessor.findString(GoogleAppsUtil.ROLE_ATTR); - if (StringUtil.isNotBlank(role)) { - String[] ids = uid.getUidValue().split("/"); - if (ids.length == 2) { - final Directory.Members.Patch patch = MembersHandler.update( - configuration.getDirectory().members(), ids[0], ids[1], role). - setFields(GoogleAppsUtil.EMAIL_ETAG); - uidAfterUpdate = execute(patch, new RequestResultHandler() { - - @Override - public Uid handleResult(final Directory.Members.Patch request, final Member value) { - LOG.ok("Member is updated:{0}/{1}", request.getGroupKey(), value.getEmail()); - return MembersHandler.generateUid(request.getGroupKey(), value); - } - }); - } else { - throw new UnknownUidException("Invalid ID format"); - } - } - } else if (GoogleAppsUtil.ORG_UNIT.equals(objectClass)) { - final Directory.Orgunits.Patch patch = OrgunitsHandler.update( - configuration.getDirectory().orgunits(), uid.getUidValue(), accessor); - if (null != patch) { - uidAfterUpdate = execute(patch, new RequestResultHandler() { - - @Override - public Uid handleResult(final Directory.Orgunits.Patch request, final OrgUnit value) { - LOG.ok("OrgUnit is updated:{0}", value.getName()); - return OrgunitsHandler.generateUid(value); - } - }); - } - } else if (GoogleAppsUtil.LICENSE_ASSIGNMENT.equals(objectClass)) { - final Licensing.LicenseAssignments.Patch patch = LicenseAssignmentsHandler.update( - configuration.getLicensing().licenseAssignments(), - uid.getUidValue(), - accessor); - if (null != patch) { - uidAfterUpdate = execute(patch, - new RequestResultHandler() { - - @Override - public Uid handleResult( - final Licensing.LicenseAssignments.Patch request, - final LicenseAssignment value) { - - LOG.ok("LicenseAssignment is Updated:{0}/{1}/{2}", - value.getProductId(), value.getSkuId(), value.getUserId()); - return LicenseAssignmentsHandler.generateUid(value); - } - }); - } - } else { - LOG.warn("Update of type {0} is not supported", configuration.getConnectorMessages() - .format(objectClass.getDisplayNameKey(), objectClass.getObjectClassValue())); - throw new UnsupportedOperationException("Update of type" - + objectClass.getObjectClassValue() + " is not supported"); - } - return uidAfterUpdate; + return new GoogleAppsUpdate(configuration, objectClass, uid).update(replaceAttributes); } - private static Object getValueFromKey( - final String customSchema, - final Map> customSchemas) { - - String[] names = customSchema.split("\\."); - return names.length > 1 - ? customSchemas.get(names[0]) != null ? customSchemas.get(names[0]).get(names[1]) : null - : null; - } - - protected ConnectorObject fromUser( - final User user, - final Set attributesToGet, - final Directory.Groups service) { - - ConnectorObjectBuilder builder = new ConnectorObjectBuilder(); - if (null != user.getEtag()) { - builder.setUid(new Uid(user.getId(), user.getEtag())); - } else { - builder.setUid(user.getId()); - } - builder.setName(user.getPrimaryEmail()); - if (user.getSuspended() != null) { - builder.addAttribute(AttributeBuilder.build(OperationalAttributes.ENABLE_NAME, !user.getSuspended())); - } - - if ((null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.ID_ATTR))) { - builder.addAttribute(AttributeBuilder.build(GoogleAppsUtil.ID_ATTR, user.getId())); - } - if ((null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.PRIMARY_EMAIL_ATTR))) { - builder.addAttribute(AttributeBuilder.build(GoogleAppsUtil.PRIMARY_EMAIL_ATTR, user.getPrimaryEmail())); - } - // Optional - // If both givenName and familyName are empty then Google didn't return with 'name' - if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.GIVEN_NAME_ATTR)) { - builder.addAttribute(AttributeBuilder.build(GoogleAppsUtil.GIVEN_NAME_ATTR, - null != user.getName() ? user.getName().getGivenName() : null)); - } - if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.FAMILY_NAME_ATTR)) { - builder.addAttribute(AttributeBuilder.build(GoogleAppsUtil.FAMILY_NAME_ATTR, - null != user.getName() ? user.getName().getFamilyName() : null)); - } - if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.FULL_NAME_ATTR)) { - builder.addAttribute(AttributeBuilder.build(GoogleAppsUtil.FULL_NAME_ATTR, - null != user.getName() ? user.getName().getFullName() : null)); - } - - if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.IS_ADMIN_ATTR)) { - builder.addAttribute(AttributeBuilder.build(GoogleAppsUtil.IS_ADMIN_ATTR, user.getIsAdmin())); - } - if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.IS_DELEGATED_ADMIN_ATTR)) { - builder.addAttribute(AttributeBuilder.build( - GoogleAppsUtil.IS_DELEGATED_ADMIN_ATTR, user.getIsDelegatedAdmin())); - } - if ((null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.LAST_LOGIN_TIME_ATTR)) - && user.getLastLoginTime() != null) { - - builder.addAttribute(AttributeBuilder.build( - GoogleAppsUtil.LAST_LOGIN_TIME_ATTR, user.getLastLoginTime().toString())); - } - if ((null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.CREATION_TIME_ATTR)) - && user.getCreationTime() != null) { - - builder.addAttribute(AttributeBuilder.build( - GoogleAppsUtil.CREATION_TIME_ATTR, user.getCreationTime().toString())); - } - if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.AGREED_TO_TERMS_ATTR)) { - builder.addAttribute(AttributeBuilder.build(GoogleAppsUtil.AGREED_TO_TERMS_ATTR, user.getAgreedToTerms())); - } - if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.SUSPENSION_REASON_ATTR)) { - builder.addAttribute(AttributeBuilder.build( - GoogleAppsUtil.SUSPENSION_REASON_ATTR, user.getSuspensionReason())); - } - if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.CHANGE_PASSWORD_AT_NEXT_LOGIN_ATTR)) { - builder.addAttribute(AttributeBuilder.build( - GoogleAppsUtil.CHANGE_PASSWORD_AT_NEXT_LOGIN_ATTR, user.getChangePasswordAtNextLogin())); - } - if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.IP_WHITELISTED_ATTR)) { - builder.addAttribute(AttributeBuilder.build(GoogleAppsUtil.IP_WHITELISTED_ATTR, user.getIpWhitelisted())); - } - if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.IMS_ATTR)) { - builder.addAttribute(AttributeBuilder.build(GoogleAppsUtil.IMS_ATTR, (Collection) user.getIms())); - } - if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.EMAILS_ATTR)) { - builder.addAttribute(AttributeBuilder.build(GoogleAppsUtil.EMAILS_ATTR, (Collection) user.getEmails())); - } - if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.EXTERNAL_IDS_ATTR)) { - builder.addAttribute(AttributeBuilder.build( - GoogleAppsUtil.EXTERNAL_IDS_ATTR, (Collection) user.getExternalIds())); - } - if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.RELATIONS_ATTR)) { - builder.addAttribute(AttributeBuilder.build( - GoogleAppsUtil.RELATIONS_ATTR, (Collection) user.getRelations())); - } - if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.ADDRESSES_ATTR)) { - builder.addAttribute(AttributeBuilder.build( - GoogleAppsUtil.ADDRESSES_ATTR, (Collection) user.getAddresses())); - } - if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.ORGANIZATIONS_ATTR)) { - builder.addAttribute(AttributeBuilder.build( - GoogleAppsUtil.ORGANIZATIONS_ATTR, (Collection) user.getOrganizations())); - } - if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.PHONES_ATTR)) { - builder.addAttribute(AttributeBuilder.build(GoogleAppsUtil.PHONES_ATTR, (Collection) user.getPhones())); - } - if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.ALIASES_ATTR)) { - builder.addAttribute(AttributeBuilder.build(GoogleAppsUtil.ALIASES_ATTR, user.getAliases())); - } - - if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.NON_EDITABLE_ALIASES_ATTR)) { - builder.addAttribute(AttributeBuilder.build( - GoogleAppsUtil.NON_EDITABLE_ALIASES_ATTR, user.getNonEditableAliases())); - } - - if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.CUSTOMER_ID_ATTR)) { - builder.addAttribute(AttributeBuilder.build(GoogleAppsUtil.CUSTOMER_ID_ATTR, user.getCustomerId())); - } - if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.ORG_UNIT_PATH_ATTR)) { - builder.addAttribute(AttributeBuilder.build(GoogleAppsUtil.ORG_UNIT_PATH_ATTR, user.getOrgUnitPath())); - } - if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.IS_MAILBOX_SETUP_ATTR)) { - builder.addAttribute(AttributeBuilder.build( - GoogleAppsUtil.IS_MAILBOX_SETUP_ATTR, user.getIsMailboxSetup())); - } - if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.INCLUDE_IN_GLOBAL_ADDRESS_LIST_ATTR)) { - builder.addAttribute(AttributeBuilder.build( - GoogleAppsUtil.INCLUDE_IN_GLOBAL_ADDRESS_LIST_ATTR, user.getIncludeInGlobalAddressList())); - } - if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.THUMBNAIL_PHOTO_URL_ATTR)) { - builder.addAttribute( - AttributeBuilder.build(GoogleAppsUtil.THUMBNAIL_PHOTO_URL_ATTR, user.getThumbnailPhotoUrl())); - } - if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.DELETION_TIME_ATTR)) { - builder.addAttribute(AttributeBuilder.build(GoogleAppsUtil.DELETION_TIME_ATTR, - null != user.getDeletionTime() ? user.getDeletionTime().toString() : null)); - } - if (null == attributesToGet || ("full".equals(configuration.getProjection()) - && StringUtil.isNotBlank(configuration.getCustomSchemasJSON()))) { - - GoogleAppsUtil.extractCustomSchemas(configuration.getCustomSchemasJSON()).forEach(customSchema -> { - if (customSchema.getType().equals("object")) { - // manage only first level inner schemas - for (GoogleAppsCustomSchema innerSchema : customSchema.getInnerSchemas()) { - String innerSchemaName = customSchema.getName() + "." + innerSchema.getName(); - builder.addAttribute(AttributeBuilder.build( - innerSchemaName, - null != user.getCustomSchemas() - ? getValueFromKey(innerSchemaName, user.getCustomSchemas()) - : null)); - } - } else { - LOG.warn("CustomSchema type {0} not allowed at this level", customSchema.getType()); - } - }); - } - // Expensive to get - if (null != attributesToGet && attributesToGet.contains(PredefinedAttributes.GROUPS_NAME)) { - builder.addAttribute(AttributeBuilder.build(PredefinedAttributes.GROUPS_NAME, - listGroups(service, user.getId(), configuration.getDomain()))); - } - - return builder.build(); - } - - protected static ConnectorObject fromGroup( - final Group group, - final Set attributesToGet, - final Directory.Members service) { - - ConnectorObjectBuilder builder = new ConnectorObjectBuilder(); - builder.setObjectClass(ObjectClass.GROUP); - - if (null != group.getEtag()) { - builder.setUid(new Uid(group.getEmail(), group.getEtag())); - } else { - builder.setUid(group.getEmail()); - } - builder.setName(group.getEmail()); - - // Optional - if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.NAME_ATTR)) { - builder.addAttribute(AttributeBuilder.build(GoogleAppsUtil.NAME_ATTR, group.getName())); - } - if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.EMAIL_ATTR)) { - builder.addAttribute(AttributeBuilder.build(GoogleAppsUtil.EMAIL_ATTR, group.getEmail())); - } - if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.DESCRIPTION_ATTR)) { - builder.addAttribute(AttributeBuilder.build(GoogleAppsUtil.DESCRIPTION_ATTR, group.getDescription())); - } - - if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.ADMIN_CREATED_ATTR)) { - builder.addAttribute(AttributeBuilder.build(GoogleAppsUtil.ADMIN_CREATED_ATTR, group.getAdminCreated())); - } - if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.ALIASES_ATTR)) { - builder.addAttribute(AttributeBuilder.build(GoogleAppsUtil.ALIASES_ATTR, group.getAliases())); - } - if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.NON_EDITABLE_ALIASES_ATTR)) { - builder.addAttribute(AttributeBuilder.build( - GoogleAppsUtil.NON_EDITABLE_ALIASES_ATTR, group.getNonEditableAliases())); - } - if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.DIRECT_MEMBERS_COUNT_ATTR)) { - builder.addAttribute(AttributeBuilder.build( - GoogleAppsUtil.DIRECT_MEMBERS_COUNT_ATTR, group.getDirectMembersCount())); - } - - // Expensive to get - if (null != attributesToGet && attributesToGet.contains(GoogleAppsUtil.MEMBERS_ATTR)) { - builder.addAttribute(AttributeBuilder.build( - GoogleAppsUtil.MEMBERS_ATTR, listMembers(service, group.getId(), null))); - } - - return builder.build(); - } - - protected static List> listMembers( - final Directory.Members service, final String groupKey, final String roles) { - - final List> result = new ArrayList<>(); - try { - Directory.Members.List request = service.list(groupKey); - request.setRoles(StringUtil.isBlank(roles) ? "OWNER,MANAGER,MEMBER" : roles); - - String nextPageToken; - do { - nextPageToken = execute(request, new RequestResultHandler() { - - @Override - public String handleResult(final Directory.Members.List request, final Members value) { - if (null != value.getMembers()) { - for (Member member : value.getMembers()) { - Map m = new LinkedHashMap<>(2); - m.put(GoogleAppsUtil.EMAIL_ATTR, member.getEmail()); - m.put(GoogleAppsUtil.ROLE_ATTR, member.getRole()); - result.add(m); - } - } - return value.getNextPageToken(); - } - }); - } while (StringUtil.isNotBlank(nextPageToken)); - } catch (IOException e) { - LOG.warn(e, "Failed to initialize Members#Delete"); - throw ConnectorException.wrap(e); - } - return result; - } - - protected static Set listGroups(final Directory.Groups service, final String userKey, final String domain) { - final Set result = CollectionUtil.newCaseInsensitiveSet(); - try { - Directory.Groups.List request = service.list(); - request.setUserKey(userKey); - request.setFields("groups/email"); - // 400 Bad Request if the Customer(my_customer or exact value) is set, only domain-userKey combination - // allowed. request.setCustomer(MY_CUSTOMER_ID); - request.setDomain(domain); - - String nextPageToken; - do { - nextPageToken = execute(request, new RequestResultHandler() { - - @Override - public String handleResult(final Directory.Groups.List request, final Groups value) { - if (null != value.getGroups()) { - for (Group group : value.getGroups()) { - result.add(group.getEmail()); - } - } - return value.getNextPageToken(); - } - }); - } while (StringUtil.isNotBlank(nextPageToken)); - } catch (IOException e) { - LOG.warn(e, "Failed to initialize Members#Delete"); - throw ConnectorException.wrap(e); - } - return result; - } - - protected static , T, R> R execute( - final G request, final RequestResultHandler handler) { - - return execute( - Assertions.nullChecked(request, "Google Json ClientRequest"), - Assertions.nullChecked(handler, "handler"), -1); - } - - protected static , T, R> R execute( - final G request, final RequestResultHandler handler, final int retry) { - - try { - if (retry >= 0) { - long sleep = (long) ((1000 * Math.pow(2, retry)) + nextLong(1000)); - try { - Thread.sleep(sleep); - } catch (InterruptedException e) { - throw ConnectorException.wrap(e); - } - } - return handler.handleResult(request, request.execute()); - } catch (GoogleJsonResponseException e) { - GoogleJsonError details = e.getDetails(); - if (null != details && null != details.getErrors()) { - GoogleJsonError.ErrorInfo errorInfo = details.getErrors().get(0); - // error: 403 - LOG.error("Unable to execute request {0} - {1} - {2}", - e.getStatusCode(), e.getStatusMessage(), errorInfo.getReason()); - switch (e.getStatusCode()) { - case HttpStatusCodes.STATUS_CODE_FORBIDDEN: - if ("userRateLimitExceeded".equalsIgnoreCase(errorInfo.getReason()) - || "rateLimitExceeded".equalsIgnoreCase(errorInfo.getReason())) { - return handler.handleError(e); - } - break; - case HttpStatusCodes.STATUS_CODE_NOT_FOUND: - if ("notFound".equalsIgnoreCase(errorInfo.getReason())) { - return handler.handleNotFound(e); - } - break; - case 409: - if ("duplicate".equalsIgnoreCase(errorInfo.getReason())) { - // Already Exists - handler.handleDuplicate(e); - } - break; - case 400: - if ("invalid".equalsIgnoreCase(errorInfo.getReason())) { - // Already Exists "Invalid Ou Id" - } - break; - case HttpStatusCodes.STATUS_CODE_SERVICE_UNAVAILABLE: - if ("backendError".equalsIgnoreCase(errorInfo.getReason())) { - throw RetryableException.wrap(e.getMessage(), e); - } - break; - default: - break; - } - } - - if (e.getStatusCode() == HttpStatusCodes.STATUS_CODE_FORBIDDEN) { - LOG.error("Forbidden request"); - handler.handleError(e); - } else if (e.getStatusCode() == HttpStatusCodes.STATUS_CODE_NOT_FOUND) { - LOG.error("Endpoint not found for request"); - return handler.handleNotFound(e); - } - throw ConnectorException.wrap(e); - } catch (IOException e) { - // https://developers.google.com/admin-sdk/directory/v1/limits - // rateLimitExceeded or userRateLimitExceeded - if (retry < 5) { - return execute(request, handler, retry + 1); - } else { - return handler.handleError(e); - } - } - } + @Override + public Set updateDelta( + final ObjectClass objectClass, + final Uid uid, + final Set modifications, + final OperationOptions options) { - private static long nextLong(final long n) { - long bits, val; - do { - bits = (RANDOM.nextLong() << 1) >>> 1; - val = bits % n; - } while (bits - val + (n - 1) < 0L); - return val; + return new GoogleAppsUpdate(configuration, objectClass, uid).updateDelta(modifications); } - private static List customSchemaNames(final String customSchemasJSON) { - List customSchemas = GoogleAppsUtil.extractCustomSchemas(customSchemasJSON); - List customSchemaNames = new ArrayList<>(); - for (GoogleAppsCustomSchema customSchema : customSchemas) { - if (customSchema.getType().equals("object")) { - // parse inner schemas - String basicName = customSchema.getName(); - // manage only first level inner schemas - for (GoogleAppsCustomSchema innerSchema : customSchema.getInnerSchemas()) { - customSchemaNames.add(basicName + "." + innerSchema.getName()); - } - } else { - LOG.warn("CustomSchema type {0} not allowed at this level", customSchema.getType()); - } - } - return customSchemaNames; + @Override + public void delete(final ObjectClass objectClass, final Uid uid, final OperationOptions options) { + new GoogleAppsDelete(configuration, objectClass, uid).execute(); } } diff --git a/src/main/java/net/tirasa/connid/bundles/googleapps/GoogleAppsCreate.java b/src/main/java/net/tirasa/connid/bundles/googleapps/GoogleAppsCreate.java new file mode 100644 index 0000000..a209ed5 --- /dev/null +++ b/src/main/java/net/tirasa/connid/bundles/googleapps/GoogleAppsCreate.java @@ -0,0 +1,319 @@ +/* + * ==================== + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright (c) 2024 ConnId. All Rights Reserved + * + * The contents of this file are subject to the terms of the Common Development + * and Distribution License("CDDL") (the "License"). You may not use this file + * except in compliance with the License. + * + * You can obtain a copy of the License at + * http://opensource.org/licenses/cddl1.php + * See the License for the specific language governing permissions and limitations + * under the License. + * + * When distributing the Covered Code, include this CDDL Header Notice in each file + * and include the License file at http://opensource.org/licenses/cddl1.php. + * If applicable, add the following below this CDDL Header, with the fields + * enclosed by brackets [] replaced by your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * ==================== + */ +package net.tirasa.connid.bundles.googleapps; + +import com.google.api.services.directory.Directory; +import com.google.api.services.directory.model.Alias; +import com.google.api.services.directory.model.Group; +import com.google.api.services.directory.model.Member; +import com.google.api.services.directory.model.OrgUnit; +import com.google.api.services.directory.model.User; +import com.google.api.services.directory.model.UserMakeAdmin; +import com.google.api.services.directory.model.UserPhoto; +import com.google.api.services.licensing.Licensing; +import com.google.api.services.licensing.model.LicenseAssignment; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.identityconnectors.common.logging.Log; +import org.identityconnectors.framework.common.exceptions.ConnectorException; +import org.identityconnectors.framework.common.exceptions.InvalidAttributeValueException; +import org.identityconnectors.framework.common.exceptions.RetryableException; +import org.identityconnectors.framework.common.objects.Attribute; +import org.identityconnectors.framework.common.objects.AttributeUtil; +import org.identityconnectors.framework.common.objects.AttributesAccessor; +import org.identityconnectors.framework.common.objects.ObjectClass; +import org.identityconnectors.framework.common.objects.PredefinedAttributes; +import org.identityconnectors.framework.common.objects.Uid; + +public class GoogleAppsCreate { + + private static final Log LOG = Log.getLog(GoogleAppsCreate.class); + + private final GoogleAppsConfiguration configuration; + + private final ObjectClass objectClass; + + private final Set createAttributes; + + public GoogleAppsCreate( + final GoogleAppsConfiguration configuration, + final ObjectClass objectClass, + final Set createAttributes) { + + this.configuration = configuration; + this.objectClass = objectClass; + this.createAttributes = createAttributes; + } + + public Uid execute() { + final AttributesAccessor accessor = new AttributesAccessor(createAttributes); + + if (ObjectClass.ACCOUNT.equals(objectClass)) { + Uid uid = GoogleApiExecutor.execute(UserHandler.createUser( + configuration.getDirectory().users(), accessor, configuration.getCustomSchemasJSON()), + new RequestResultHandler() { + + @Override + public Uid handleResult(final Directory.Users.Insert request, final User value) { + LOG.ok("New User is created: {0}", value.getId()); + return new Uid(value.getId(), value.getEtag()); + } + }); + + List aliases = accessor.findList(GoogleAppsUtil.ALIASES_ATTR); + if (null != aliases) { + final Directory.Users.Aliases aliasesService = configuration.getDirectory().users().aliases(); + for (Object member : aliases) { + if (member instanceof String) { + String id = GoogleApiExecutor.execute( + UserHandler.createUserAlias(aliasesService, uid.getUidValue(), (String) member), + new RequestResultHandler() { + + @Override + public String handleResult( + final Directory.Users.Aliases.Insert request, final Alias value) { + + return value == null ? null : value.getId(); + } + }); + + if (null == id) { + // TODO make warn about failed update + } + } else if (null != member) { + // Delete user and Error or + RetryableException e = + RetryableException.wrap("Invalid attribute value: " + String.valueOf(member), uid); + e.initCause(new InvalidAttributeValueException("Attribute 'aliases' must be a String list")); + throw e; + } + } + } + + Attribute photo = accessor.find(GoogleAppsUtil.PHOTO_ATTR); + if (null != photo) { + Object photoObject = AttributeUtil.getSingleValue(photo); + if (photoObject instanceof byte[]) { + String id = GoogleApiExecutor.execute(UserHandler.createUpdateUserPhoto( + configuration.getDirectory().users().photos(), uid.getUidValue(), (byte[]) photoObject), + new RequestResultHandler() { + + @Override + public String handleResult(final Directory.Users.Photos.Update request, final UserPhoto value) { + return value == null ? null : value.getId(); + } + }); + + if (null == id) { + // TODO make warn about failed update + } + } else if (null != photoObject) { + // Delete group and Error or + RetryableException e = RetryableException.wrap( + "Invalid attribute value: " + String.valueOf(photoObject), uid); + e.initCause(new InvalidAttributeValueException("Attribute 'photo' must be a single byte[] value")); + throw e; + } + } + + Attribute isAdmin = accessor.find(GoogleAppsUtil.IS_ADMIN_ATTR); + if (null != isAdmin) { + try { + Boolean isAdminValue = AttributeUtil.getBooleanValue(isAdmin); + if (null != isAdminValue && isAdminValue) { + UserMakeAdmin content = new UserMakeAdmin(); + content.setStatus(isAdminValue); + + GoogleApiExecutor.execute( + configuration.getDirectory().users().makeAdmin(uid.getUidValue(), content), + new RequestResultHandler() { + + @Override + public Void handleResult(final Directory.Users.MakeAdmin request, final Void value) { + return null; + } + }); + } + } catch (final Exception e) { + // TODO Delete user and throw Exception + throw ConnectorException.wrap(e); + } + } + + Attribute groups = accessor.find(PredefinedAttributes.GROUPS_NAME); + if (null != groups && null != groups.getValue()) { + final Directory.Members service = configuration.getDirectory().members(); + if (!groups.getValue().isEmpty()) { + final List addGroups = new ArrayList<>(); + + for (Object member : groups.getValue()) { + if (member instanceof String) { + String email = accessor.getName().getNameValue(); + addGroups.add(MembersHandler.create(service, (String) member, email, null)); + } else if (null != member) { + // throw error/revert? + throw new InvalidAttributeValueException("Attribute '__GROUPS__' must be a String list"); + } + } + + // Add new Member object + for (Directory.Members.Insert insert : addGroups) { + GoogleApiExecutor.execute(insert, + new RequestResultHandler() { + + @Override + public Object handleResult(final Directory.Members.Insert request, final Member value) { + return null; + } + + @Override + public Object handleDuplicate(final IOException e) { + // Do nothing + return null; + } + }); + } + } + } + + return uid; + } else if (ObjectClass.GROUP.equals(objectClass)) { + // @formatter:off + /* AlreadyExistsException + * { + * "code" : 409, + * "errors" : [ { + * "domain" : "global", + * "message" : "Entity already exists.", + * "reason" : "duplicate" + * } ], + * "message" : "Entity already exists." + * } + */ + // @formatter:on + Uid uid = GoogleApiExecutor.execute( + GroupHandler.create(configuration.getDirectory().groups(), accessor), + new RequestResultHandler() { + + @Override + public Uid handleResult(final Directory.Groups.Insert request, + final Group value) { + LOG.ok("New Group is created:{0}", value.getEmail()); + return new Uid(value.getEmail(), value.getEtag()); + } + }); + List members = accessor.findList(GoogleAppsUtil.MEMBERS_ATTR); + if (null != members) { + final Directory.Members membersService = configuration.getDirectory().members(); + for (Object member : members) { + if (member instanceof Map) { + String email = (String) ((Map) member).get(GoogleAppsUtil.EMAIL_ATTR); + String role = (String) ((Map) member).get(GoogleAppsUtil.ROLE_ATTR); + + String id = GoogleApiExecutor.execute( + MembersHandler.create(membersService, uid.getUidValue(), email, role), + new RequestResultHandler() { + + @Override + public String handleResult(final Directory.Members.Insert request, final Member value) { + + return value == null ? null : value.getId(); + } + }); + + if (null == id) { + // TODO make warn about failed update + } + } else if (null != member) { + // Delete group and Error or + RetryableException e = + RetryableException.wrap("Invalid attribute value: " + String.valueOf(member), uid); + e.initCause(new InvalidAttributeValueException("Attribute 'members' must be a Map list")); + throw e; + } + } + } + + return uid; + } else if (GoogleAppsUtil.MEMBER.equals(objectClass)) { + return GoogleApiExecutor.execute( + MembersHandler.create(configuration.getDirectory().members(), accessor), + new RequestResultHandler() { + + @Override + public Uid handleResult(final Directory.Members.Insert request, final Member value) { + LOG.ok("New Member is created:{0}/{1}", request.getGroupKey(), value.getEmail()); + return MembersHandler.generateUid(request.getGroupKey(), value); + } + }); + } else if (GoogleAppsUtil.ORG_UNIT.equals(objectClass)) { + return GoogleApiExecutor.execute( + OrgunitsHandler.create(configuration.getDirectory().orgunits(), accessor), + new RequestResultHandler() { + + @Override + public Uid handleResult(final Directory.Orgunits.Insert request, final OrgUnit value) { + LOG.ok("New OrgUnit is created:{0}", value.getName()); + return OrgunitsHandler.generateUid(value); + } + }); + } else if (GoogleAppsUtil.LICENSE_ASSIGNMENT.equals(objectClass)) { + // @formatter:off + /* AlreadyExistsException + * { + * "code" : 400, + * "errors" : [ { + * "domain" : "global", + * "message" : "Invalid Ou Id", + * "reason" : "invalid" + * } ], + * "message" : "Invalid Ou Id" + * } + */ + // @formatter:on + + return GoogleApiExecutor.execute( + LicenseAssignmentsHandler.create(configuration.getLicensing().licenseAssignments(), accessor), + new RequestResultHandler() { + + @Override + public Uid handleResult( + final Licensing.LicenseAssignments.Insert request, + final LicenseAssignment value) { + + LOG.ok("LicenseAssignment is Created:{0}/{1}/{2}", + value.getProductId(), value.getSkuId(), value.getUserId()); + return LicenseAssignmentsHandler.generateUid(value); + } + }); + } else { + LOG.warn("Create of type {0} is not supported", configuration.getConnectorMessages() + .format(objectClass.getDisplayNameKey(), objectClass.getObjectClassValue())); + throw new UnsupportedOperationException("Create of type" + + objectClass.getObjectClassValue() + " is not supported"); + } + } +} diff --git a/src/main/java/net/tirasa/connid/bundles/googleapps/GoogleAppsDelete.java b/src/main/java/net/tirasa/connid/bundles/googleapps/GoogleAppsDelete.java new file mode 100644 index 0000000..f09067b --- /dev/null +++ b/src/main/java/net/tirasa/connid/bundles/googleapps/GoogleAppsDelete.java @@ -0,0 +1,134 @@ +/* + * ==================== + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright (c) 2024 ConnId. All Rights Reserved + * + * The contents of this file are subject to the terms of the Common Development + * and Distribution License("CDDL") (the "License"). You may not use this file + * except in compliance with the License. + * + * You can obtain a copy of the License at + * http://opensource.org/licenses/cddl1.php + * See the License for the specific language governing permissions and limitations + * under the License. + * + * When distributing the Covered Code, include this CDDL Header Notice in each file + * and include the License file at http://opensource.org/licenses/cddl1.php. + * If applicable, add the following below this CDDL Header, with the fields + * enclosed by brackets [] replaced by your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * ==================== + */ +package net.tirasa.connid.bundles.googleapps; + +import com.google.api.client.googleapis.services.json.AbstractGoogleJsonClientRequest; +import com.google.api.services.directory.DirectoryRequest; +import com.google.api.services.licensing.LicensingRequest; +import com.google.api.services.licensing.model.Empty; +import java.io.IOException; +import org.identityconnectors.common.logging.Log; +import org.identityconnectors.framework.common.exceptions.ConnectorException; +import org.identityconnectors.framework.common.exceptions.UnknownUidException; +import org.identityconnectors.framework.common.objects.ObjectClass; +import org.identityconnectors.framework.common.objects.Uid; + +public class GoogleAppsDelete { + + private static final Log LOG = Log.getLog(GoogleAppsDelete.class); + + private final GoogleAppsConfiguration configuration; + + private final ObjectClass objectClass; + + private final Uid uid; + + public GoogleAppsDelete( + final GoogleAppsConfiguration configuration, + final ObjectClass objectClass, + final Uid uid) { + + this.configuration = configuration; + this.objectClass = objectClass; + this.uid = uid; + } + + public void execute() { + DirectoryRequest directoryRequest = null; + LicensingRequest licesingRequest = null; + + try { + if (ObjectClass.ACCOUNT.equals(objectClass)) { + directoryRequest = configuration.getDirectory().users().delete(uid.getUidValue()); + } else if (ObjectClass.GROUP.equals(objectClass)) { + directoryRequest = configuration.getDirectory().groups().delete(uid.getUidValue()); + } else if (GoogleAppsUtil.MEMBER.equals(objectClass)) { + // @formatter:off + /* Already deleted + * { + * "code" : 400, + * "errors" : [ { + * "domain" : "global", + * "message" : "Missing required field: memberKey", + * "reason" : "required" + * } ], + * "message" : "Missing required field: memberKey" + * } + */ + // @formatter:on + String[] ids = uid.getUidValue().split("/"); + if (ids.length == 2) { + directoryRequest = configuration.getDirectory().members().delete(ids[0], ids[1]); + } else { + throw new UnknownUidException("Invalid ID format"); + } + } else if (GoogleAppsUtil.ORG_UNIT.equals(objectClass)) { + directoryRequest = configuration.getDirectory().orgunits(). + delete(GoogleAppsUtil.MY_CUSTOMER_ID, uid.getUidValue()); + } else if (GoogleAppsUtil.LICENSE_ASSIGNMENT.equals(objectClass)) { + licesingRequest = LicenseAssignmentsHandler.delete( + configuration.getLicensing().licenseAssignments(), uid.getUidValue()); + } + } catch (IOException e) { + throw ConnectorException.wrap(e); + } + + if (null == directoryRequest && null == licesingRequest) { + LOG.warn("Delete of type {0} is not supported", configuration.getConnectorMessages() + .format(objectClass.getDisplayNameKey(), objectClass.getObjectClassValue())); + throw new UnsupportedOperationException("Delete of type" + + objectClass.getObjectClassValue() + " is not supported"); + } + + if (directoryRequest != null) { + GoogleApiExecutor.execute(directoryRequest, + new RequestResultHandler, Void, Void>() { + + @Override + public Void handleResult(final AbstractGoogleJsonClientRequest request, final Void value) { + return null; + } + + @Override + public Void handleNotFound(final IOException e) { + throw new UnknownUidException(uid, objectClass); + } + }); + } + if (licesingRequest != null) { + GoogleApiExecutor.execute(licesingRequest, + new RequestResultHandler, Empty, Empty>() { + + @Override + public Empty handleResult(final AbstractGoogleJsonClientRequest request, final Empty value) { + return null; + } + + @Override + public Empty handleNotFound(final IOException e) { + throw new UnknownUidException(uid, objectClass); + } + }); + } + } +} diff --git a/src/main/java/net/tirasa/connid/bundles/googleapps/GoogleAppsSearch.java b/src/main/java/net/tirasa/connid/bundles/googleapps/GoogleAppsSearch.java new file mode 100644 index 0000000..d6b16b7 --- /dev/null +++ b/src/main/java/net/tirasa/connid/bundles/googleapps/GoogleAppsSearch.java @@ -0,0 +1,746 @@ +/* + * ==================== + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright (c) 2024 ConnId. All Rights Reserved + * + * The contents of this file are subject to the terms of the Common Development + * and Distribution License("CDDL") (the "License"). You may not use this file + * except in compliance with the License. + * + * You can obtain a copy of the License at + * http://opensource.org/licenses/cddl1.php + * See the License for the specific language governing permissions and limitations + * under the License. + * + * When distributing the Covered Code, include this CDDL Header Notice in each file + * and include the License file at http://opensource.org/licenses/cddl1.php. + * If applicable, add the following below this CDDL Header, with the fields + * enclosed by brackets [] replaced by your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * ==================== + */ +package net.tirasa.connid.bundles.googleapps; + +import com.google.api.services.directory.Directory; +import com.google.api.services.directory.model.Group; +import com.google.api.services.directory.model.Groups; +import com.google.api.services.directory.model.Member; +import com.google.api.services.directory.model.Members; +import com.google.api.services.directory.model.OrgUnit; +import com.google.api.services.directory.model.OrgUnits; +import com.google.api.services.directory.model.User; +import com.google.api.services.directory.model.Users; +import com.google.api.services.licensing.Licensing; +import com.google.api.services.licensing.LicensingRequest; +import com.google.api.services.licensing.model.LicenseAssignment; +import com.google.api.services.licensing.model.LicenseAssignmentList; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import org.identityconnectors.common.CollectionUtil; +import org.identityconnectors.common.StringUtil; +import org.identityconnectors.common.logging.Log; +import org.identityconnectors.framework.common.exceptions.ConnectorException; +import org.identityconnectors.framework.common.exceptions.InvalidAttributeValueException; +import org.identityconnectors.framework.common.objects.Attribute; +import org.identityconnectors.framework.common.objects.AttributeUtil; +import org.identityconnectors.framework.common.objects.Name; +import org.identityconnectors.framework.common.objects.ObjectClass; +import org.identityconnectors.framework.common.objects.OperationOptions; +import org.identityconnectors.framework.common.objects.ResultsHandler; +import org.identityconnectors.framework.common.objects.SearchResult; +import org.identityconnectors.framework.common.objects.SortKey; +import org.identityconnectors.framework.common.objects.Uid; +import org.identityconnectors.framework.common.objects.filter.AndFilter; +import org.identityconnectors.framework.common.objects.filter.AttributeFilter; +import org.identityconnectors.framework.common.objects.filter.EqualsFilter; +import org.identityconnectors.framework.common.objects.filter.Filter; +import org.identityconnectors.framework.common.objects.filter.StartsWithFilter; +import org.identityconnectors.framework.spi.SearchResultsHandler; + +public class GoogleAppsSearch { + + private static final Log LOG = Log.getLog(GoogleAppsSearch.class); + + private static Set getAttributesToGet(final ObjectClass objectClass, final OperationOptions options) { + Set attributesToGet = null; + if (null != options.getAttributesToGet()) { + attributesToGet = CollectionUtil.newCaseInsensitiveSet(); + if (GoogleAppsUtil.ORG_UNIT.equals(objectClass)) { + attributesToGet.add(GoogleAppsUtil.ORG_UNIT_PATH_ATTR); + } else { + attributesToGet.add(GoogleAppsUtil.ID_ATTR); + } + attributesToGet.add(GoogleAppsUtil.ETAG_ATTR); + for (String attribute : options.getAttributesToGet()) { + int i = attribute.indexOf('/'); + if (i == 0) { + // Strip off the leading '/' + attribute = attribute.substring(1); + i = attribute.indexOf('/'); + } + int j = attribute.indexOf('('); + if (i < 0 && j < 0) { + attributesToGet.add(attribute); + } else if (i == 0 || j == 0) { + throw new IllegalArgumentException("Invalid attribute name to get:/" + attribute); + } else { + int l = attribute.length(); + if (i > 0) { + l = Math.min(l, i); + } + if (j > 0) { + l = Math.min(l, j); + } + attributesToGet.add(attribute.substring(0, l)); + } + } + } + return attributesToGet; + } + + private static List customSchemaNames(final String customSchemasJSON) { + List customSchemas = GoogleAppsUtil.extractCustomSchemas(customSchemasJSON); + List customSchemaNames = new ArrayList<>(); + for (GoogleAppsCustomSchema customSchema : customSchemas) { + if (customSchema.getType().equals("object")) { + // parse inner schemas + String basicName = customSchema.getName(); + // manage only first level inner schemas + for (GoogleAppsCustomSchema innerSchema : customSchema.getInnerSchemas()) { + customSchemaNames.add(basicName + "." + innerSchema.getName()); + } + } else { + LOG.warn("CustomSchema type {0} not allowed at this level", customSchema.getType()); + } + } + return customSchemaNames; + } + + private static Attribute getKeyFromFilter(final ObjectClass objectClass, final Filter filter) { + Attribute key = null; + if (filter instanceof EqualsFilter) { + // Account, Group, OrgUnit object classes + Attribute filterAttr = ((EqualsFilter) filter).getAttribute(); + if (filterAttr instanceof Uid) { + key = filterAttr; + } else if (ObjectClass.ACCOUNT.equals(objectClass) || ObjectClass.GROUP.equals(objectClass) + && (filterAttr instanceof Name + || filterAttr.getName().equalsIgnoreCase(GoogleAppsUtil.ALIASES_ATTR))) { + key = filterAttr; + } else if (GoogleAppsUtil.ORG_UNIT.equals(objectClass) && filterAttr.getName().equalsIgnoreCase( + GoogleAppsUtil.ORG_UNIT_PATH_ATTR)) { + key = filterAttr; + } else if (ObjectClass.GROUP.equals(objectClass) && filterAttr.is(GoogleAppsUtil.EMAIL_ATTR)) { + key = filterAttr; + } + } else if (filter instanceof AndFilter) { + // Member object class + if (GoogleAppsUtil.MEMBER.equals(objectClass)) { + Attribute groupKey = null; + Attribute memberKey = null; + StringBuilder memberId = new StringBuilder(); + + Collection filters = ((AndFilter) filter).getFilters(); + for (Filter f : filters) { + if (f instanceof EqualsFilter) { + Attribute filterAttr = ((EqualsFilter) f).getAttribute(); + if (filterAttr.getName().equalsIgnoreCase(GoogleAppsUtil.GROUP_KEY_ATTR)) { + groupKey = filterAttr; + } else if (filterAttr.getName().equalsIgnoreCase(GoogleAppsUtil.EMAIL_ATTR) + || filterAttr.getName().equalsIgnoreCase(GoogleAppsUtil.ALIAS_ATTR) + || filterAttr instanceof Uid) { + memberKey = filterAttr; + } else { + throw new UnsupportedOperationException( + "Only AndFilter('groupKey','memberKey') is supported"); + } + } else { + throw new UnsupportedOperationException( + "Only AndFilter('groupKey','memberKey') is supported"); + } + } + if (memberKey != null && groupKey != null) { + memberId.append(groupKey.getValue().get(0)); + memberId.append("/"); + memberId.append(memberKey.getValue().get(0)); + key = new Uid(memberId.toString()); + } + } + } + return key; + } + + private final GoogleAppsConfiguration configuration; + + private final ObjectClass objectClass; + + private final Filter query; + + private final ResultsHandler handler; + + private final OperationOptions options; + + public GoogleAppsSearch( + final GoogleAppsConfiguration configuration, + final ObjectClass objectClass, + final Filter query, + final ResultsHandler handler, + final OperationOptions options) { + + this.configuration = configuration; + this.objectClass = objectClass; + this.query = query; + this.handler = handler; + this.options = options; + } + + protected String getFields(final OperationOptions options, final String... nameAttribute) { + if (null != options.getAttributesToGet()) { + Set attributes = CollectionUtil.newCaseInsensitiveSet(); + attributes.addAll(Arrays.asList(nameAttribute)); + final boolean notBlankCustomSchemas = StringUtil.isNotBlank(configuration.getCustomSchemasJSON()); + final List customSchemaNames = notBlankCustomSchemas + ? customSchemaNames(configuration.getCustomSchemasJSON()) + : new ArrayList<>(); + for (String attribute : options.getAttributesToGet()) { + if (AttributeUtil.namesEqual(GoogleAppsUtil.DESCRIPTION_ATTR, attribute)) { + attributes.add(GoogleAppsUtil.DESCRIPTION_ATTR); + } else if (AttributeUtil.isSpecialName(attribute)) { + // nothing to do + } else if (AttributeUtil.namesEqual(GoogleAppsUtil.FAMILY_NAME_ATTR, attribute)) { + attributes.add("name/familyName"); + } else if (AttributeUtil.namesEqual(GoogleAppsUtil.GIVEN_NAME_ATTR, attribute)) { + attributes.add("name/givenName"); + } else if (AttributeUtil.namesEqual(GoogleAppsUtil.FULL_NAME_ATTR, attribute)) { + attributes.add("name/fullName"); + } else if (!customSchemaNames.contains(attribute)) { + attributes.add(attribute); + } + // return also customSchemas according to configuration + if ("full".equals(configuration.getProjection()) && notBlankCustomSchemas) { + attributes.add(GoogleAppsUtil.CUSTOM_SCHEMAS); + } + } + return StringUtil.join(attributes, GoogleAppsUtil.COMMA); + } + return null; + } + + public void execute() { + final Set attributesToGet = getAttributesToGet(objectClass, options); + Attribute key = getKeyFromFilter(objectClass, query); + + if (ObjectClass.ACCOUNT.equals(objectClass)) { + if (null == key || null == key.getValue() || key.getValue().isEmpty() || null == key.getValue().get(0)) { + // Search request + try { + Directory.Users.List request = configuration.getDirectory().users().list(); + if (null != query) { + StringBuilder queryBuilder = query.accept(new UserHandler(), request); + if (null != queryBuilder) { + String queryString = queryBuilder.toString(); + LOG.ok("Executing Query: {0}", queryString); + request.setQuery(queryString); + } + if (null == request.getDomain() && null == request.getCustomer()) { + request.setCustomer(GoogleAppsUtil.MY_CUSTOMER_ID); + } + } else { + request.setCustomer(GoogleAppsUtil.MY_CUSTOMER_ID); + } + + // Implementation to support the 'OP_PAGE_SIZE' + boolean paged = false; + if (options.getPageSize() != null && 0 < options.getPageSize()) { + if (options.getPageSize() >= 1 && options.getPageSize() <= 500) { + request.setMaxResults(options.getPageSize()); + paged = true; + } else { + throw new IllegalArgumentException( + "Invalid pageSize value. Default is 100. Max allowed is 500 (integer, 1-500)"); + } + } + // Implementation to support the 'OP_PAGED_RESULTS_COOKIE' + request.setPageToken(options.getPagedResultsCookie()); + request.setProjection(configuration.getProjection()); + + // Implementation to support the 'OP_ATTRIBUTES_TO_GET' + String fields = getFields(options, GoogleAppsUtil.ID_ATTR, + GoogleAppsUtil.ETAG_ATTR, GoogleAppsUtil.PRIMARY_EMAIL_ATTR); + if (null != fields) { + request.setFields("nextPageToken,users(" + fields + ")"); + } + + if (options.getOptions().get(GoogleAppsUtil.SHOW_DELETED_PARAM) instanceof Boolean) { + request.setShowDeleted(options.getOptions().get(GoogleAppsUtil.SHOW_DELETED_PARAM).toString()); + } + + // Implementation to support the 'OP_SORT_KEYS' + if (null != options.getSortKeys()) { + for (SortKey sortKey : options.getSortKeys()) { + String orderBy; + if (sortKey.getField().equalsIgnoreCase(GoogleAppsUtil.EMAIL_ATTR) + || sortKey.getField().equalsIgnoreCase(GoogleAppsUtil.PRIMARY_EMAIL_ATTR) + || sortKey.getField().equalsIgnoreCase(GoogleAppsUtil.ALIASES_ATTR) + || sortKey.getField().equalsIgnoreCase(GoogleAppsUtil.ALIAS_ATTR)) { + orderBy = GoogleAppsUtil.EMAIL_ATTR; + } else if (sortKey.getField().equalsIgnoreCase(GoogleAppsUtil.GIVEN_NAME_ATTR)) { + orderBy = GoogleAppsUtil.GIVEN_NAME_ATTR; + } else if (sortKey.getField().equalsIgnoreCase(GoogleAppsUtil.FAMILY_NAME_ATTR)) { + orderBy = GoogleAppsUtil.FAMILY_NAME_ATTR; + } else { + LOG.ok("Unsupported SortKey:{0}", sortKey); + continue; + } + + request.setOrderBy(orderBy); + if (sortKey.isAscendingOrder()) { + request.setSortOrder(GoogleAppsUtil.ASCENDING_ORDER); + } else { + request.setSortOrder(GoogleAppsUtil.DESCENDING_ORDER); + } + break; + } + } + + String nextPageToken = null; + do { + nextPageToken = GoogleApiExecutor.execute(request, + new RequestResultHandler() { + + @Override + public String handleResult(final Directory.Users.List request, final Users value) { + if (null != value.getUsers()) { + for (User user : value.getUsers()) { + handler.handle(UserHandler.fromUser( + configuration, user, attributesToGet, configuration.getDirectory(). + groups())); + } + } + return value.getNextPageToken(); + } + }); + request.setPageToken(nextPageToken); + } while (!paged && StringUtil.isNotBlank(nextPageToken)); + + if (paged && StringUtil.isNotBlank(nextPageToken)) { + LOG.info("Paged Search was requested and next token is:{0}", nextPageToken); + ((SearchResultsHandler) handler).handleResult(new SearchResult(nextPageToken, 0)); + } + } catch (IOException e) { + LOG.warn(e, "Failed to initialize Groups#List"); + throw ConnectorException.wrap(e); + } + + } else { + // Read request + try { + Directory.Users.Get request = + configuration.getDirectory().users().get((String) key.getValue().get(0)); + request.setFields(getFields(options, GoogleAppsUtil.ID_ATTR, + GoogleAppsUtil.ETAG_ATTR, GoogleAppsUtil.PRIMARY_EMAIL_ATTR)); + request.setProjection(configuration.getProjection()); + + GoogleApiExecutor.execute(request, + new RequestResultHandler() { + + @Override + public Boolean handleResult(final Directory.Users.Get request, final User user) { + return handler.handle(UserHandler.fromUser( + configuration, user, attributesToGet, configuration.getDirectory().groups())); + } + + @Override + public Boolean handleNotFound(final IOException e) { + // Do nothing if not found + return true; + } + }); + } catch (IOException e) { + LOG.warn(e, "Failed to initialize Users#Get"); + throw ConnectorException.wrap(e); + } + } + } else if (ObjectClass.GROUP.equals(objectClass)) { + if (null == key) { + // Search request + try { + // userKey excludes the customer and domain!! + Directory.Groups.List request = configuration.getDirectory().groups().list(); + if (null != query) { + StringBuilder queryBuilder = query.accept(new GroupHandler(), request); + if (null != queryBuilder) { + String queryString = queryBuilder.toString(); + LOG.ok("Executing Query: {0}", queryString); + request.setQuery(queryString); + } + if (null == request.getDomain() && null == request.getCustomer()) { + request.setCustomer(GoogleAppsUtil.MY_CUSTOMER_ID); + } + } else { + request.setCustomer(GoogleAppsUtil.MY_CUSTOMER_ID); + } + + boolean paged = false; + // Groups + if (options.getPageSize() != null && 0 < options.getPageSize()) { + request.setMaxResults(options.getPageSize()); + paged = true; + } + request.setPageToken(options.getPagedResultsCookie()); + + // Implementation to support the 'OP_ATTRIBUTES_TO_GET' + String fields = getFields(options, GoogleAppsUtil.ID_ATTR, + GoogleAppsUtil.ETAG_ATTR, GoogleAppsUtil.EMAIL_ATTR); + if (null != fields) { + request.setFields("nextPageToken,groups(" + fields + ")"); + } + + String nextPageToken = null; + do { + nextPageToken = GoogleApiExecutor.execute(request, + new RequestResultHandler() { + + @Override + public String handleResult(final Directory.Groups.List request, final Groups value) { + if (null != value.getGroups()) { + for (Group group : value.getGroups()) { + handler.handle(GroupHandler.fromGroup( + group, attributesToGet, configuration.getDirectory().members())); + } + } + return value.getNextPageToken(); + } + }); + request.setPageToken(nextPageToken); + } while (!paged && StringUtil.isNotBlank(nextPageToken)); + + if (paged && StringUtil.isNotBlank(nextPageToken)) { + LOG.info("Paged Search was requested"); + ((SearchResultsHandler) handler).handleResult(new SearchResult(nextPageToken, 0)); + } + } catch (IOException e) { + LOG.warn(e, "Failed to initialize Groups#List"); + throw ConnectorException.wrap(e); + } + } else { + // Read request + try { + Directory.Groups.Get request = + configuration.getDirectory().groups().get((String) key.getValue().get(0)); + request.setFields(getFields(options, GoogleAppsUtil.ID_ATTR, + GoogleAppsUtil.ETAG_ATTR, GoogleAppsUtil.EMAIL_ATTR)); + + GoogleApiExecutor.execute(request, + new RequestResultHandler() { + + @Override + public Boolean handleResult(final Directory.Groups.Get request, final Group value) { + return handler.handle(GroupHandler.fromGroup( + value, attributesToGet, configuration.getDirectory().members())); + } + + @Override + public Boolean handleNotFound(final IOException e) { + // Do nothing if not found + return true; + } + }); + } catch (IOException e) { + LOG.warn(e, "Failed to initialize Groups#Get"); + throw ConnectorException.wrap(e); + } + } + } else if (GoogleAppsUtil.MEMBER.equals(objectClass)) { + if (null == key) { + // Search request + // TODO support AND role + try { + String groupKey = null; + + if (query instanceof EqualsFilter + && ((EqualsFilter) query).getAttribute().is(GoogleAppsUtil.GROUP_KEY_ATTR)) { + + groupKey = AttributeUtil.getStringValue(((AttributeFilter) query).getAttribute()); + } else { + throw new UnsupportedOperationException("Only EqualsFilter('groupKey') is supported"); + } + + if (StringUtil.isBlank(groupKey)) { + throw new InvalidAttributeValueException("The 'groupKey' can not be blank."); + } + Directory.Members.List request = configuration.getDirectory().members().list(groupKey); + + boolean paged = false; + // Groups + if (options.getPageSize() != null && 0 < options.getPageSize()) { + request.setMaxResults(options.getPageSize()); + paged = true; + } + request.setPageToken(options.getPagedResultsCookie()); + + String nextPageToken = null; + do { + nextPageToken = GoogleApiExecutor.execute(request, + new RequestResultHandler() { + + @Override + public String handleResult( + final Directory.Members.List request, + final Members value) { + if (null != value.getMembers()) { + for (Member group : value.getMembers()) { + handler.handle(MembersHandler.from(request.getGroupKey(), group)); + } + } + return value.getNextPageToken(); + } + }); + request.setPageToken(nextPageToken); + } while (!paged && StringUtil.isNotBlank(nextPageToken)); + + if (paged && StringUtil.isNotBlank(nextPageToken)) { + LOG.info("Paged Search was requested"); + ((SearchResultsHandler) handler).handleResult(new SearchResult(nextPageToken, 0)); + } + } catch (IOException e) { + LOG.warn(e, "Failed to initialize Groups#List"); + throw ConnectorException.wrap(e); + } + } else { + // Read request + try { + String[] ids = ((Uid) key).getUidValue().split("/"); + if (ids.length != 2) { + // TODO fix the exception + throw new InvalidAttributeValueException("Unrecognised UID format"); + } + + Directory.Members.Get request = configuration.getDirectory().members().get(ids[0], ids[1]); + GoogleApiExecutor.execute(request, + new RequestResultHandler() { + + @Override + public Boolean handleResult(final Directory.Members.Get request, final Member value) { + return handler.handle(MembersHandler.from(request.getGroupKey(), value)); + } + + @Override + public Boolean handleNotFound(final IOException e) { + // Do nothing if not found + return true; + } + }); + } catch (IOException e) { + LOG.warn(e, "Failed to initialize Groups#Get"); + throw ConnectorException.wrap(e); + } + } + } else if (GoogleAppsUtil.ORG_UNIT.equals(objectClass)) { + if (null == key) { + // Search request + try { + Directory.Orgunits.List request = configuration.getDirectory().orgunits(). + list(GoogleAppsUtil.MY_CUSTOMER_ID); + if (null != query) { + if (query instanceof StartsWithFilter + && AttributeUtil.namesEqual(GoogleAppsUtil.ORG_UNIT_PATH_ATTR, + ((StartsWithFilter) query).getName())) { + + request.setOrgUnitPath(((StartsWithFilter) query).getValue()); + } else { + throw new UnsupportedOperationException( + "Only StartsWithFilter('orgUnitPath') is supported"); + } + } else { + request.setOrgUnitPath("/"); + } + + String scope = options.getScope(); + if (OperationOptions.SCOPE_OBJECT.equalsIgnoreCase(scope) + || OperationOptions.SCOPE_ONE_LEVEL.equalsIgnoreCase(scope)) { + + request.setType("children"); + } else { + request.setType("all"); + } + + // Implementation to support the 'OP_ATTRIBUTES_TO_GET' + String fields = getFields( + options, + GoogleAppsUtil.ORG_UNIT_PATH_ATTR, + GoogleAppsUtil.ETAG_ATTR, + GoogleAppsUtil.NAME_ATTR); + if (null != fields) { + request.setFields("organizationUnits(" + fields + ")"); + } + + GoogleApiExecutor.execute(request, + new RequestResultHandler() { + + @Override + public Void handleResult(final Directory.Orgunits.List request, + final OrgUnits value) { + if (null != value.getOrganizationUnits()) { + for (OrgUnit group : value.getOrganizationUnits()) { + handler.handle(OrgunitsHandler.from(group, attributesToGet)); + } + } + return null; + } + }); + } catch (IOException e) { + LOG.warn(e, "Failed to initialize OrgUnits#List"); + throw ConnectorException.wrap(e); + } + } else { + // Read request + try { + Directory.Orgunits.Get request = configuration.getDirectory().orgunits(). + get(GoogleAppsUtil.MY_CUSTOMER_ID, (String) key.getValue().get(0)); + request.setFields(getFields(options, GoogleAppsUtil.ORG_UNIT_PATH_ATTR, + GoogleAppsUtil.ETAG_ATTR, GoogleAppsUtil.NAME_ATTR)); + + GoogleApiExecutor.execute(request, + new RequestResultHandler() { + + @Override + public Boolean handleResult(final Directory.Orgunits.Get request, final OrgUnit value) { + return handler.handle(OrgunitsHandler.from(value, attributesToGet)); + } + + @Override + public Boolean handleNotFound(final IOException e) { + // Do nothing if not found + return true; + } + }); + } catch (IOException e) { + LOG.warn(e, "Failed to initialize OrgUnits#Get"); + throw ConnectorException.wrap(e); + } + } + } else if (GoogleAppsUtil.LICENSE_ASSIGNMENT.equals(objectClass)) { + if (null == key) { + // Search request + try { + String productId = ""; + String skuId = ""; + + boolean paged = false; + + LicensingRequest request = null; + + if (StringUtil.isBlank(productId)) { + // TODO iterate over the three productids + throw new ConnectorException("productId is required"); + } else if (StringUtil.isBlank(skuId)) { + Licensing.LicenseAssignments.ListForProduct r = + configuration.getLicensing().licenseAssignments(). + listForProduct(productId, GoogleAppsUtil.MY_CUSTOMER_ID); + + if (options.getPageSize() != null && 0 < options.getPageSize()) { + r.setMaxResults(Long.valueOf(options.getPageSize())); + paged = true; + } + r.setPageToken(options.getPagedResultsCookie()); + request = r; + } else { + Licensing.LicenseAssignments.ListForProductAndSku r = + configuration.getLicensing().licenseAssignments(). + listForProductAndSku(productId, skuId, GoogleAppsUtil.MY_CUSTOMER_ID); + + if (options.getPageSize() != null && 0 < options.getPageSize()) { + r.setMaxResults(Long.valueOf(options.getPageSize())); + paged = true; + } + r.setPageToken(options.getPagedResultsCookie()); + request = r; + } + + String nextPageToken = null; + do { + nextPageToken = GoogleApiExecutor.execute(request, + new RequestResultHandler< + LicensingRequest, LicenseAssignmentList, String>() { + + @Override + public String handleResult( + final LicensingRequest request, + final LicenseAssignmentList value) { + + if (null != value.getItems()) { + for (LicenseAssignment resource : value.getItems()) { + handler.handle(LicenseAssignmentsHandler.from(resource)); + } + } + return value.getNextPageToken(); + } + }); + if (request instanceof Licensing.LicenseAssignments.ListForProduct) { + ((Licensing.LicenseAssignments.ListForProduct) request).setPageToken(nextPageToken); + } else { + ((Licensing.LicenseAssignments.ListForProductAndSku) request).setPageToken(nextPageToken); + } + } while (!paged && StringUtil.isNotBlank(nextPageToken)); + + if (paged && StringUtil.isNotBlank(nextPageToken)) { + LOG.info("Paged Search was requested"); + ((SearchResultsHandler) handler).handleResult(new SearchResult(nextPageToken, 0)); + } + + } catch (IOException e) { + LOG.warn(e, "Failed to initialize Groups#List"); + throw ConnectorException.wrap(e); + } + } else { + // Read request + try { + Matcher name = LicenseAssignmentsHandler.LICENSE_NAME_PATTERN.matcher(((Uid) key).getUidValue()); + if (!name.matches()) { + return; + } + + String productId = name.group(0); + String skuId = name.group(1); + String userId = name.group(2); + + Licensing.LicenseAssignments.Get request = + configuration.getLicensing().licenseAssignments().get(productId, skuId, userId); + + GoogleApiExecutor.execute(request, + new RequestResultHandler() { + + @Override + public Boolean handleResult( + final Licensing.LicenseAssignments.Get request, + final LicenseAssignment value) { + + return handler.handle(LicenseAssignmentsHandler.from(value)); + } + + @Override + public Boolean handleNotFound(final IOException e) { + // Do nothing if not found + return true; + } + }); + } catch (IOException e) { + LOG.warn(e, "Failed to initialize Groups#Get"); + throw ConnectorException.wrap(e); + } + } + } else { + LOG.warn("Search of type {0} is not supported", configuration.getConnectorMessages() + .format(objectClass.getDisplayNameKey(), objectClass.getObjectClassValue())); + throw new UnsupportedOperationException("Search of type" + + objectClass.getObjectClassValue() + " is not supported"); + } + } +} diff --git a/src/main/java/net/tirasa/connid/bundles/googleapps/GoogleAppsUpdate.java b/src/main/java/net/tirasa/connid/bundles/googleapps/GoogleAppsUpdate.java new file mode 100644 index 0000000..5e20d45 --- /dev/null +++ b/src/main/java/net/tirasa/connid/bundles/googleapps/GoogleAppsUpdate.java @@ -0,0 +1,620 @@ +/* + * ==================== + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright (c) 2024 ConnId. All Rights Reserved + * + * The contents of this file are subject to the terms of the Common Development + * and Distribution License("CDDL") (the "License"). You may not use this file + * except in compliance with the License. + * + * You can obtain a copy of the License at + * http://opensource.org/licenses/cddl1.php + * See the License for the specific language governing permissions and limitations + * under the License. + * + * When distributing the Covered Code, include this CDDL Header Notice in each file + * and include the License file at http://opensource.org/licenses/cddl1.php. + * If applicable, add the following below this CDDL Header, with the fields + * enclosed by brackets [] replaced by your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * ==================== + */ +package net.tirasa.connid.bundles.googleapps; + +import static net.tirasa.connid.bundles.googleapps.GoogleApiExecutor.execute; +import static net.tirasa.connid.bundles.googleapps.GroupHandler.listGroups; +import static net.tirasa.connid.bundles.googleapps.MembersHandler.listMembers; + +import com.google.api.services.directory.Directory; +import com.google.api.services.directory.model.Group; +import com.google.api.services.directory.model.Member; +import com.google.api.services.directory.model.OrgUnit; +import com.google.api.services.directory.model.User; +import com.google.api.services.licensing.Licensing; +import com.google.api.services.licensing.model.LicenseAssignment; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import org.identityconnectors.common.CollectionUtil; +import org.identityconnectors.common.StringUtil; +import org.identityconnectors.common.logging.Log; +import org.identityconnectors.framework.common.exceptions.ConnectorException; +import org.identityconnectors.framework.common.exceptions.InvalidAttributeValueException; +import org.identityconnectors.framework.common.exceptions.UnknownUidException; +import org.identityconnectors.framework.common.objects.Attribute; +import org.identityconnectors.framework.common.objects.AttributeDelta; +import org.identityconnectors.framework.common.objects.AttributeDeltaUtil; +import org.identityconnectors.framework.common.objects.AttributesAccessor; +import org.identityconnectors.framework.common.objects.ObjectClass; +import org.identityconnectors.framework.common.objects.OperationalAttributes; +import org.identityconnectors.framework.common.objects.PredefinedAttributes; +import org.identityconnectors.framework.common.objects.Uid; + +public class GoogleAppsUpdate { + + private static final Log LOG = Log.getLog(GoogleAppsUpdate.class); + + private final GoogleAppsConfiguration configuration; + + private final ObjectClass objectClass; + + private final Uid uid; + + public GoogleAppsUpdate( + final GoogleAppsConfiguration configuration, + final ObjectClass objectClass, + final Uid uid) { + + this.configuration = configuration; + this.objectClass = objectClass; + this.uid = uid; + } + + public Uid update(final Set replaceAttributes) { + final AttributesAccessor accessor = new AttributesAccessor(replaceAttributes); + + Uid uidAfterUpdate = uid; + if (ObjectClass.ACCOUNT.equals(objectClass)) { + Directory.Users.Patch patch = UserHandler.updateUser( + configuration.getDirectory().users(), + uid.getUidValue(), + accessor, + configuration.getCustomSchemasJSON()); + if (null != patch) { + uidAfterUpdate = execute(patch, new RequestResultHandler() { + + @Override + public Uid handleResult(final Directory.Users.Patch request, final User value) { + LOG.ok("User is Updated:{0}", value.getId()); + return new Uid(value.getId(), value.getEtag()); + } + }); + } + Attribute groups = accessor.find(PredefinedAttributes.GROUPS_NAME); + if (null != groups && null != groups.getValue()) { + final Directory.Members service = configuration.getDirectory().members(); + if (groups.getValue().isEmpty()) { + // Remove all membership + for (String groupKey : listGroups( + configuration.getDirectory().groups(), + uidAfterUpdate.getUidValue(), + configuration.getDomain())) { + + execute(MembersHandler.delete(service, groupKey, uidAfterUpdate.getUidValue()), + new RequestResultHandler() { + + @Override + public Object handleResult(final Directory.Members.Delete request, final Void value) { + return null; + } + + @Override + public Object handleNotFound(final IOException e) { + // It may be an indirect membership, not able to delete + return null; + } + }); + } + } else { + final Set activeGroups = listGroups( + configuration.getDirectory().groups(), + uidAfterUpdate.getUidValue(), + configuration.getDomain()); + + final List addGroups = new ArrayList<>(); + final Set keepGroups = CollectionUtil.newCaseInsensitiveSet(); + + for (Object member : groups.getValue()) { + if (member instanceof String) { + if (activeGroups.contains((String) member)) { + keepGroups.add((String) member); + } else { + String email = accessor.getName().getNameValue(); + addGroups.add(MembersHandler.create(service, (String) member, email, null)); + } + } else if (null != member) { + // throw error/revert? + throw new InvalidAttributeValueException("Attribute '__GROUPS__' must be a String list"); + } + } + + // Add new Member object + for (Directory.Members.Insert insert : addGroups) { + execute(insert, new RequestResultHandler() { + + @Override + public Object handleResult(final Directory.Members.Insert request, final Member value) { + return null; + } + + @Override + public Object handleDuplicate(final IOException e) { + // Do nothing + return null; + } + }); + } + + // Delete existing Member object + if (activeGroups.removeAll(keepGroups)) { + for (String groupKey : activeGroups) { + execute(MembersHandler.delete(service, groupKey, uidAfterUpdate.getUidValue()), + new RequestResultHandler() { + + @Override + public Object handleResult(final Directory.Members.Delete request, final Void value) { + return null; + } + + @Override + public Object handleNotFound(final IOException e) { + // It may be an indirect membership, not able to delete + return null; + } + }); + } + } + } + } + // GOOGLEAPPS-9 + // license management: if remove license param is true and __ENABLE__ is false perform delete license + // license read must be performed with the user primaryEmail, userId is not allowed + if (configuration.getRemoveLicenseOnDisable() + && accessor.hasAttribute(OperationalAttributes.ENABLE_NAME) + && !accessor.findBoolean(OperationalAttributes.ENABLE_NAME) + && StringUtil.isNotBlank(accessor.findString(GoogleAppsUtil.PRIMARY_EMAIL_ATTR))) { + for (String skuId : configuration.getSkuIds()) { + // 1. retrieve user license + try { + // use email as key + Licensing.LicenseAssignments.Get request = + configuration.getLicensing().licenseAssignments().get( + configuration.getProductId(), + skuId, + accessor.findString(GoogleAppsUtil.PRIMARY_EMAIL_ATTR)); + execute(request, + new RequestResultHandler< + Licensing.LicenseAssignments.Get, LicenseAssignment, Boolean>() { + + @Override + public Boolean handleResult( + final Licensing.LicenseAssignments.Get request, + final LicenseAssignment value) { + + try { + // 2. remove license + new GoogleAppsDelete( + configuration, + GoogleAppsUtil.LICENSE_ASSIGNMENT, + new Uid(GoogleAppsUtil.generateLicenseId( + value.getProductId(), + value.getSkuId(), + value.getUserId()))).execute(); + } catch (Exception e) { + LOG.error(e, "Failed to delete license for user {0}", value.getUserId()); + throw ConnectorException.wrap(e); + } + return true; + } + + @Override + public Boolean handleNotFound(final IOException e) { + // Do nothing if not found + return true; + } + }); + } catch (IOException e) { + LOG.error(e, "Unable to find license for {0}-{1}-{2}", configuration.getProductId(), skuId, + accessor.findString(GoogleAppsUtil.PRIMARY_EMAIL_ATTR)); + } + } + } + } else if (ObjectClass.GROUP.equals(objectClass)) { + final Directory.Groups.Patch patch = GroupHandler.update( + configuration.getDirectory().groups(), + uid.getUidValue(), + accessor); + if (null != patch) { + uidAfterUpdate = execute(patch, new RequestResultHandler() { + + @Override + public Uid handleResult(final Directory.Groups.Patch request, final Group value) { + LOG.ok("Group is Updated:{0}", value.getEmail()); + return new Uid(value.getEmail(), value.getEtag()); + } + }); + } + Attribute members = accessor.find(GoogleAppsUtil.MEMBERS_ATTR); + if (null != members && null != members.getValue()) { + final Directory.Members service = configuration.getDirectory().members(); + if (members.getValue().isEmpty()) { + // Remove all membership + for (Map member : listMembers(service, uidAfterUpdate.getUidValue(), null)) { + execute(MembersHandler.delete( + service, uidAfterUpdate.getUidValue(), member.get(GoogleAppsUtil.EMAIL_ATTR)), + new RequestResultHandler() { + + @Override + public Object handleResult(final Directory.Members.Delete request, final Void value) { + return null; + } + + @Override + public Object handleNotFound(final IOException e) { + // Do nothing + return null; + } + }); + } + } else { + final List> activeMembership = + listMembers(service, uidAfterUpdate.getUidValue(), null); + + final List addMembership = new ArrayList<>(); + final List patchMembership = new ArrayList<>(); + + for (Object member : members.getValue()) { + if (member instanceof Map) { + String email = (String) ((Map) member).get(GoogleAppsUtil.EMAIL_ATTR); + if (null == email) { + continue; + } + String role = (String) ((Map) member).get(GoogleAppsUtil.ROLE_ATTR); + if (null == role) { + role = "MEMBER"; + } + + boolean notMember = true; + for (Map a : activeMembership) { + // How to handle ROLE update? + // OWNER -> MANAGER -> MEMBER + if (email.equalsIgnoreCase(a.get(GoogleAppsUtil.EMAIL_ATTR))) { + a.put("keep", null); + if (!role.equalsIgnoreCase(a.get(GoogleAppsUtil.ROLE_ATTR))) { + patchMembership.add(MembersHandler.update( + service, uidAfterUpdate.getUidValue(), email, role)); + } + notMember = false; + break; + } + } + if (notMember) { + addMembership.add(MembersHandler.create( + service, uidAfterUpdate.getUidValue(), email, role)); + } + } else if (null != member) { + // throw error/revert? + throw new InvalidAttributeValueException( + "Attribute 'members' must be a Map list"); + } + } + + // Add new Member object + for (Directory.Members.Insert insert : addMembership) { + execute(insert, new RequestResultHandler() { + + @Override + public Object handleResult(final Directory.Members.Insert request, final Member value) { + return null; + } + + @Override + public Object handleDuplicate(final IOException e) { + // Do nothing + return null; + } + }); + } + + // Update existing Member object + for (Directory.Members.Patch request : patchMembership) { + execute(request, new RequestResultHandler() { + + @Override + public Object handleResult(final Directory.Members.Patch request, final Member value) { + return null; + } + }); + } + + // Delete existing Member object + for (Map am : activeMembership) { + if (!am.containsKey("keep")) { + execute(MembersHandler.delete( + service, uidAfterUpdate.getUidValue(), am.get(GoogleAppsUtil.EMAIL_ATTR)), + new RequestResultHandler() { + + @Override + public Object handleResult(final Directory.Members.Delete request, final Void value) { + return null; + } + + @Override + public Object handleNotFound(final IOException e) { + // Do nothing + return null; + } + }); + } + } + } + } + } else if (GoogleAppsUtil.MEMBER.equals(objectClass)) { + String role = accessor.findString(GoogleAppsUtil.ROLE_ATTR); + if (StringUtil.isNotBlank(role)) { + String[] ids = uid.getUidValue().split("/"); + if (ids.length == 2) { + final Directory.Members.Patch patch = MembersHandler.update( + configuration.getDirectory().members(), ids[0], ids[1], role). + setFields(GoogleAppsUtil.EMAIL_ETAG); + uidAfterUpdate = execute(patch, new RequestResultHandler() { + + @Override + public Uid handleResult(final Directory.Members.Patch request, final Member value) { + LOG.ok("Member is updated:{0}/{1}", request.getGroupKey(), value.getEmail()); + return MembersHandler.generateUid(request.getGroupKey(), value); + } + }); + } else { + throw new UnknownUidException("Invalid ID format"); + } + } + } else if (GoogleAppsUtil.ORG_UNIT.equals(objectClass)) { + final Directory.Orgunits.Patch patch = OrgunitsHandler.update( + configuration.getDirectory().orgunits(), uid.getUidValue(), accessor); + if (null != patch) { + uidAfterUpdate = execute(patch, new RequestResultHandler() { + + @Override + public Uid handleResult(final Directory.Orgunits.Patch request, final OrgUnit value) { + LOG.ok("OrgUnit is updated:{0}", value.getName()); + return OrgunitsHandler.generateUid(value); + } + }); + } + } else if (GoogleAppsUtil.LICENSE_ASSIGNMENT.equals(objectClass)) { + final Licensing.LicenseAssignments.Patch patch = LicenseAssignmentsHandler.update( + configuration.getLicensing().licenseAssignments(), + uid.getUidValue(), + accessor); + if (null != patch) { + uidAfterUpdate = execute(patch, + new RequestResultHandler() { + + @Override + public Uid handleResult( + final Licensing.LicenseAssignments.Patch request, + final LicenseAssignment value) { + + LOG.ok("LicenseAssignment is Updated:{0}/{1}/{2}", + value.getProductId(), value.getSkuId(), value.getUserId()); + return LicenseAssignmentsHandler.generateUid(value); + } + }); + } + } else { + LOG.warn("Update of type {0} is not supported", configuration.getConnectorMessages() + .format(objectClass.getDisplayNameKey(), objectClass.getObjectClassValue())); + throw new UnsupportedOperationException("Update of type" + + objectClass.getObjectClassValue() + " is not supported"); + } + return uidAfterUpdate; + } + + public Set updateDelta(final Set modifications) { + if (ObjectClass.ACCOUNT.equals(objectClass)) { + Directory.Users.Update update = UserHandler.updateUser( + configuration.getDirectory().users(), + uid.getUidValue(), + modifications, + configuration.getCustomSchemasJSON()); + if (null != update) { + execute(update, new RequestResultHandler() { + + @Override + public Uid handleResult(final Directory.Users.Update request, final User value) { + LOG.ok("User is Updated:{0}", value.getId()); + return uid; + } + }); + } + + Set groupsToAdd = new HashSet<>(); + Set groupsToRemove = new HashSet<>(); + Optional.ofNullable(AttributeDeltaUtil.find(PredefinedAttributes.GROUPS_NAME, modifications)). + ifPresent(groups -> { + if (CollectionUtil.isEmpty(groups.getValuesToReplace())) { + if (!CollectionUtil.isEmpty(groups.getValuesToAdd())) { + for (Object group : CollectionUtil.nullAsEmpty(groups.getValuesToAdd())) { + groupsToAdd.add(group.toString()); + } + } + + if (!CollectionUtil.isEmpty(groups.getValuesToRemove())) { + for (Object group : CollectionUtil.nullAsEmpty(groups.getValuesToRemove())) { + groupsToRemove.add(group.toString()); + } + } + } else { + for (String groupKey : listGroups( + configuration.getDirectory().groups(), + uid.getUidValue(), + configuration.getDomain())) { + + groupsToRemove.add(groupKey); + } + + if (!CollectionUtil.isEmpty(groups.getValuesToAdd())) { + for (Object group : CollectionUtil.nullAsEmpty(groups.getValuesToReplace())) { + groupsToAdd.add(group.toString()); + } + } + } + }); + + final Directory.Members service = configuration.getDirectory().members(); + // Delete existing Member object + for (String groupKey : groupsToRemove) { + execute(MembersHandler.delete(service, groupKey, uid.getUidValue()), + new RequestResultHandler() { + + @Override + public Object handleResult(final Directory.Members.Delete request, final Void value) { + return null; + } + + @Override + public Object handleNotFound(final IOException e) { + // It may be an indirect membership, not able to delete + return null; + } + }); + } + // Add new Member object + for (String groupKey : groupsToAdd) { + execute(MembersHandler.create(service, groupKey, uid, null), + new RequestResultHandler() { + + @Override + public Object handleResult(final Directory.Members.Insert request, final Member value) { + return null; + } + + @Override + public Object handleDuplicate(final IOException e) { + // Do nothing + return null; + } + }); + } + } else if (ObjectClass.GROUP.equals(objectClass)) { + final Directory.Groups.Update update = GroupHandler.update( + configuration.getDirectory().groups(), uid.getUidValue(), modifications); + if (null != update) { + execute(update, new RequestResultHandler() { + + @Override + public Uid handleResult(final Directory.Groups.Update request, final Group value) { + LOG.ok("Group is Updated:{0}", value.getEmail()); + return uid; + } + }); + } + + final Directory.Members service = configuration.getDirectory().members(); + + Set membersToAdd = new HashSet<>(); + Set membersToRemove = new HashSet<>(); + Optional.ofNullable(AttributeDeltaUtil.find(GoogleAppsUtil.MEMBERS_ATTR, modifications)). + ifPresent(members -> { + if (CollectionUtil.isEmpty(members.getValuesToReplace())) { + if (!CollectionUtil.isEmpty(members.getValuesToAdd())) { + for (Object group : CollectionUtil.nullAsEmpty(members.getValuesToAdd())) { + membersToAdd.add(group.toString()); + } + } + + if (!CollectionUtil.isEmpty(members.getValuesToRemove())) { + for (Object group : CollectionUtil.nullAsEmpty(members.getValuesToRemove())) { + membersToRemove.add(group.toString()); + } + } + } else { + for (Map member : listMembers( + service, + uid.getUidValue(), + null)) { + + membersToRemove.add(member.get(GoogleAppsUtil.EMAIL_ATTR)); + } + + if (!CollectionUtil.isEmpty(members.getValuesToAdd())) { + for (Object group : CollectionUtil.nullAsEmpty(members.getValuesToReplace())) { + membersToAdd.add(group.toString()); + } + } + } + }); + + // Remove all membership + for (String member : membersToRemove) { + execute(MembersHandler.delete(service, uid.getUidValue(), member), + new RequestResultHandler() { + + @Override + public Object handleResult(final Directory.Members.Delete request, final Void value) { + return null; + } + + @Override + public Object handleNotFound(final IOException e) { + // Do nothing + return null; + } + }); + } + // Add new Member object + for (String member : membersToAdd) { + execute(MembersHandler.create(service, uid.getUidValue(), member, null), + new RequestResultHandler() { + + @Override + public Object handleResult(final Directory.Members.Insert request, final Member value) { + return null; + } + + @Override + public Object handleDuplicate(final IOException e) { + // Do nothing + return null; + } + }); + } + } else if (GoogleAppsUtil.ORG_UNIT.equals(objectClass)) { + final Directory.Orgunits.Update update = OrgunitsHandler.update( + configuration.getDirectory().orgunits(), uid.getUidValue(), modifications); + if (null != update) { + execute(update, new RequestResultHandler() { + + @Override + public Uid handleResult(final Directory.Orgunits.Update request, final OrgUnit value) { + LOG.ok("OrgUnit is updated:{0}", value.getName()); + return uid; + } + }); + } + } else { + LOG.warn("Update delta of type {0} is not supported", configuration.getConnectorMessages() + .format(objectClass.getDisplayNameKey(), objectClass.getObjectClassValue())); + throw new UnsupportedOperationException("Update delta of type" + + objectClass.getObjectClassValue() + " is not supported"); + } + + return modifications; + } +} diff --git a/src/main/java/net/tirasa/connid/bundles/googleapps/GoogleAppsUtil.java b/src/main/java/net/tirasa/connid/bundles/googleapps/GoogleAppsUtil.java index 29bb839..1d03397 100644 --- a/src/main/java/net/tirasa/connid/bundles/googleapps/GoogleAppsUtil.java +++ b/src/main/java/net/tirasa/connid/bundles/googleapps/GoogleAppsUtil.java @@ -31,10 +31,13 @@ import java.io.IOException; import java.util.List; import java.util.Optional; +import org.identityconnectors.common.CollectionUtil; import org.identityconnectors.common.StringUtil; import org.identityconnectors.common.logging.Log; import org.identityconnectors.framework.common.exceptions.InvalidAttributeValueException; import org.identityconnectors.framework.common.objects.Attribute; +import org.identityconnectors.framework.common.objects.AttributeDelta; +import org.identityconnectors.framework.common.objects.AttributeDeltaUtil; import org.identityconnectors.framework.common.objects.AttributeUtil; import org.identityconnectors.framework.common.objects.Name; import org.identityconnectors.framework.common.objects.ObjectClass; @@ -229,6 +232,34 @@ public static Optional getBooleanValue(final Attribute source) { return Optional.empty(); } + public static Optional getStringValue(final AttributeDelta source) { + Object value = Optional.ofNullable(AttributeDeltaUtil.getSingleValue(source)). + orElseGet(() -> CollectionUtil.isEmpty(source.getValuesToAdd()) + ? null : source.getValuesToAdd().get(0)); + if (value instanceof String) { + return Optional.of((String) value); + } else if (null != value) { + throw new InvalidAttributeValueException("The " + source.getName() + + " attribute is not String value attribute. It has value with type " + + value.getClass().getSimpleName()); + } + return Optional.empty(); + } + + public static Optional getBooleanValue(final AttributeDelta source) { + Object value = Optional.ofNullable(AttributeDeltaUtil.getSingleValue(source)). + orElseGet(() -> CollectionUtil.isEmpty(source.getValuesToAdd()) + ? null : source.getValuesToAdd().get(0)); + if (value instanceof Boolean) { + return Optional.of((Boolean) value); + } else if (null != value) { + throw new InvalidAttributeValueException("The " + source.getName() + + " attribute is not Boolean value attribute. It has value with type " + + value.getClass().getSimpleName()); + } + return Optional.empty(); + } + public static String generateLicenseId(final String productId, final String skuId, final String userId) { if (StringUtil.isBlank(productId) || StringUtil.isBlank(skuId) || StringUtil.isBlank(userId)) { throw new IllegalArgumentException("productId, skuId and/or userId value(s) are not valid"); diff --git a/src/main/java/net/tirasa/connid/bundles/googleapps/GroupHandler.java b/src/main/java/net/tirasa/connid/bundles/googleapps/GroupHandler.java index b99c728..abb8de8 100644 --- a/src/main/java/net/tirasa/connid/bundles/googleapps/GroupHandler.java +++ b/src/main/java/net/tirasa/connid/bundles/googleapps/GroupHandler.java @@ -23,27 +23,38 @@ */ package net.tirasa.connid.bundles.googleapps; -import com.google.api.services.admin.directory.Directory; -import com.google.api.services.admin.directory.model.Group; +import static net.tirasa.connid.bundles.googleapps.GoogleApiExecutor.execute; + +import com.google.api.services.directory.Directory; +import com.google.api.services.directory.model.Group; +import com.google.api.services.directory.model.Groups; import com.google.common.base.CharMatcher; import com.google.common.escape.Escaper; import com.google.common.escape.Escapers; import java.io.IOException; import java.util.Optional; +import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; +import org.identityconnectors.common.CollectionUtil; import org.identityconnectors.common.StringUtil; import org.identityconnectors.common.logging.Log; import org.identityconnectors.framework.common.exceptions.ConnectorException; import org.identityconnectors.framework.common.exceptions.InvalidAttributeValueException; import org.identityconnectors.framework.common.objects.Attribute; +import org.identityconnectors.framework.common.objects.AttributeBuilder; +import org.identityconnectors.framework.common.objects.AttributeDelta; +import org.identityconnectors.framework.common.objects.AttributeDeltaUtil; import org.identityconnectors.framework.common.objects.AttributeInfoBuilder; import org.identityconnectors.framework.common.objects.AttributeUtil; import org.identityconnectors.framework.common.objects.AttributesAccessor; +import org.identityconnectors.framework.common.objects.ConnectorObject; +import org.identityconnectors.framework.common.objects.ConnectorObjectBuilder; import org.identityconnectors.framework.common.objects.Name; import org.identityconnectors.framework.common.objects.ObjectClass; import org.identityconnectors.framework.common.objects.ObjectClassInfo; import org.identityconnectors.framework.common.objects.ObjectClassInfoBuilder; +import org.identityconnectors.framework.common.objects.Uid; import org.identityconnectors.framework.common.objects.filter.AndFilter; import org.identityconnectors.framework.common.objects.filter.ContainsAllValuesFilter; import org.identityconnectors.framework.common.objects.filter.ContainsFilter; @@ -284,6 +295,7 @@ public static ObjectClassInfo getObjectClassInfo() { builder.setType(ObjectClass.GROUP_NAME); // email builder.addAttributeInfo(Name.INFO); + builder.addAttributeInfo(AttributeInfoBuilder.build(GoogleAppsUtil.ID_ATTR)); builder.addAttributeInfo(AttributeInfoBuilder.build(GoogleAppsUtil.NAME_ATTR)); builder.addAttributeInfo(AttributeInfoBuilder.build(GoogleAppsUtil.DESCRIPTION_ATTR)); @@ -316,7 +328,6 @@ public static Directory.Groups.Insert create( try { return service.insert(group).setFields(GoogleAppsUtil.ID_EMAIL_ETAG); - // } catch (HttpResponseException e){ } catch (IOException e) { LOG.warn(e, "Failed to initialize Groups#Insert"); throw ConnectorException.wrap(e); @@ -342,11 +353,11 @@ public static Directory.Groups.Patch update( .ifPresent(email -> set(content, g -> g.setEmail(email.getNameValue()))); Optional.ofNullable(attributes.find(GoogleAppsUtil.NAME_ATTR)) - .flatMap(name -> GoogleAppsUtil.getStringValue(name)) + .flatMap(GoogleAppsUtil::getStringValue) .ifPresent(stringValue -> set(content, g -> g.setName(stringValue))); Optional.ofNullable(attributes.find(GoogleAppsUtil.DESCRIPTION_ATTR)) - .flatMap(description -> GoogleAppsUtil.getStringValue(description)) + .flatMap(GoogleAppsUtil::getStringValue) .ifPresent(stringValue -> set(content, g -> g.setDescription(stringValue))); if (null == content.get()) { @@ -354,10 +365,124 @@ public static Directory.Groups.Patch update( } try { return service.patch(groupKey, content.get()).setFields(GoogleAppsUtil.ID_EMAIL_ETAG); - // } catch (HttpResponseException e){ } catch (IOException e) { LOG.warn(e, "Failed to initialize Groups#Patch"); throw ConnectorException.wrap(e); } } + + public static Directory.Groups.Update update( + final Directory.Groups service, + final String groupKey, + final Set modifications) { + + if (AttributeDeltaUtil.getUidAttributeDelta(modifications) != null + || AttributeDeltaUtil.getAttributeDeltaForName(modifications) != null) { + + throw new IllegalArgumentException("Do not perform rename via updateDelta, use standard update"); + } + + AtomicReference content = new AtomicReference<>(); + + Optional.ofNullable(AttributeDeltaUtil.find(GoogleAppsUtil.NAME_ATTR, modifications)) + .flatMap(GoogleAppsUtil::getStringValue) + .ifPresent(stringValue -> set(content, g -> g.setName(stringValue))); + + Optional.ofNullable(AttributeDeltaUtil.find(GoogleAppsUtil.DESCRIPTION_ATTR, modifications)) + .flatMap(GoogleAppsUtil::getStringValue) + .ifPresent(stringValue -> set(content, g -> g.setDescription(stringValue))); + + if (null == content.get()) { + return null; + } + try { + return service.update(groupKey, content.get()).setFields(GoogleAppsUtil.ID_EMAIL_ETAG); + } catch (IOException e) { + LOG.warn(e, "Failed to initialize Groups#update"); + throw ConnectorException.wrap(e); + } + } + + public static Set listGroups(final Directory.Groups service, final String userKey, final String domain) { + final Set result = CollectionUtil.newCaseInsensitiveSet(); + try { + Directory.Groups.List request = service.list(); + request.setUserKey(userKey); + request.setFields("groups/email"); + // 400 Bad Request if the Customer(my_customer or exact value) is set, only domain-userKey combination + // allowed. request.setCustomer(MY_CUSTOMER_ID); + request.setDomain(domain); + + String nextPageToken; + do { + nextPageToken = execute(request, new RequestResultHandler() { + + @Override + public String handleResult(final Directory.Groups.List request, final Groups value) { + if (null != value.getGroups()) { + for (Group group : value.getGroups()) { + result.add(group.getEmail()); + } + } + return value.getNextPageToken(); + } + }); + } while (StringUtil.isNotBlank(nextPageToken)); + } catch (IOException e) { + LOG.warn(e, "Failed to initialize Members#Delete"); + throw ConnectorException.wrap(e); + } + return result; + } + + public static ConnectorObject fromGroup( + final Group group, + final Set attributesToGet, + final Directory.Members service) { + + ConnectorObjectBuilder builder = new ConnectorObjectBuilder(); + builder.setObjectClass(ObjectClass.GROUP); + + if (null != group.getEtag()) { + builder.setUid(new Uid(group.getEmail(), group.getEtag())); + } else { + builder.setUid(group.getEmail()); + } + builder.setName(group.getEmail()); + + // Optional + if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.NAME_ATTR)) { + builder.addAttribute(AttributeBuilder.build(GoogleAppsUtil.NAME_ATTR, group.getName())); + } + if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.EMAIL_ATTR)) { + builder.addAttribute(AttributeBuilder.build(GoogleAppsUtil.EMAIL_ATTR, group.getEmail())); + } + if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.DESCRIPTION_ATTR)) { + builder.addAttribute(AttributeBuilder.build(GoogleAppsUtil.DESCRIPTION_ATTR, group.getDescription())); + } + + if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.ADMIN_CREATED_ATTR)) { + builder.addAttribute(AttributeBuilder.build(GoogleAppsUtil.ADMIN_CREATED_ATTR, group.getAdminCreated())); + } + if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.ALIASES_ATTR)) { + builder.addAttribute(AttributeBuilder.build(GoogleAppsUtil.ALIASES_ATTR, group.getAliases())); + } + if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.NON_EDITABLE_ALIASES_ATTR)) { + builder.addAttribute(AttributeBuilder.build( + GoogleAppsUtil.NON_EDITABLE_ALIASES_ATTR, group.getNonEditableAliases())); + } + if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.DIRECT_MEMBERS_COUNT_ATTR)) { + builder.addAttribute(AttributeBuilder.build( + GoogleAppsUtil.DIRECT_MEMBERS_COUNT_ATTR, group.getDirectMembersCount())); + } + + // Expensive to get + if (null != attributesToGet && attributesToGet.contains(GoogleAppsUtil.MEMBERS_ATTR)) { + builder.addAttribute(AttributeBuilder.build( + GoogleAppsUtil.MEMBERS_ATTR, MembersHandler.listMembers(service, group.getId(), null))); + } + + return builder.build(); + } + } diff --git a/src/main/java/net/tirasa/connid/bundles/googleapps/LicenseAssignmentsHandler.java b/src/main/java/net/tirasa/connid/bundles/googleapps/LicenseAssignmentsHandler.java index 162ae37..c3571b2 100644 --- a/src/main/java/net/tirasa/connid/bundles/googleapps/LicenseAssignmentsHandler.java +++ b/src/main/java/net/tirasa/connid/bundles/googleapps/LicenseAssignmentsHandler.java @@ -200,7 +200,6 @@ public static Licensing.LicenseAssignments.Delete delete( try { return service.delete(productId, skuId, userId); - // } catch (HttpResponseException e){ } catch (IOException e) { LOG.warn(e, "Failed to initialize LicenseAssignments#Delete"); throw ConnectorException.wrap(e); diff --git a/src/main/java/net/tirasa/connid/bundles/googleapps/MembersHandler.java b/src/main/java/net/tirasa/connid/bundles/googleapps/MembersHandler.java index 8292292..c80110c 100644 --- a/src/main/java/net/tirasa/connid/bundles/googleapps/MembersHandler.java +++ b/src/main/java/net/tirasa/connid/bundles/googleapps/MembersHandler.java @@ -22,9 +22,16 @@ */ package net.tirasa.connid.bundles.googleapps; -import com.google.api.services.admin.directory.Directory; -import com.google.api.services.admin.directory.model.Member; +import static net.tirasa.connid.bundles.googleapps.GoogleApiExecutor.execute; + +import com.google.api.services.directory.Directory; +import com.google.api.services.directory.model.Member; +import com.google.api.services.directory.model.Members; import java.io.IOException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; import org.identityconnectors.common.StringUtil; import org.identityconnectors.common.logging.Log; import org.identityconnectors.framework.common.exceptions.ConnectorException; @@ -90,10 +97,29 @@ public static Directory.Members.Insert create( } public static Directory.Members.Insert create( - final Directory.Members service, final String groupKey, final String memberKey, final String role) { + final Directory.Members service, final String groupKey, final Uid uid, final String role) { Member content = new Member(); - content.setEmail(memberKey); + content.setId(uid.getUidValue()); + if (StringUtil.isBlank(role)) { + content.setRole("MEMBER"); + } else { + // OWNER. MANAGER. MEMBER. + content.setRole(role); + } + try { + return service.insert(groupKey, content).setFields(GoogleAppsUtil.EMAIL_ETAG); + } catch (IOException e) { + LOG.warn(e, "Failed to initialize Members#Insert"); + throw ConnectorException.wrap(e); + } + } + + public static Directory.Members.Insert create( + final Directory.Members service, final String groupKey, final String email, final String role) { + + Member content = new Member(); + content.setEmail(email); if (StringUtil.isBlank(role)) { content.setRole("MEMBER"); } else { @@ -167,6 +193,39 @@ public static Uid generateUid(final String groupKey, final Member content) { return uid; } + public static List> listMembers( + final Directory.Members service, final String groupKey, final String roles) { + + final List> result = new ArrayList<>(); + try { + Directory.Members.List request = service.list(groupKey); + request.setRoles(StringUtil.isBlank(roles) ? "OWNER,MANAGER,MEMBER" : roles); + + String nextPageToken; + do { + nextPageToken = execute(request, new RequestResultHandler() { + + @Override + public String handleResult(final Directory.Members.List request, final Members value) { + if (null != value.getMembers()) { + for (Member member : value.getMembers()) { + Map m = new LinkedHashMap<>(2); + m.put(GoogleAppsUtil.EMAIL_ATTR, member.getEmail()); + m.put(GoogleAppsUtil.ROLE_ATTR, member.getRole()); + result.add(m); + } + } + return value.getNextPageToken(); + } + }); + } while (StringUtil.isNotBlank(nextPageToken)); + } catch (IOException e) { + LOG.warn(e, "Failed to initialize Members#Delete"); + throw ConnectorException.wrap(e); + } + return result; + } + private MembersHandler() { // private constructor for static utility class } diff --git a/src/main/java/net/tirasa/connid/bundles/googleapps/OrgunitsHandler.java b/src/main/java/net/tirasa/connid/bundles/googleapps/OrgunitsHandler.java index 8f4e39d..5318fb8 100644 --- a/src/main/java/net/tirasa/connid/bundles/googleapps/OrgunitsHandler.java +++ b/src/main/java/net/tirasa/connid/bundles/googleapps/OrgunitsHandler.java @@ -23,19 +23,20 @@ */ package net.tirasa.connid.bundles.googleapps; -import com.google.api.services.admin.directory.Directory; -import com.google.api.services.admin.directory.model.OrgUnit; +import com.google.api.services.directory.Directory; +import com.google.api.services.directory.model.OrgUnit; import java.io.IOException; import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; -import org.identityconnectors.common.CollectionUtil; import org.identityconnectors.common.StringUtil; import org.identityconnectors.common.logging.Log; import org.identityconnectors.framework.common.exceptions.ConnectorException; import org.identityconnectors.framework.common.exceptions.InvalidAttributeValueException; import org.identityconnectors.framework.common.objects.AttributeBuilder; +import org.identityconnectors.framework.common.objects.AttributeDelta; +import org.identityconnectors.framework.common.objects.AttributeDeltaUtil; import org.identityconnectors.framework.common.objects.AttributeInfoBuilder; import org.identityconnectors.framework.common.objects.AttributesAccessor; import org.identityconnectors.framework.common.objects.ConnectorObject; @@ -81,7 +82,6 @@ public static ObjectClassInfo getObjectClassInfo() { ObjectClassInfoBuilder builder = new ObjectClassInfoBuilder(); builder.setType(GoogleAppsUtil.ORG_UNIT.getObjectClassValue()); builder.setContainer(true); - // primaryEmail builder.addAttributeInfo(Name.INFO); // parentOrgUnitPath builder.addAttributeInfo(AttributeInfoBuilder.define(GoogleAppsUtil.PARENT_ORG_UNIT_PATH_ATTR). @@ -170,7 +170,7 @@ public static Directory.Orgunits.Patch update( .ifPresent(stringValue -> set(content, o -> o.setDescription(stringValue))); Optional.ofNullable(attributes.findBoolean(GoogleAppsUtil.BLOCK_INHERITANCE_ATTR)) - .ifPresent(blockInheritance -> set(content, u -> u.setBlockInheritance(!blockInheritance))); + .ifPresent(blockInheritance -> set(content, o -> o.setBlockInheritance(!blockInheritance))); if (null == content.get()) { return null; @@ -179,7 +179,7 @@ public static Directory.Orgunits.Patch update( // Full path of the organization unit return service.patch( GoogleAppsUtil.MY_CUSTOMER_ID, - CollectionUtil.newList(orgUnitPath), + orgUnitPath, content.get()).setFields(GoogleAppsUtil.ORG_UNIT_PATH_ETAG); } catch (IOException e) { LOG.warn(e, "Failed to initialize Orgunits#Patch"); @@ -187,6 +187,45 @@ public static Directory.Orgunits.Patch update( } } + public static Directory.Orgunits.Update update( + final Directory.Orgunits service, + final String orgUnitPath, + final Set modifications) { + + AtomicReference content = new AtomicReference<>(); + + Optional.ofNullable(AttributeDeltaUtil.getAttributeDeltaForName(modifications)) + .map(AttributeDeltaUtil::getStringValue) + .flatMap(name -> getOrgUnitNameFromPath(new Name(name))) + .ifPresent(name -> set(content, o -> o.setName(name))); + + Optional.ofNullable(AttributeDeltaUtil.find(GoogleAppsUtil.PARENT_ORG_UNIT_PATH_ATTR, modifications)) + .flatMap(GoogleAppsUtil::getStringValue) + .ifPresent(stringValue -> set(content, o -> o.setParentOrgUnitPath(stringValue))); + + Optional.ofNullable(AttributeDeltaUtil.find(GoogleAppsUtil.DESCRIPTION_ATTR, modifications)) + .flatMap(GoogleAppsUtil::getStringValue) + .ifPresent(stringValue -> set(content, o -> o.setDescription(stringValue))); + + Optional.ofNullable(AttributeDeltaUtil.find(GoogleAppsUtil.BLOCK_INHERITANCE_ATTR, modifications)) + .flatMap(GoogleAppsUtil::getBooleanValue) + .ifPresent(blockInheritance -> set(content, o -> o.setBlockInheritance(!blockInheritance))); + + if (null == content.get()) { + return null; + } + try { + // Full path of the organization unit + return service.update( + GoogleAppsUtil.MY_CUSTOMER_ID, + orgUnitPath, + content.get()).setFields(GoogleAppsUtil.ORG_UNIT_PATH_ETAG); + } catch (IOException e) { + LOG.warn(e, "Failed to initialize Orgunits#update"); + throw ConnectorException.wrap(e); + } + } + public static ConnectorObject from(final OrgUnit content, final Set attributesToGet) { ConnectorObjectBuilder builder = new ConnectorObjectBuilder(); builder.setObjectClass(GoogleAppsUtil.ORG_UNIT); diff --git a/src/main/java/net/tirasa/connid/bundles/googleapps/UserHandler.java b/src/main/java/net/tirasa/connid/bundles/googleapps/UserHandler.java index e55c7c9..1dbaad7 100644 --- a/src/main/java/net/tirasa/connid/bundles/googleapps/UserHandler.java +++ b/src/main/java/net/tirasa/connid/bundles/googleapps/UserHandler.java @@ -24,23 +24,23 @@ package net.tirasa.connid.bundles.googleapps; import com.google.api.client.util.GenericData; -import com.google.api.services.admin.directory.Directory; -import com.google.api.services.admin.directory.model.Alias; -import com.google.api.services.admin.directory.model.User; -import com.google.api.services.admin.directory.model.UserAddress; -import com.google.api.services.admin.directory.model.UserEmail; -import com.google.api.services.admin.directory.model.UserExternalId; -import com.google.api.services.admin.directory.model.UserIm; -import com.google.api.services.admin.directory.model.UserName; -import com.google.api.services.admin.directory.model.UserOrganization; -import com.google.api.services.admin.directory.model.UserPhone; -import com.google.api.services.admin.directory.model.UserPhoto; -import com.google.api.services.admin.directory.model.UserRelation; +import com.google.api.services.directory.Directory; +import com.google.api.services.directory.model.Alias; +import com.google.api.services.directory.model.User; +import com.google.api.services.directory.model.UserAddress; +import com.google.api.services.directory.model.UserExternalId; +import com.google.api.services.directory.model.UserIm; +import com.google.api.services.directory.model.UserName; +import com.google.api.services.directory.model.UserOrganization; +import com.google.api.services.directory.model.UserPhone; +import com.google.api.services.directory.model.UserPhoto; +import com.google.api.services.directory.model.UserRelation; import com.google.common.base.CharMatcher; import com.google.common.escape.Escaper; import com.google.common.escape.Escapers; import java.io.IOException; import java.util.Base64; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -58,14 +58,20 @@ import org.identityconnectors.framework.common.exceptions.ConnectorException; import org.identityconnectors.framework.common.exceptions.InvalidAttributeValueException; import org.identityconnectors.framework.common.objects.Attribute; +import org.identityconnectors.framework.common.objects.AttributeBuilder; +import org.identityconnectors.framework.common.objects.AttributeDelta; +import org.identityconnectors.framework.common.objects.AttributeDeltaUtil; import org.identityconnectors.framework.common.objects.AttributeInfoBuilder; import org.identityconnectors.framework.common.objects.AttributeUtil; import org.identityconnectors.framework.common.objects.AttributesAccessor; +import org.identityconnectors.framework.common.objects.ConnectorObject; +import org.identityconnectors.framework.common.objects.ConnectorObjectBuilder; import org.identityconnectors.framework.common.objects.Name; import org.identityconnectors.framework.common.objects.ObjectClassInfo; import org.identityconnectors.framework.common.objects.ObjectClassInfoBuilder; import org.identityconnectors.framework.common.objects.OperationalAttributes; import org.identityconnectors.framework.common.objects.PredefinedAttributeInfos; +import org.identityconnectors.framework.common.objects.PredefinedAttributes; import org.identityconnectors.framework.common.objects.Uid; import org.identityconnectors.framework.common.objects.filter.AndFilter; import org.identityconnectors.framework.common.objects.filter.ContainsAllValuesFilter; @@ -486,6 +492,8 @@ public static ObjectClassInfo getObjectClassInfo(final String customSchemasJSON) // primaryEmail builder.addAttributeInfo(Name.INFO); + builder.addAttributeInfo(AttributeInfoBuilder.define(GoogleAppsUtil.ID_ATTR).setRequired(true) + .build()); builder.addAttributeInfo(AttributeInfoBuilder.define(GoogleAppsUtil.GIVEN_NAME_ATTR).setRequired(true) .build()); builder.addAttributeInfo(AttributeInfoBuilder.define(GoogleAppsUtil.FAMILY_NAME_ATTR).setRequired(true) @@ -618,8 +626,7 @@ public static Directory.Users.Insert createUser( } user.setName(new UserName()); - // givenName The user's first name. Required when creating a user - // account. + // givenName The user's first name. Required when creating a user account. String givenName = attributes.findString(GoogleAppsUtil.GIVEN_NAME_ATTR); if (StringUtil.isNotBlank(givenName)) { user.getName().setGivenName(givenName); @@ -772,11 +779,11 @@ public static Directory.Users.Patch updateUser( })); Optional.ofNullable(attributes.find(GoogleAppsUtil.CHANGE_PASSWORD_AT_NEXT_LOGIN_ATTR)) - .flatMap(changePasswordAtNextLogin -> GoogleAppsUtil.getBooleanValue(changePasswordAtNextLogin)) + .flatMap(GoogleAppsUtil::getBooleanValue) .ifPresent(booleanValue -> set(content, u -> u.setChangePasswordAtNextLogin(booleanValue))); Optional.ofNullable(attributes.find(GoogleAppsUtil.IP_WHITELISTED_ATTR)) - .flatMap(ipWhitelisted -> GoogleAppsUtil.getBooleanValue(ipWhitelisted)) + .flatMap(GoogleAppsUtil::getBooleanValue) .ifPresent(booleanValue -> set(content, u -> u.setIpWhitelisted(booleanValue))); Optional.ofNullable(attributes.find(GoogleAppsUtil.ORG_UNIT_PATH_ATTR)) @@ -784,12 +791,13 @@ public static Directory.Users.Patch updateUser( .ifPresent(stringValue -> set(content, u -> u.setOrgUnitPath(stringValue))); Optional.ofNullable(attributes.find(GoogleAppsUtil.INCLUDE_IN_GLOBAL_ADDRESS_LIST_ATTR)) - .flatMap(includeInGlobalAddressList -> GoogleAppsUtil.getBooleanValue(includeInGlobalAddressList)) + .flatMap(GoogleAppsUtil::getBooleanValue) .ifPresent(booleanValue -> set(content, u -> u.setIncludeInGlobalAddressList(booleanValue))); - // Complex attributes Optional.ofNullable(attributes.find(GoogleAppsUtil.EMAILS_ATTR)) - .ifPresent(emails -> set(content, u -> u.setEmails(buildObjs(emails.getValue(), UserEmail.class)))); + .ifPresent(emails -> set(content, u -> u.setEmails(emails.getValue()))); + + // Complex attributes Optional.ofNullable(attributes.findList(GoogleAppsUtil.IMS_ATTR)) .ifPresent(value -> set(content, u -> u.setIms(buildObjs(value, UserIm.class)))); Optional.ofNullable(attributes.findList(GoogleAppsUtil.EXTERNAL_IDS_ATTR)) @@ -818,6 +826,139 @@ public static Directory.Users.Patch updateUser( } } + private static Object getValueByType( + final GoogleAppsCustomSchema innerSchema, + final Set modifications, + final String innerSchemaName) { + + return Optional.ofNullable(AttributeDeltaUtil.find(innerSchemaName, modifications)). + map(attrDelta -> innerSchema.getMultiValued() + ? attrDelta.getValuesToReplace() : AttributeDeltaUtil.getStringValue(attrDelta)). + orElse(null); + } + + private static Map> buildCustomAttrs( + final String customSchemas, final Set modifications) { + + List schemas = GoogleAppsUtil.extractCustomSchemas(customSchemas); + Map> attrsToAdd = new HashMap<>(); + for (GoogleAppsCustomSchema customSchema : schemas) { + if (customSchema.getType().equals("object")) { + // parse inner schemas + String basicName = customSchema.getName(); + // manage only first level inner schemas + for (GoogleAppsCustomSchema innerSchema : customSchema.getInnerSchemas()) { + final String innerSchemaName = basicName + "." + innerSchema.getName(); + if (attrsToAdd.containsKey(basicName)) { + attrsToAdd.get(basicName).put( + innerSchema.getName(), + getValueByType(innerSchema, modifications, innerSchemaName)); + } else { + Map value = new HashMap<>(); + value.put(innerSchema.getName(), getValueByType(innerSchema, modifications, innerSchemaName)); + attrsToAdd.put(basicName, value); + } + } + } else { + LOG.warn("CustomSchema type {0} not allowed at this level", customSchema.getType()); + } + } + return attrsToAdd; + } + + public static Directory.Users.Update updateUser( + final Directory.Users service, + final String userKey, + final Set modifications, + final String customSchemas) { + + if (AttributeDeltaUtil.getUidAttributeDelta(modifications) != null + || AttributeDeltaUtil.getAttributeDeltaForName(modifications) != null) { + + throw new IllegalArgumentException("Do not perform rename via updateDelta, use standard update"); + } + + AtomicReference content = new AtomicReference<>(); + + Optional.ofNullable(AttributeDeltaUtil.find(OperationalAttributes.ENABLE_NAME, modifications)) + .flatMap(GoogleAppsUtil::getBooleanValue) + .ifPresent(enable -> set(content, u -> u.setSuspended(!enable))); + + Optional.ofNullable(AttributeDeltaUtil.getPasswordValue(modifications)) + .ifPresent(password -> set(content, u -> u.setPassword(SecurityUtil.decrypt(password)))); + + Optional.ofNullable(AttributeDeltaUtil.find(GoogleAppsUtil.GIVEN_NAME_ATTR, modifications)) + .flatMap(GoogleAppsUtil::getStringValue) + .ifPresent(stringValue -> set(content, u -> { + + u.setName(new UserName()); + u.getName().setGivenName(stringValue); + })); + + Optional.ofNullable(AttributeDeltaUtil.find(GoogleAppsUtil.FAMILY_NAME_ATTR, modifications)) + .flatMap(GoogleAppsUtil::getStringValue) + .ifPresent(stringValue -> set(content, u -> { + + Optional.ofNullable(u.getName()).orElseGet(() -> { + u.setName(new UserName()); + return u.getName(); + }).setFamilyName(stringValue); + })); + + Optional.ofNullable(AttributeDeltaUtil.find(GoogleAppsUtil.CHANGE_PASSWORD_AT_NEXT_LOGIN_ATTR, modifications)) + .flatMap(GoogleAppsUtil::getBooleanValue) + .ifPresent(booleanValue -> set(content, u -> u.setChangePasswordAtNextLogin(booleanValue))); + + Optional.ofNullable(AttributeDeltaUtil.find(GoogleAppsUtil.IP_WHITELISTED_ATTR, modifications)) + .flatMap(GoogleAppsUtil::getBooleanValue) + .ifPresent(booleanValue -> set(content, u -> u.setIpWhitelisted(booleanValue))); + + Optional.ofNullable(AttributeDeltaUtil.find(GoogleAppsUtil.ORG_UNIT_PATH_ATTR, modifications)) + .flatMap(GoogleAppsUtil::getStringValue) + .ifPresent(stringValue -> set(content, u -> u.setOrgUnitPath(stringValue))); + + Optional.ofNullable(AttributeDeltaUtil.find(GoogleAppsUtil.INCLUDE_IN_GLOBAL_ADDRESS_LIST_ATTR, modifications)) + .flatMap(GoogleAppsUtil::getBooleanValue) + .ifPresent(booleanValue -> set(content, u -> u.setIncludeInGlobalAddressList(booleanValue))); + + Optional.ofNullable(AttributeDeltaUtil.find(GoogleAppsUtil.EMAILS_ATTR, modifications)) + .ifPresent(a -> set(content, u -> u.setEmails(a.getValuesToReplace()))); + + // Complex attributes + Optional.ofNullable(AttributeDeltaUtil.find(GoogleAppsUtil.IMS_ATTR, modifications)) + .ifPresent(a -> set(content, u -> u.setIms( + buildObjs(a.getValuesToReplace(), UserIm.class)))); + Optional.ofNullable(AttributeDeltaUtil.find(GoogleAppsUtil.EXTERNAL_IDS_ATTR, modifications)) + .ifPresent(a -> set(content, u -> u.setExternalIds( + buildObjs(a.getValuesToReplace(), UserExternalId.class)))); + Optional.ofNullable(AttributeDeltaUtil.find(GoogleAppsUtil.RELATIONS_ATTR, modifications)) + .ifPresent(a -> set(content, u -> u.setRelations( + buildObjs(a.getValuesToReplace(), UserRelation.class)))); + Optional.ofNullable(AttributeDeltaUtil.find(GoogleAppsUtil.ADDRESSES_ATTR, modifications)) + .ifPresent(a -> set(content, u -> u.setAddresses( + buildObjs(a.getValuesToReplace(), UserAddress.class)))); + Optional.ofNullable(AttributeDeltaUtil.find(GoogleAppsUtil.ORGANIZATIONS_ATTR, modifications)) + .ifPresent(a -> set(content, u -> u.setOrganizations( + buildObjs(a.getValuesToReplace(), UserOrganization.class)))); + Optional.ofNullable(AttributeDeltaUtil.find(GoogleAppsUtil.PHONES_ATTR, modifications)) + .ifPresent(a -> set(content, u -> u.setPhones( + buildObjs(a.getValuesToReplace(), UserPhone.class)))); + + if (StringUtil.isNotBlank(customSchemas)) { + set(content, u -> u.setCustomSchemas(buildCustomAttrs(customSchemas, modifications))); + } + + if (null == content.get()) { + return null; + } + try { + return service.update(userKey, content.get()).setFields(GoogleAppsUtil.ID_ETAG); + } catch (IOException e) { + LOG.warn(e, "Failed to initialize Users#update"); + throw ConnectorException.wrap(e); + } + } + public static Directory.Users.Photos.Update createUpdateUserPhoto( final Directory.Users.Photos service, final String userKey, final byte[] data) { @@ -855,4 +996,169 @@ public static Directory.Users.Aliases.Delete deleteUserAlias( throw ConnectorException.wrap(e); } } + + private static Object getValueFromKey( + final String customSchema, + final Map> customSchemas) { + + String[] names = customSchema.split("\\."); + return names.length > 1 + ? customSchemas.get(names[0]) != null ? customSchemas.get(names[0]).get(names[1]) : null + : null; + } + + public static ConnectorObject fromUser( + final GoogleAppsConfiguration configuration, + final User user, + final Set attributesToGet, + final Directory.Groups service) { + + ConnectorObjectBuilder builder = new ConnectorObjectBuilder(); + if (null != user.getEtag()) { + builder.setUid(new Uid(user.getId(), user.getEtag())); + } else { + builder.setUid(user.getId()); + } + builder.setName(user.getPrimaryEmail()); + if (user.getSuspended() != null) { + builder.addAttribute(AttributeBuilder.build(OperationalAttributes.ENABLE_NAME, !user.getSuspended())); + } + + if ((null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.ID_ATTR))) { + builder.addAttribute(AttributeBuilder.build(GoogleAppsUtil.ID_ATTR, user.getId())); + } + if ((null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.PRIMARY_EMAIL_ATTR))) { + builder.addAttribute(AttributeBuilder.build(GoogleAppsUtil.PRIMARY_EMAIL_ATTR, user.getPrimaryEmail())); + } + // Optional + // If both givenName and familyName are empty then Google didn't return with 'name' + if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.GIVEN_NAME_ATTR)) { + builder.addAttribute(AttributeBuilder.build(GoogleAppsUtil.GIVEN_NAME_ATTR, + null != user.getName() ? user.getName().getGivenName() : null)); + } + if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.FAMILY_NAME_ATTR)) { + builder.addAttribute(AttributeBuilder.build(GoogleAppsUtil.FAMILY_NAME_ATTR, + null != user.getName() ? user.getName().getFamilyName() : null)); + } + if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.FULL_NAME_ATTR)) { + builder.addAttribute(AttributeBuilder.build(GoogleAppsUtil.FULL_NAME_ATTR, + null != user.getName() ? user.getName().getFullName() : null)); + } + + if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.IS_ADMIN_ATTR)) { + builder.addAttribute(AttributeBuilder.build(GoogleAppsUtil.IS_ADMIN_ATTR, user.getIsAdmin())); + } + if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.IS_DELEGATED_ADMIN_ATTR)) { + builder.addAttribute(AttributeBuilder.build( + GoogleAppsUtil.IS_DELEGATED_ADMIN_ATTR, user.getIsDelegatedAdmin())); + } + if ((null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.LAST_LOGIN_TIME_ATTR)) + && user.getLastLoginTime() != null) { + + builder.addAttribute(AttributeBuilder.build( + GoogleAppsUtil.LAST_LOGIN_TIME_ATTR, user.getLastLoginTime().toString())); + } + if ((null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.CREATION_TIME_ATTR)) + && user.getCreationTime() != null) { + + builder.addAttribute(AttributeBuilder.build( + GoogleAppsUtil.CREATION_TIME_ATTR, user.getCreationTime().toString())); + } + if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.AGREED_TO_TERMS_ATTR)) { + builder.addAttribute(AttributeBuilder.build(GoogleAppsUtil.AGREED_TO_TERMS_ATTR, user.getAgreedToTerms())); + } + if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.SUSPENSION_REASON_ATTR)) { + builder.addAttribute(AttributeBuilder.build( + GoogleAppsUtil.SUSPENSION_REASON_ATTR, user.getSuspensionReason())); + } + if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.CHANGE_PASSWORD_AT_NEXT_LOGIN_ATTR)) { + builder.addAttribute(AttributeBuilder.build( + GoogleAppsUtil.CHANGE_PASSWORD_AT_NEXT_LOGIN_ATTR, user.getChangePasswordAtNextLogin())); + } + if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.IP_WHITELISTED_ATTR)) { + builder.addAttribute(AttributeBuilder.build(GoogleAppsUtil.IP_WHITELISTED_ATTR, user.getIpWhitelisted())); + } + if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.IMS_ATTR)) { + builder.addAttribute(AttributeBuilder.build(GoogleAppsUtil.IMS_ATTR, (Collection) user.getIms())); + } + if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.EMAILS_ATTR)) { + builder.addAttribute(AttributeBuilder.build(GoogleAppsUtil.EMAILS_ATTR, (Collection) user.getEmails())); + } + if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.EXTERNAL_IDS_ATTR)) { + builder.addAttribute(AttributeBuilder.build( + GoogleAppsUtil.EXTERNAL_IDS_ATTR, (Collection) user.getExternalIds())); + } + if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.RELATIONS_ATTR)) { + builder.addAttribute(AttributeBuilder.build( + GoogleAppsUtil.RELATIONS_ATTR, (Collection) user.getRelations())); + } + if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.ADDRESSES_ATTR)) { + builder.addAttribute(AttributeBuilder.build( + GoogleAppsUtil.ADDRESSES_ATTR, (Collection) user.getAddresses())); + } + if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.ORGANIZATIONS_ATTR)) { + builder.addAttribute(AttributeBuilder.build( + GoogleAppsUtil.ORGANIZATIONS_ATTR, (Collection) user.getOrganizations())); + } + if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.PHONES_ATTR)) { + builder.addAttribute(AttributeBuilder.build(GoogleAppsUtil.PHONES_ATTR, (Collection) user.getPhones())); + } + if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.ALIASES_ATTR)) { + builder.addAttribute(AttributeBuilder.build(GoogleAppsUtil.ALIASES_ATTR, user.getAliases())); + } + + if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.NON_EDITABLE_ALIASES_ATTR)) { + builder.addAttribute(AttributeBuilder.build( + GoogleAppsUtil.NON_EDITABLE_ALIASES_ATTR, user.getNonEditableAliases())); + } + + if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.CUSTOMER_ID_ATTR)) { + builder.addAttribute(AttributeBuilder.build(GoogleAppsUtil.CUSTOMER_ID_ATTR, user.getCustomerId())); + } + if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.ORG_UNIT_PATH_ATTR)) { + builder.addAttribute(AttributeBuilder.build(GoogleAppsUtil.ORG_UNIT_PATH_ATTR, user.getOrgUnitPath())); + } + if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.IS_MAILBOX_SETUP_ATTR)) { + builder.addAttribute(AttributeBuilder.build( + GoogleAppsUtil.IS_MAILBOX_SETUP_ATTR, user.getIsMailboxSetup())); + } + if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.INCLUDE_IN_GLOBAL_ADDRESS_LIST_ATTR)) { + builder.addAttribute(AttributeBuilder.build( + GoogleAppsUtil.INCLUDE_IN_GLOBAL_ADDRESS_LIST_ATTR, user.getIncludeInGlobalAddressList())); + } + if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.THUMBNAIL_PHOTO_URL_ATTR)) { + builder.addAttribute( + AttributeBuilder.build(GoogleAppsUtil.THUMBNAIL_PHOTO_URL_ATTR, user.getThumbnailPhotoUrl())); + } + if (null == attributesToGet || attributesToGet.contains(GoogleAppsUtil.DELETION_TIME_ATTR)) { + builder.addAttribute(AttributeBuilder.build(GoogleAppsUtil.DELETION_TIME_ATTR, + null != user.getDeletionTime() ? user.getDeletionTime().toString() : null)); + } + if (null == attributesToGet || ("full".equals(configuration.getProjection()) + && StringUtil.isNotBlank(configuration.getCustomSchemasJSON()))) { + + GoogleAppsUtil.extractCustomSchemas(configuration.getCustomSchemasJSON()).forEach(customSchema -> { + if (customSchema.getType().equals("object")) { + // manage only first level inner schemas + for (GoogleAppsCustomSchema innerSchema : customSchema.getInnerSchemas()) { + String innerSchemaName = customSchema.getName() + "." + innerSchema.getName(); + builder.addAttribute(AttributeBuilder.build( + innerSchemaName, + null != user.getCustomSchemas() + ? getValueFromKey(innerSchemaName, user.getCustomSchemas()) + : null)); + } + } else { + LOG.warn("CustomSchema type {0} not allowed at this level", customSchema.getType()); + } + }); + } + // Expensive to get + if (null != attributesToGet && attributesToGet.contains(PredefinedAttributes.GROUPS_NAME)) { + builder.addAttribute(AttributeBuilder.build(PredefinedAttributes.GROUPS_NAME, + GroupHandler.listGroups(service, user.getId(), configuration.getDomain()))); + } + + return builder.build(); + } } diff --git a/src/main/java/net/tirasa/connid/bundles/googleapps/credentialsgenerator/CredentialsGeneratorApplication.java b/src/main/java/net/tirasa/connid/bundles/googleapps/credentialsgenerator/CredentialsGeneratorApplication.java index 3137d38..ddba883 100644 --- a/src/main/java/net/tirasa/connid/bundles/googleapps/credentialsgenerator/CredentialsGeneratorApplication.java +++ b/src/main/java/net/tirasa/connid/bundles/googleapps/credentialsgenerator/CredentialsGeneratorApplication.java @@ -38,6 +38,8 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.Banner; import org.springframework.boot.CommandLineRunner; @@ -51,6 +53,8 @@ @EnableConfigurationProperties public class CredentialsGeneratorApplication implements CommandLineRunner { + private static final Logger LOG = LoggerFactory.getLogger(CredentialsGeneratorApplication.class); + // Path to client_secrets.json which should contain a JSON document such as: // { // "web or installed": { @@ -62,16 +66,16 @@ public class CredentialsGeneratorApplication implements CommandLineRunner { // } private static final String CLIENTSECRETS_LOCATION = "client_secrets.json"; - public static final String ADMIN_DIRECTORY_GROUP = + private static final String ADMIN_DIRECTORY_GROUP = "https://www.googleapis.com/auth/admin.directory.group"; - public static final String ADMIN_DIRECTORY_ORGUNIT = + private static final String ADMIN_DIRECTORY_ORGUNIT = "https://www.googleapis.com/auth/admin.directory.orgunit"; - public static final String ADMIN_DIRECTORY_USER = + private static final String ADMIN_DIRECTORY_USER = "https://www.googleapis.com/auth/admin.directory.user"; - public static final String ADMIN_ENTERPRISE_LICENSE = + private static final String ADMIN_ENTERPRISE_LICENSE = "https://www.googleapis.com/auth/apps.licensing"; public static final Map CONFIG_MAP = new LinkedHashMap<>(3); @@ -103,12 +107,23 @@ private void getConfigurationMap(final File clientJson) throws IOException, URIS CONFIG_MAP.put("clientSecret", clientSecrets.getDetails().getClientSecret()); String requestUrl = new GoogleAuthorizationCodeRequestUrl( - clientSecrets.getDetails().getClientId(), - redirectUri, SCOPES) - .setState("/profile").build(); - System.out.println("Request Url is " + requestUrl); - - Desktop.getDesktop().browse(new URI(requestUrl)); + clientSecrets.getDetails().getClientId(), redirectUri, SCOPES).setState("/profile").build(); + LOG.info("Request Url is {}", requestUrl); + + if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) { + try { + Desktop.getDesktop().browse(new URI(requestUrl)); + } catch (IOException | URISyntaxException e) { + LOG.error("Could not browse the URL above", e); + } + } else { + Runtime runtime = Runtime.getRuntime(); + try { + runtime.exec("xdg-open " + requestUrl); + } catch (IOException e) { + LOG.error("Could not browse the URL above", e); + } + } } @Bean @@ -131,7 +146,7 @@ public void run(final String... args) throws IOException, URISyntaxException { if (clientJson.exists() && clientJson.isFile()) { getConfigurationMap(clientJson); } else { - System.err.println("Invalid client secret path. File not exists " + clientJson); + LOG.error("Invalid client secret path: {}", clientJson); } } }