/*
* 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.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import org.identityconnectors.common.logging.Log;
import org.identityconnectors.common.security.GuardedString;
import org.identityconnectors.framework.common.exceptions.AlreadyExistsException;
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;
import com.google.gdata.client.appsforyourdomain.AppsForYourDomainQuery;
import com.google.gdata.client.appsforyourdomain.AppsGroupsService;
import com.google.gdata.client.appsforyourdomain.NicknameService;
import com.google.gdata.client.appsforyourdomain.UserService;
import com.google.gdata.data.Link;
import com.google.gdata.data.appsforyourdomain.AppsForYourDomainErrorCode;
import com.google.gdata.data.appsforyourdomain.AppsForYourDomainException;
import com.google.gdata.data.appsforyourdomain.Login;
import com.google.gdata.data.appsforyourdomain.Name;
import com.google.gdata.data.appsforyourdomain.Nickname;
import com.google.gdata.data.appsforyourdomain.Quota;
import com.google.gdata.data.appsforyourdomain.generic.GenericEntry;
import com.google.gdata.data.appsforyourdomain.generic.GenericFeed;
import com.google.gdata.data.appsforyourdomain.provisioning.NicknameEntry;
import com.google.gdata.data.appsforyourdomain.provisioning.NicknameFeed;
import com.google.gdata.data.appsforyourdomain.provisioning.UserEntry;
import com.google.gdata.data.appsforyourdomain.provisioning.UserFeed;
import com.google.gdata.util.ServiceException;
/**
*
* Helper class containing the google apps methods to perform CRUD operations on
* google apps accounts
*
*
* Attribution: This is based on the sample application in the google apps java
* toolkit.
*
* @author Warren Strange
*/
public class GoogleAppsClient {
public final static String SERVICENAME = "identityConnectors";
protected static final String SERVICE_VERSION = "2.0";
protected NicknameService nicknameService;
protected UserService userService;
protected AppsGroupsService groupService;
private String domainUrlBase;
private String admin;
private static final Log log = Log.getLog(GoogleAppsClient.class);
/**
* Constructs a google apps connection object for the given domain using the
* given admin credentials.
*
* @param config
*/
public GoogleAppsClient(GoogleAppsConfiguration config) throws Exception {
this.domainUrlBase = config.getConnectionUrl();
if (!domainUrlBase.endsWith("/")) {
domainUrlBase += "/";
}
admin = config.getLogin();
String adminEmail = admin + "@" + config.getDomain();
final String[] clearText = new String[1];
GuardedString.Accessor accessor = new GuardedString.Accessor() {
public void access(char[] clearChars) {
clearText[0] = new String(clearChars);
}
};
config.getPassword().access(accessor);
// Configure all of the different Provisioning services
userService = new UserService(SERVICENAME + "-UserService");
userService.setUserCredentials(adminEmail, clearText[0]);
nicknameService = new NicknameService(SERVICENAME + "-NicknameService");
nicknameService.setUserCredentials(adminEmail, clearText[0]);
// todo: FIX ME - need to extract domain -
groupService =
new AppsGroupsService(adminEmail, clearText[0], config.getDomain(), SERVICENAME
+ "-AppsGroupService");
}
/**
* Retrieves a user.
*
* @param username
* The user you wish to retrieve.
* @return A UserEntry object of the retrieved user.
* @throws AppsForYourDomainException
* If a Provisioning API specific occurs.
* @throws ServiceException
* If a generic GData framework error occurs.
* @throws IOException
* If an error occurs communicating with the GData service.
*/
public UserEntry getUserEntry(String username) {
try {
URL retrieveUrl = new URL(domainUrlBase + "user/" + SERVICE_VERSION + "/" + username);
return (UserEntry) userService.getEntry(retrieveUrl, UserEntry.class);
} catch (AppsForYourDomainException e) {
if (e.getErrorCode() == AppsForYourDomainErrorCode.EntityDoesNotExist) {
// ok
return null;
}
throw ConnectorException.wrap(e);
} catch (Exception e) {
throw ConnectorException.wrap(e);
}
}
/**
* Test the connection.
*
* todo: We need to find a less expensive call to test the connection
*
*/
public void testConnection() {
// fetch the admin account to test the connection.
// as long as the connection is valid we should not throw an exception
try {
getUserEntry(admin);
} catch (Exception e) {
throw new ConnectorException("TestConnection failed. Wrapped exception ", e);
}
}
/**
* Retrieves all users in domain. This method may be very slow for domains
* with a large number of users. Any changes to users, including creations
* and deletions, which are made after this method is called may or may not
* be included in the Feed which is returned.
*
* @return A UserFeed object of the retrieved users.
* @throws AppsForYourDomainException
* If a Provisioning API specific occurs.
* @throws ServiceException
* If a generic GData framework error occurs.
* @throws IOException
* If an error occurs communicating with the GData service.
*/
private UserFeed getUserFeed() throws AppsForYourDomainException, ServiceException, IOException {
URL retrieveUrl = new URL(domainUrlBase + "user/" + SERVICE_VERSION + "/");
UserFeed allUsers = new UserFeed();
UserFeed currentPage;
Link nextLink;
do {
currentPage = (UserFeed) userService.getFeed(retrieveUrl, UserFeed.class);
allUsers.getEntries().addAll(currentPage.getEntries());
nextLink = currentPage.getLink(Link.Rel.NEXT, Link.Type.ATOM);
if (nextLink != null) {
retrieveUrl = new URL(nextLink.getHref());
}
} while (nextLink != null);
return allUsers;
}
public GoogleAppsAccountIterator getIterator() {
return new GoogleAppsAccountIterator();
}
public void updateGroup(String groupId, String groupName, String description, String permissions) {
try {
groupService.updateGroup(groupId, groupName, description, permissions);
} catch (Exception e) {
throw ConnectorException.wrap(e);
}
}
void addGroupOwner(String groupId, String owner) {
try {
groupService.addOwnerToGroup(groupId, owner);
} catch (Exception e) {
ConnectorException.wrap(e);
}
}
/**
* The google java api does not have a remove owner api?. See
* http://code.google.com/p/gdata-java-client/issues/detail?id=179
*
* @param groupId
* @param user
*/
void removeGroupOwner(String groupId, String user) {
throw new RuntimeException("Google API does not implment remove group owner?");
}
List<String> getGroupMembershipsForUser(String accountId) {
List<String> l = new ArrayList<String>();
try {
GenericFeed f = groupService.retrieveGroups(accountId, true);
List<GenericEntry> x = f.getEntries();
for (GenericEntry item : x) {
String group = item.getProperty(AppsGroupsService.APPS_PROP_GROUP_ID);
l.add(group);
}
} catch (Exception e) {
throw ConnectorException.wrap(e);
}
return l;
}
/**
* An iterator over google apps accounts. This is wrapper around the google
* apps iterator mechanism. Google apps returns results one page at at time.
* This iterator advances the pages as we consume entries.
*
* Exceptions here are wrapped and converted to RuntimeException - this is
* done to ease integration into the open connectors framework.
*
*/
public class GoogleAppsAccountIterator implements Iterator {
URL retrieveUrl;
UserFeed currentPage;
Iterator userIterator;
public GoogleAppsAccountIterator() {
try {
retrieveUrl = new URL(domainUrlBase + "user/" + SERVICE_VERSION + "/");
currentPage = userService.getFeed(retrieveUrl, UserFeed.class);
} catch (Exception ex) {
// rethrow as a generic IOException
throw new RuntimeException("Error creating google apps iterator", ex);
}
}
public UserEntry next() {
if (!hasNext()) {
throw new NoSuchElementException("Unexpected end of user list");
}
UserEntry ue = (UserEntry) userIterator.next();
/*
*
* for debug Name n = ue.getName(); Login l = ue.getLogin(); Quota q
* = ue.getQuota();
*
* System.out.println("Get entry id=" + l.getUserName() + " name=" +
* n.getFamilyName() + "," + n.getGivenName() + " Email=" +
* ue.getEmail() + " isSuspended=" + l.getSuspended() + " Quota=" +
* q.getLimit());
*/
return ue;
}
public void close() {
// no -op
}
/**
* Return true if there are more elements
*
* As a side effect - Advance the google apps page fetched if required.
*
* @return true if there are more user entry elements to read
*/
public boolean hasNext() {
if (currentPage == null) {
return false;
}
if (userIterator == null) {
userIterator = currentPage.getEntries().iterator();
}
if (userIterator.hasNext()) {
return true;
} // no need to advance - we have more entries to return
// iterator is empty. See if there are more pages to fetch
Link nextLink = currentPage.getLink(Link.Rel.NEXT, Link.Type.ATOM);
if (nextLink == null) {
return false;
}
try {
retrieveUrl = new URL(nextLink.getHref());
currentPage = (UserFeed) userService.getFeed(retrieveUrl, UserFeed.class);
} catch (Exception ex) {
throw new RuntimeException("Error trying to fetch next page of results", ex);
}
userIterator = null;
// todo: danger will robinson. Will this ever recurse forever? It
// should not...
return hasNext();
}
public void remove() {
throw new UnsupportedOperationException("Not supported yet.");
}
}
/**
* Retrieves all nicknames for the given username.
*
* @param username
* The user for which you want all nicknames.
* @return A NicknameFeed object with all the nicknames for the user.
* @throws AppsForYourDomainException
* If a Provisioning API specific occurs.
* @throws ServiceException
* If a generic GData framework error occurs.
* @throws IOException
* If an error occurs communicating with the GData service.
*/
public NicknameFeed getNicknameFeed(String username) {
try {
URL feedUrl = new URL(domainUrlBase + "nickname/" + SERVICE_VERSION);
AppsForYourDomainQuery query = new AppsForYourDomainQuery(feedUrl);
query.setUsername(username);
return (NicknameFeed) nicknameService.query(query, NicknameFeed.class);
} catch (Exception ex) {
throw ConnectorException.wrap(ex);
}
}
public List<String> getNicknamesAsList(String username) {
List<String> nnlist = new ArrayList<String>();
try {
NicknameFeed nicknames = getNicknameFeed(username);
if (nicknames != null) {
for (Iterator i = nicknames.getEntries().iterator(); i.hasNext();) {
NicknameEntry ne = (NicknameEntry) i.next();
Nickname nickname = ne.getNickname();
nnlist.add(nickname.getName());
}
}
return nnlist;
} catch (Exception ex) {
throw ConnectorException.wrap(ex);
}
}
/**
* Creates a nickname for the username.
*
* @param username
* The user for which we want to create a nickname.
* @param nickname
* The nickname you wish to create.
* @return A NicknameEntry object of the newly created nickname.
* @throws AppsForYourDomainException
* If a Provisioning API specific occurs.
* @throws ServiceException
* If a generic GData framework error occurs.
* @throws IOException
* If an error occurs communicating with the GData service.
*/
public void createNickname(String username, String nickname) {
try {
// System.out.println("Add nickname for " + username + " nick=" +
// nickname);
NicknameEntry entry = new NicknameEntry();
Nickname nicknameExtension = new Nickname();
nicknameExtension.setName(nickname);
entry.addExtension(nicknameExtension);
Login login = new Login();
login.setUserName(username);
entry.addExtension(login);
URL insertUrl = new URL(domainUrlBase + "nickname/" + SERVICE_VERSION);
NicknameEntry ne = nicknameService.insert(insertUrl, entry);
} catch (Exception e) {
throw ConnectorException.wrap(e);
}
}
/**
* Deletes a nickname.
*
* @param nickname
* The nickname you wish to delete.
* @throws AppsForYourDomainException
* If a Provisioning API specific occurs.
* @throws ServiceException
* If a generic GData framework error occurs.
* @throws IOException
* If an error occurs communicating with the GData service.
*/
public void deleteNickname(String nickname) {
try {
URL deleteUrl = new URL(domainUrlBase + "nickname/" + SERVICE_VERSION + "/" + nickname);
nicknameService.delete(deleteUrl);
} catch (Exception e) {
throw ConnectorException.wrap(e);
}
}
/**
* Creates a new user with an email account.
*
* @param entry
* - A user entry structure that represents the user account.
*
* @return A UserEntry object of the newly created user. service.
*/
public UserEntry createUser(UserEntry entry) {
try {
URL insertUrl = new URL(domainUrlBase + "user/" + SERVICE_VERSION);
return (UserEntry) userService.insert(insertUrl, entry);
} catch (AppsForYourDomainException e) {
if (e.getErrorCode() == AppsForYourDomainErrorCode.EntityExists) {
throw new AlreadyExistsException(e);
}
throw ConnectorException.wrap(e);
} catch (Exception e) {
// log.error("Got an exception trying to create a user: {0}",
// e.getMessage());
throw ConnectorException.wrap(e);
}
}
/**
* Update a google apps account.
*
*
*
* @param username
* - user (accountId) to update
* @param userEntry
* - the new values to update
* @return the modified UserEntry
*/
public UserEntry updateUser(String username, UserEntry userEntry) {
try {
URL updateUrl = new URL(domainUrlBase + "user/" + SERVICE_VERSION + "/" + username);
return (UserEntry) userService.update(updateUrl, userEntry);
} catch (Exception e) {
throw ConnectorException.wrap(e);
}
}
/**
* Deletes a user.
*
* @param username
* The user you wish to delete.
* @throws AppsForYourDomainException
* If a Provisioning API specific occurs.
* @throws ServiceException
* If a generic GData framework error occurs.
* @throws IOException
* If an error occurs communicating with the GData service.
*/
public void deleteUser(String username) {
try {
URL deleteUrl = new URL(domainUrlBase + "user/" + SERVICE_VERSION + "/" + username);
userService.delete(deleteUrl);
} catch (AppsForYourDomainException e) {
if (e.getErrorCode() == AppsForYourDomainErrorCode.EntityDoesNotExist) {
throw new UnknownUidException(new Uid(username), ObjectClass.ACCOUNT);
}
throw ConnectorException.wrap(e);
} catch (Exception e) {
throw ConnectorException.wrap(e);
}
}
/**
* This method is used to create a new User Entry object (if userEntry ==
* null), OR update an existing one (if userEntry is non null).
*
* Non null values are set in the userEntry object
*
* @param userEntry
* - Existing UserEntry object to be updated. If null a new entry
* is created
* @param username
* - The login identifier
* @param password
* - Password - in the clear.
* @param givenName
* - Persons first (given) name
* @param familyName
* = Persons last (family) name
* @param suspended
* - set to true if the account is suspended
* @param quotaLimitInMb
* - Users email quota limit in MB
*
* @return a new user entry object or an updated copy of the one passed in.
*/
public UserEntry setUserEntry(UserEntry userEntry, String username, String password,
String givenName, String familyName, boolean suspended, Integer quotaLimitInMb) {
if (userEntry == null) {
userEntry = new UserEntry();
}
Login login = userEntry.getLogin();
if (login == null) {
login = new Login();
}
if (username != null) {
login.setUserName(username);
}
if (password != null) {
login.setPassword(password);
}
login.setSuspended(suspended);
userEntry.setExtension(login);
Name name = userEntry.getName();
if (name == null) {
name = new Name();
}
if (givenName != null) {
name.setGivenName(givenName);
}
if (familyName != null) {
name.setFamilyName(familyName);
}
userEntry.setExtension(name);
if (quotaLimitInMb != null) {
Quota quota = new Quota();
quota.setLimit(quotaLimitInMb);
userEntry.setExtension(quota);
}
return userEntry;
}
/**
* Convenience method to dump a userentry objet to a string for debug
*
* @param ue
* userentry
* @return String repreneation of the user entry
*/
public static String userEntrytoString(UserEntry ue) {
StringBuffer sb = new StringBuffer("UserEntry:");
if (ue != null) {
Login login = ue.getLogin();
if (login != null) {
sb.append("Login=" + login.getUserName());
}
Name n = ue.getName();
if (n != null) {
sb.append(" Name=" + n.getGivenName() + " " + n.getFamilyName());
}
Quota q = ue.getQuota();
if (q != null) {
sb.append(" quota=" + q.getLimit());
}
}
return sb.toString();
}
// Group Operations
public Iterator getGroupIterator() {
try {
GenericFeed groupsFeed = groupService.retrieveAllGroups();
Iterator<GenericEntry> groupIterator = groupsFeed.getEntries().iterator();
return groupIterator;
} catch (Exception e) {
throw ConnectorException.wrap(e);
}
}
public GenericEntry getGroupEntry(String id) {
try {
return groupService.retrieveGroup(id);
} catch (AppsForYourDomainException e) {
if (e.getErrorCode() == AppsForYourDomainErrorCode.EntityDoesNotExist) {
// ok
return null;
}
throw ConnectorException.wrap(e);
} catch (Exception e) {
throw ConnectorException.wrap(e);
}
}
public void deleteGroup(String id) {
try {
groupService.deleteGroup(id);
} catch (AppsForYourDomainException e) {
if (e.getErrorCode() == AppsForYourDomainErrorCode.EntityDoesNotExist) {
throw new UnknownUidException(new Uid(id), ObjectClass.GROUP);
}
throw ConnectorException.wrap(e);
} catch (Exception e) {
throw ConnectorException.wrap(e);
}
}
public void addGroupMember(String groupId, String memberId) {
try {
groupService.addMemberToGroup(groupId, memberId);
} catch (Exception e) {
throw ConnectorException.wrap(e);
}
}
public void removeGroupMember(String groupId, String memberId) {
try {
groupService.deleteMemberFromGroup(groupId, memberId);
} catch (Exception e) {
throw ConnectorException.wrap(e);
}
}
public void createGroup(String id, String Name, String groupDescription, String emailPermission) {
try {
groupService.createGroup(id, Name, groupDescription, emailPermission);
} catch (AppsForYourDomainException e) {
if (e.getErrorCode() == AppsForYourDomainErrorCode.EntityExists) {
throw new AlreadyExistsException(e);
}
throw ConnectorException.wrap(e);
} catch (Exception e) {
throw ConnectorException.wrap(e);
}
}
public List<String> getMembersAsList(String id) {
List<String> members = new ArrayList<String>();
try {
GenericFeed groupsFeed = groupService.retrieveAllMembers(id);
Iterator<GenericEntry> groupsEntryIterator = groupsFeed.getEntries().iterator();
while (groupsEntryIterator.hasNext()) {
members.add(groupsEntryIterator.next().getProperty(
AppsGroupsService.APPS_PROP_GROUP_MEMBER_ID));
}
} catch (Exception e) {
throw ConnectorException.wrap(e);
}
return members;
}
public List<String> getOwnersAsList(String id) {
List<String> owners = new ArrayList<String>();
try {
GenericFeed groupsFeed = groupService.retreiveGroupOwners(id);
Iterator<GenericEntry> groupsEntryIterator = groupsFeed.getEntries().iterator();
while (groupsEntryIterator.hasNext()) {
owners.add(groupsEntryIterator.next().getProperty(
AppsGroupsService.APPS_PROP_GROUP_EMAIL));
}
} catch (Exception e) {
throw ConnectorException.wrap(e);
}
return owners;
}
}