/*
* Copyright 2008 Sun Microsystems, Inc. All rights reserved.
*
* U.S. Government Rights - Commercial software. Government users
* are subject to the Sun Microsystems, Inc. standard license agreement
* and applicable provisions of the FAR and its supplements.
*
* Use is subject to license terms.
*
* This distribution may include materials developed by third parties.
* Sun, Sun Microsystems, the Sun logo, Java and Project Identity
* Connectors are trademarks or registered trademarks of Sun
* Microsystems, Inc. or its subsidiaries in the U.S. and other
* countries.
*
* UNIX is a registered trademark in the U.S. and other countries,
* exclusively licensed through X/Open Company, Ltd.
*
* -----------
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 2008 Sun Microsystems, Inc. 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://identityconnectors.dev.java.net/CDDLv1.0.html
* 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 identityconnectors/legal/license.txt.
* 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]"
* -----------
*
* Portions Copyrighted 2012 ForgeRock Inc.
*
*/
package org.forgerock.openicf.connectors.googleapps;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import org.identityconnectors.common.logging.Log;
import org.identityconnectors.common.security.GuardedString;
import org.identityconnectors.framework.common.exceptions.ConnectorException;
import org.identityconnectors.framework.common.objects.Attribute;
import org.identityconnectors.framework.common.objects.AttributeBuilder;
import org.identityconnectors.framework.common.objects.AttributeInfo;
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.Name;
import org.identityconnectors.framework.common.objects.ObjectClass;
import org.identityconnectors.framework.common.objects.ObjectClassInfoBuilder;
import org.identityconnectors.framework.common.objects.OperationOptions;
import org.identityconnectors.framework.common.objects.OperationalAttributeInfos;
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.ResultsHandler;
import org.identityconnectors.framework.common.objects.Schema;
import org.identityconnectors.framework.common.objects.SchemaBuilder;
import org.identityconnectors.framework.common.objects.ScriptContext;
import org.identityconnectors.framework.common.objects.SyncResultsHandler;
import org.identityconnectors.framework.common.objects.SyncToken;
import org.identityconnectors.framework.common.objects.Uid;
import org.identityconnectors.framework.common.objects.filter.FilterTranslator;
import org.identityconnectors.framework.spi.AttributeNormalizer;
import org.identityconnectors.framework.spi.Configuration;
import org.identityconnectors.framework.spi.ConnectorClass;
import org.identityconnectors.framework.spi.PoolableConnector;
import org.identityconnectors.framework.spi.operations.AuthenticateOp;
import org.identityconnectors.framework.spi.operations.CreateOp;
import org.identityconnectors.framework.spi.operations.DeleteOp;
import org.identityconnectors.framework.spi.operations.ResolveUsernameOp;
import org.identityconnectors.framework.spi.operations.SchemaOp;
import org.identityconnectors.framework.spi.operations.ScriptOnConnectorOp;
import org.identityconnectors.framework.spi.operations.ScriptOnResourceOp;
import org.identityconnectors.framework.spi.operations.SearchOp;
import org.identityconnectors.framework.spi.operations.SyncOp;
import org.identityconnectors.framework.spi.operations.TestOp;
import org.identityconnectors.framework.spi.operations.UpdateAttributeValuesOp;
import org.identityconnectors.framework.spi.operations.UpdateOp;
/**
* <p>
* A connector for Google Apps for your domain. This connector can manage
* accounts on Google Apps. It also manages nicknames (email aliases) associated
* with users.
* <p>
*
* Notes
*
* According to this thread: <br/>
* http://groups.google.com/group/google-apps-apis/browse_thread/thread/
* d68b2458b4e84777 <br/>
* The API does not support a rename operation on an account. You have to delete
* and receate the account. <br/>
* Quota can be read - but it appears to ignore it on a create. This is the same
* behavior as creating through the Google Admin Web GUI (it doesnt even let you
* set it). I believe this is due to the type of google apps account I have used
* for testing. The default is to allow the user to have the maxium quota -
* which is what most organizations will want anyways.
*
*
* @author $author$
* @version $Revision$ $Date$
*/
@ConnectorClass(displayNameKey = "GoogleApps", configurationClass = GoogleAppsConfiguration.class)
public class GoogleAppsConnector implements PoolableConnector, AuthenticateOp, CreateOp, DeleteOp,
ResolveUsernameOp, SchemaOp, ScriptOnConnectorOp, ScriptOnResourceOp, SearchOp<String>,
SyncOp, TestOp, UpdateAttributeValuesOp, UpdateOp, AttributeNormalizer {
public static final String ATTR_FAMILY_NAME = "familyName";
public static final String ATTR_GIVEN_NAME = "givenName";
public static final String ATTR_QUOTA = "quota";
public static final String ATTR_NICKNAME_LIST = "nicknames";
// Group Objects
public static final String ATTR_MEMBER_LIST = "members";
public static final String ATTR_OWNER_LIST = "owners";
public static final String ATTR_GROUP_TEXT_NAME = "groupName";
public static final String ATTR_GROUP_PERMISSIONS = "groupPermissions";
/**
* Setup logging for the {@link GoogleAppsConnector}.
*/
private static final Log LOGGER = Log.getLog(GoogleAppsConnector.class);
/**
* Place holder for the Connection created in the init method
*/
private GoogleAppsClient connection;
/**
* Place holder for the {@link Configuration} passed into the init() method
* {@link GoogleAppsConnector#init(org.identityconnectors.framework.spi.Configuration)}
* .
*/
private GoogleAppsConfiguration configuration;
private GoogleAppsUserOps userOps;
private GoogleAppsGroupOps groupOps;
/**
* Gets the Configuration context for this connector.
*/
public Configuration getConfiguration() {
return this.configuration;
}
/**
* Callback method to receive the {@link Configuration}.
*
* @see org.identityconnectors.framework.spi.Connector#init(org.identityconnectors.framework.spi.Configuration)
*/
public void init(Configuration configuration) {
this.configuration = (GoogleAppsConfiguration) configuration;
this.userOps = new GoogleAppsUserOps(this);
this.groupOps = new GoogleAppsGroupOps(this);
}
/**
* Disposes of the {@link GoogleAppsConnector}'s resources.
*
* @see org.identityconnectors.framework.spi.Connector#dispose()
*/
public void dispose() {
configuration = null;
if (connection != null) {
connection = null;
}
}
/**
* {@inheritDoc}
*/
public void checkAlive() {
}
/******************
* SPI Operations
*
* Implement the following operations using the contract and description
* found in the Javadoc for these methods.
******************/
/**
* {@inheritDoc}
*/
public Uid authenticate(final ObjectClass objectClass, final String userName,
final GuardedString password, final OperationOptions options) {
throw new UnsupportedOperationException();
}
/**
* {@inheritDoc}
*/
public Uid resolveUsername(final ObjectClass objectClass, final String userName,
final OperationOptions options) {
throw new UnsupportedOperationException();
}
/**
* {@inheritDoc}
*/
public Uid create(final ObjectClass objectClass, final Set<Attribute> createAttributes,
final OperationOptions options) {
Name name = AttributeUtil.getNameFromAttributes(createAttributes);
LOGGER.info("Create {0}", name);
AttributesAccessor a = new AttributesAccessor(createAttributes);
if (name == null)
return null;
if (ObjectClass.ACCOUNT.equals(objectClass))
return userOps.createUser(name, a);
else if (ObjectClass.GROUP.equals(objectClass))
return groupOps.createGroup(name, a);
else
throw new IllegalArgumentException("Unsupported Object Class="
+ objectClass.getObjectClassValue());
}
/**
* {@inheritDoc}
*/
public void delete(final ObjectClass objectClass, final Uid uid, final OperationOptions options) {
String id = uid.getUidValue();
LOGGER.info("Deleting {0}", id);
if (ObjectClass.ACCOUNT.equals(objectClass))
userOps.delete(id);
else if (ObjectClass.GROUP.equals(objectClass))
groupOps.delete(id);
else
throw new IllegalArgumentException("Unsupported Object Class="
+ objectClass.getObjectClassValue());
}
/**
* {@inheritDoc}
*/
public Schema schema() {
SchemaBuilder schemaBuilder = new SchemaBuilder(GoogleAppsConnector.class);
// Build User Schema
ObjectClassInfoBuilder ocBuilder = new ObjectClassInfoBuilder();
ocBuilder.addAttributeInfo(PredefinedAttributeInfos.GROUPS);
ocBuilder.addAttributeInfo(new AttributeInfoBuilder(Name.NAME).setCreateable(true)
.setUpdateable(false).setRequired(true).build());
ocBuilder.addAttributeInfo(new AttributeInfoBuilder(ATTR_FAMILY_NAME).setRequired(true)
.build());
ocBuilder.addAttributeInfo(new AttributeInfoBuilder(ATTR_GIVEN_NAME).setRequired(true)
.build());
ocBuilder.addAttributeInfo(new AttributeInfoBuilder(ATTR_QUOTA, Integer.class)
.setCreateable(true).setUpdateable(false).build());
// Multi-valued attributes - nicknames - not required, not returned by
// default
ocBuilder.addAttributeInfo(new AttributeInfoBuilder(ATTR_NICKNAME_LIST)
.setMultiValued(true).setReturnedByDefault(false).build());
// Operational Attributes - password and enable/disable status
ocBuilder
.addAttributeInfo(AttributeInfoBuilder.build(OperationalAttributes.PASSWORD_NAME,
GuardedString.class, EnumSet.of(AttributeInfo.Flags.NOT_READABLE,
AttributeInfo.Flags.NOT_RETURNED_BY_DEFAULT,
AttributeInfo.Flags.REQUIRED)));
ocBuilder.addAttributeInfo(OperationalAttributeInfos.ENABLE);
schemaBuilder.defineObjectClass(ocBuilder.build());
// Build Group Schema
ocBuilder = new ObjectClassInfoBuilder();
ocBuilder.setType(ObjectClass.GROUP_NAME);
ocBuilder.addAttributeInfo(new AttributeInfoBuilder(Name.NAME).setCreateable(true)
.setUpdateable(false).setRequired(true).build());
ocBuilder.addAttributeInfo(AttributeInfoBuilder.build(ATTR_GROUP_TEXT_NAME, String.class,
EnumSet.of(AttributeInfo.Flags.REQUIRED)));
ocBuilder.addAttributeInfo(AttributeInfoBuilder.build(PredefinedAttributes.DESCRIPTION,
String.class, EnumSet.of(AttributeInfo.Flags.REQUIRED)));
ocBuilder.addAttributeInfo(AttributeInfoBuilder.build(ATTR_GROUP_PERMISSIONS, String.class,
EnumSet.of(AttributeInfo.Flags.REQUIRED)));
ocBuilder.addAttributeInfo(AttributeInfoBuilder.build(ATTR_OWNER_LIST, String.class,
EnumSet.of(AttributeInfo.Flags.REQUIRED, AttributeInfo.Flags.MULTIVALUED,
AttributeInfo.Flags.NOT_RETURNED_BY_DEFAULT)));
ocBuilder.addAttributeInfo(AttributeInfoBuilder.build(ATTR_MEMBER_LIST, String.class,
EnumSet.of(AttributeInfo.Flags.REQUIRED, AttributeInfo.Flags.MULTIVALUED,
AttributeInfo.Flags.NOT_RETURNED_BY_DEFAULT)));
schemaBuilder.defineObjectClass(ocBuilder.build());
return schemaBuilder.build();
}
/**
* {@inheritDoc}
*/
public Object runScriptOnConnector(ScriptContext request, OperationOptions options) {
throw new UnsupportedOperationException();
}
/**
* {@inheritDoc}
*/
public Object runScriptOnResource(ScriptContext request, OperationOptions options) {
throw new UnsupportedOperationException();
}
/**
* {@inheritDoc}
*/
public FilterTranslator<String> createFilterTranslator(ObjectClass objectClass,
OperationOptions options) {
return new GoogleAppsFilterTranslator();
}
/**
* {@inheritDoc}
*/
public void executeQuery(ObjectClass objectClass, String query, ResultsHandler handler,
OperationOptions options) {
LOGGER.info("query string = {0} options = {1}", query, options.getAttributesToGet());
if (ObjectClass.ACCOUNT.equals(objectClass))
userOps.query(query, handler, options);
else if (ObjectClass.GROUP.equals(objectClass))
groupOps.query(query, handler, options);
else
throw new IllegalArgumentException("Unsupported objectclass '" + objectClass + "'");
}
/**
* {@inheritDoc}
*/
public void sync(ObjectClass objectClass, SyncToken token, SyncResultsHandler handler,
final OperationOptions options) {
throw new UnsupportedOperationException();
}
/**
* {@inheritDoc}
*/
public SyncToken getLatestSyncToken(ObjectClass objectClass) {
throw new UnsupportedOperationException();
}
/**
* {@inheritDoc}
*/
public void test() {
LOGGER.info("test connection");
getClient().testConnection();
}
/**
* {@inheritDoc}
*/
public Uid update(ObjectClass objectClass, Uid uid, Set<Attribute> replaceAttributes,
OperationOptions options) {
LOGGER.info("Update {0}", replaceAttributes);
if (uid == null) {
throw new ConnectorException("Uid attribute is missing!");
}
if (replaceAttributes == null || replaceAttributes.size() == 0) {
throw new IllegalArgumentException("Invalid attributes provided to a update operation.");
}
if (ObjectClass.ACCOUNT.equals(objectClass))
return userOps.updateUser(uid, replaceAttributes, options);
else if (ObjectClass.GROUP.equals(objectClass))
return groupOps.updateGroup(uid, replaceAttributes, options);
else
throw new IllegalArgumentException("Unsupported objectclass '" + objectClass + "'");
}
/**
* {@inheritDoc}
*/
public Uid addAttributeValues(ObjectClass objectClass, Uid uid, Set<Attribute> valuesToAdd,
OperationOptions options) {
throw new UnsupportedOperationException();
}
/**
* {@inheritDoc}
*/
public Uid removeAttributeValues(ObjectClass objectClass, Uid uid,
Set<Attribute> valuesToRemove, OperationOptions options) {
throw new UnsupportedOperationException();
}
public Attribute normalizeAttribute(final ObjectClass oclass, final Attribute attribute) {
if (ObjectClass.ACCOUNT.equals(oclass)) {
if (attribute.is(Name.NAME) || attribute.is(Uid.NAME)) {
// lowercased id
return AttributeBuilder.build(attribute.getName(), AttributeUtil.getStringValue(
attribute).toLowerCase());
} else if (attribute.is(PredefinedAttributes.GROUPS_NAME)) {
// all values should include domain name and be lowercased
return normalizeDomainAttribute(attribute);
} else if (attribute.is(ATTR_NICKNAME_LIST)) {
// all nicknames lowercased and alphabetically ordered
List<Object> values = attribute.getValue();
// no values to normalize
if (values == null)
return attribute;
List<String> normalized = new ArrayList<String>(values.size());
for (Object value : values) {
assert value instanceof String;
String strValue = (String) value;
normalized.add(strValue.toLowerCase());
}
Collections.sort(normalized);
return AttributeBuilder.build(attribute.getName(), normalized);
}
} else if (ObjectClass.GROUP.equals(oclass)) {
if (attribute.is(Name.NAME) || attribute.is(Uid.NAME) || attribute.is(ATTR_MEMBER_LIST)
|| attribute.is(ATTR_OWNER_LIST)) {
// all values should include domain name and be lowercased
return normalizeDomainAttribute(attribute);
}
}
return attribute;
}
private Attribute normalizeDomainAttribute(final Attribute attribute) {
List<Object> values = attribute.getValue();
// no values to normalize
if (values == null)
return attribute;
List<Object> normalized = new ArrayList<Object>(values.size());
for (Object value : values) {
assert value instanceof String;
String strValue = (String) value;
normalized.add(normalizeDomainValue(strValue));
}
return AttributeBuilder.build(attribute.getName(), normalized);
}
private String normalizeDomainValue(String oldValue) {
String newValue = oldValue;
if (oldValue.indexOf('@') == -1) {
// add domain name
StringBuilder sb = new StringBuilder(oldValue);
sb.append("@").append(configuration.getDomain());
newValue = sb.toString();
}
return newValue.toLowerCase();
}
/**
* Gets a {@code Connection}
*/
GoogleAppsClient getClient() {
try {
synchronized (this) {
if (connection == null) {
connection = new GoogleAppsClient(configuration);
}
return connection;
}
} catch (Exception e) {
throw ConnectorException.wrap(e);
}
}
}