com.identity4j.connector.office365.Office365Connector Maven / Gradle / Ivy
package com.identity4j.connector.office365;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.identity4j.connector.AbstractConnector;
import com.identity4j.connector.ConnectorCapability;
import com.identity4j.connector.ConnectorConfigurationParameters;
import com.identity4j.connector.WebAuthenticationAPI;
import com.identity4j.connector.exception.ConnectorException;
import com.identity4j.connector.exception.PrincipalAlreadyExistsException;
import com.identity4j.connector.exception.PrincipalNotFoundException;
import com.identity4j.connector.office365.entity.Group;
import com.identity4j.connector.office365.entity.Groups;
import com.identity4j.connector.office365.entity.User;
import com.identity4j.connector.office365.entity.Users;
import com.identity4j.connector.office365.services.Directory;
import com.identity4j.connector.principal.Identity;
import com.identity4j.connector.principal.Role;
import com.identity4j.util.CollectionUtil;
import com.identity4j.util.passwords.PasswordCharacteristics;
/**
* Office 365 connector makes use of Active Directory Graph REST API to perform
* admin operations. Connector enables CRUD operations on Users and can map them
* to Groups. Users and Groups are referred as Office365Identity and Role
* respectively in identity4j domain.
*
*
* To map properties of User not supported by Office365Identity we can make use
* attributes map.
*
*
* E.g.role.setAttribute("email", group.getEmail());
*
*
* Here we are using attribute map with key "email" to store email id which is
* not a property in role.
*
*
*
* The API can be referred from
* Active
* Directory Graph REST API
*
*
* @author gaurav
*
*/
public class Office365Connector extends AbstractConnector {
private final class FilterIterator implements Iterator {
private Identity current;
private Iterator source;
FilterIterator(Iterator source) {
this.source = source;
}
@Override
public boolean hasNext() {
checkNext();
return current != null;
}
@Override
public Identity next() {
checkNext();
if (current == null)
throw new NoSuchElementException();
try {
return current;
} finally {
current = null;
}
}
private void checkNext() {
if (current == null) {
while (true) {
if (source.hasNext()) {
current = source.next();
if (matches(current))
return;
else
// Try next user
current = null;
} else
return;
}
}
}
private boolean matches(Identity identity) {
boolean ok;
Set inc = configuration.getIncludedGroups();
Set exc = configuration.getExcludedGroups();
// Are all of the roles the user has included
ok = inc.isEmpty();
if (!ok) {
for (Role r : identity.getRoles()) {
for (String f : inc) {
if (matchesGroup(r.getPrincipalName(), f)) {
ok = true;
break;
}
}
if (ok)
break;
}
}
if (ok && !exc.isEmpty()) {
// Make sure none are excluded
for (Role r : identity.getRoles()) {
for (String f : exc) {
if (matchesGroup(r.getPrincipalName(), f)) {
ok = false;
break;
}
}
if (!ok)
break;
}
}
return ok;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}
private final class IdentityIterator implements Iterator {
private Users users;
private String nextLink;
private Iterator inner;
private User current;
private boolean eof;
@Override
public boolean hasNext() {
checkNext();
return current != null;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
@Override
public Identity next() {
checkNext();
if (current == null)
throw new NoSuchElementException();
try {
return Office365ModelConvertor.convertOffice365UserToOfficeIdentity(current);
} finally {
current = null;
}
}
private void checkNext() {
if (current != null)
// Already have an unconsumed user
return;
while (!eof && current == null) {
while (!eof && current == null) {
if (users == null) {
// Get the next batch
users = directory.users().all(nextLink);
nextLink = users.getNextLink();
inner = users.getUsers() == null ? null : users.getUsers().iterator();
}
if (inner != null && inner.hasNext()) {
break;
}
// Finished inner iterator,
users = null;
inner = null;
if (nextLink == null) {
eof = true;
// No more
break;
}
}
if (inner != null && inner.hasNext()) {
current = inner.next();
directory.users().probeGroupsAndRoles(current);
}
}
}
}
private Office365Configuration configuration;
private Directory directory;
private static final Log log = LogFactory.getLog(Office365Connector.class);
private boolean isDeletePrivilege;
static Set capabilities = new HashSet(
Arrays.asList(new ConnectorCapability[] { ConnectorCapability.passwordChange,
ConnectorCapability.passwordSet, ConnectorCapability.createUser, ConnectorCapability.deleteUser,
ConnectorCapability.updateUser, ConnectorCapability.hasFullName, ConnectorCapability.hasEmail,
ConnectorCapability.roles, ConnectorCapability.createRole, ConnectorCapability.deleteRole,
ConnectorCapability.updateRole, ConnectorCapability.webAuthentication,
ConnectorCapability.identities, ConnectorCapability.accountDisable }));
@Override
public Set getCapabilities() {
return capabilities;
}
@Override
public WebAuthenticationAPI> startAuthentication() throws ConnectorException {
return new Office365OAuth();
}
/**
* Check to see if connector is open
*/
@Override
public boolean isOpen() {
return directory != null;
}
/**
* Check to see connector if in read only mode
*/
@Override
public boolean isReadOnly() {
return !isDeletePrivilege;
}
@Override
public PasswordCharacteristics getPasswordCharacteristics() {
return Office365PasswordCharacteristics.getInstance();
}
/**
*
* Retrieves all the roles (groups) present in the active directory.
* Note: Role in active directory is referred as Groups and
* identified by only guid, not group name.
* Refer groups by guid as all operations on groups are performed using
* only guid.
* Please refer
* Groups
* .
*
*
* @throws ConnectorException
* for api, connection related errors.
*/
@Override
public Iterator allRoles() throws ConnectorException {
Groups groups = directory.groups().all();
List roles = new ArrayList();
Set inc = configuration.getIncludedGroups();
Set exc = configuration.getExcludedGroups();
for (Group group : groups.getGroups()) {
if ((inc.isEmpty() || matchesGroups(group, inc)) && (exc.isEmpty() || !(matchesGroups(group, exc))))
roles.add(Office365ModelConvertor.groupToRole(group));
}
return roles.iterator();
}
/**
*
* Creates a role in the active directory.
* Note: Role in active directory is referred as Groups and
* identified by only guid, not group name.
* Refer groups by guid as all operations on groups are performed using
* only guid.
* Please refer
* Groups
* .
*
*
* @throws PrincipalAlreadyExistsException
* if role with same email id/principal already present in
* active directory.
* @throws ConnectorException
* for api, connection related errors.
*/
@Override
public Role createRole(Role role) throws ConnectorException {
// we have to check this REST api does not throws exception for same
// principal name and we cannot fetch role by name
if (isRolePresent(role.getPrincipalName())) {
throw new PrincipalAlreadyExistsException(
"Principal contains conflicting properties which already exists, " + role.getPrincipalName());
}
Group group = Office365ModelConvertor.roleToGroup(role);
return Office365ModelConvertor.groupToRole(directory.groups().save(group));
}
/**
*
* Updates a role in the active directory with specified changes.
* Note: Role in active directory is referred as Groups and
* identified by only guid, not group name.
* Refer groups by guid as all operations on groups are performed using
* only guid.
* Please refer
* Groups
* .
*
*
* @throws PrincipalNotFoundException
* if role specified by email id/principal name not present in
* active directory.
* @throws ConnectorException
* for api, connection related errors or delete privilege not
* given to service id.
*/
@Override
public void updateRole(Role role) throws ConnectorException {
if (isReadOnly()) {
throw new ConnectorException(
"This directory is read only because the service account does not have sufficient privileges to perform all required operations");
}
Group group = Office365ModelConvertor.roleToGroup(role);
directory.groups().update(group);
}
/**
*
* Deletes a role in the active directory with specified principal name.
*
* Note: Role in active directory is referred as Groups and
* identified by only guid, not group name.
* Refer groups by guid as all operations on groups are performed using
* only guid.
* Please refer
* Groups
* .
*
*
* @throws PrincipalNotFoundException
* if role specified by email id/principal name not present in
* active directory.
* @throws ConnectorException
* for api, connection related errors or delete privilege not
* given to service id.
*/
@Override
public void deleteRole(String guid) throws ConnectorException {
if (isReadOnly()) {
throw new ConnectorException(
"This directory is read only because the service account does not have sufficient privileges to perform all required operations");
}
Role role = getRoleByName(guid);
directory.groups().delete(role.getGuid());
}
/**
*
* Finds a role in the active directory with specified principal name.
* Note: Role in active directory is referred as Groups and
* identified by only guid, not group name.
* Refer groups by guid as all operations on groups are performed using
* only guid.
* Please refer
* Groups
* .
*
*
* @throws PrincipalNotFoundException
* if role specified by email id/principal name not present in
* active directory.
* @throws ConnectorException
* for api, connection related errors.
*/
@Override
public Role getRoleByName(String guid) throws PrincipalNotFoundException, ConnectorException {
Group group = directory.groups().get(guid);
return Office365ModelConvertor.groupToRole(group);
}
/**
*
* Finds all identities present in active directory.
*
* The extra attributes supported by graph e.g. department, postalCode are
* populated in identites's attributes map.
*
*
* identity.getAttribute("department")
*
*
*
*
* Please refer
* Groups
* and
* User
*
*
*
* @throws ConnectorException
* for api, connection related errors.
*
* @return iterator with all identities.
*/
@Override
public Iterator allIdentities() throws ConnectorException {
return isGroupFilterInUse() ? new FilterIterator(new IdentityIterator()) : new IdentityIterator();
}
/**
*
* Finds an identity by principal/email id supplied.
*
* The extra attributes supported by graph e.g. department, postalCode are
* populated in identites's attributes map.
*
*
* identity.getAttribute("department")
*
*
*
*
* Please refer
* Groups
* and
* User
*
*
* @throws PrincipalNotFoundException
* if identity specified by email id/principal name not present
* in active directory.
* @throws ConnectorException
* for api, connection related errors.
*
* @return Identity instance found by the specified email id/principal name.
*/
@Override
public Identity getIdentityByName(String name) throws PrincipalNotFoundException, ConnectorException {
User user = directory.users().get(name);
return Office365ModelConvertor.convertOffice365UserToOfficeIdentity(user);
}
/**
*
* Deletes an identity in active directory. >
*
*
* Please refer
* Groups
* and
* User
*
*
* @throws PrincipalNotFoundException
* if identity specified by email id/principal name not present
* in active directory.
* @throws ConnectorException
* for api, connection related errors or delete privilege not
* given to service id.
*/
@Override
public void deleteIdentity(String principalName) throws ConnectorException {
if (isReadOnly()) {
throw new ConnectorException(
"This directory is read only because the service account does not have sufficient privileges to perform all required operations");
}
directory.users().delete(principalName);
}
/**
*
* Creates identity with specified roles and password provided. Role in
* Graph api is known as Group and is identified by unique guid. Role guid
* is auto generated number from Graph API.
*
*
*
* Please refer
* Groups
* and
* User
*
*
*
* The extra attributes supported by graph e.g. department, postalCode are
* populated in identites's attributes map.
*
*
* identity.getAttribute("department")
*
*
*
* @throws PrincipalAlreadyExistsException
* if identity with same email id/principal already present in
* active directory.
* @throws ConnectorException
* for api, connection related errors.
*
* @return Identity instance with values specified for creation.
*/
@Override
public Identity createIdentity(Identity identity, char[] password) throws ConnectorException {
User user = Office365ModelConvertor.covertOfficeIdentityToOffice365User(identity);
List groups = user.getMemberOf();
user.setMemberOf(null);// as groups will be saved independent from User
user.getPasswordProfile().setForceChangePasswordNextLogin(false);
user.getPasswordProfile().setPassword(new String(password));
Identity identitySaved = Office365ModelConvertor
.convertOffice365UserToOfficeIdentity(directory.users().save(user));
for (Group group : groups) {
directory.groups().addUserToGroup(identitySaved.getGuid(), group.getObjectId());
}
identitySaved.setRoles(identity.getRoles());
return identitySaved;
}
/**
*
* Updates an identity in active directory. To update extra attributes
* supported by active directory, use attributes map.
*
*
* e.g.identity.setAttribute("department", "engineering")
*
*
*
*
* Please refer
* Groups
* and
* User
*
*
* @throws PrincipalNotFoundException
* if identity specified by email id/principal name not present
* in active directory.
* @throws ConnectorException
* for api, connection related errors or delete privilege not
* given to service id.
*/
@Override
public void updateIdentity(Identity identity) throws ConnectorException {
if (isReadOnly()) {
throw new ConnectorException(
"This directory is read only because the service account does not have sufficient privileges to perform all required operations");
}
User user = Office365ModelConvertor.covertOfficeIdentityToOffice365User(identity);
user.setMemberOf(null);// will be updated individually not along with user
directory.users().update(user);
adjustAdditionRemovalOfRoleOnIdentityUpdate(identity);
}
/**
* Disables/Suspends an account in active directory.
*
* @throws PrincipalNotFoundException
* if identity specified by email id/principal name not present
* in active directory.
* @throws ConnectorException
* for api, connection related errors.
*/
@Override
public void disableIdentity(Identity identity) {
identitySuspensionHelper(identity, true);
identity.getAccountStatus().setDisabled(true);
}
/**
* Enables an account in active directory.
*
* @throws PrincipalNotFoundException
* if identity specified by email id/principal name not present
* in active directory.
* @throws ConnectorException
* for api, connection related errors.
*/
@Override
public void enableIdentity(Identity identity) {
identitySuspensionHelper(identity, false);
identity.getAccountStatus().setDisabled(false);
}
@Override
protected boolean areCredentialsValid(Identity identity, char[] password) throws ConnectorException {
throw new UnsupportedOperationException(
"Standard credential validation is not supported, web authentication must be used.");
}
/**
* Changes the password of the identity specified by email id/principal.
* Also provides provision to force change password in next logon attempt.
*
*
* @throws ConnectorException
* for api, connection related errors.
*/
@Override
protected void setPassword(Identity identity, char[] password, boolean forcePasswordChangeAtLogon,
PasswordResetType type) throws ConnectorException {
User user = new User();
user.getPasswordProfile().setPassword(new String(password));
user.getPasswordProfile().setForceChangePasswordNextLogin(forcePasswordChangeAtLogon);
user.setObjectId(identity.getGuid());
directory.users().update(user);
}
/**
* Helper utility method to enable/disable suspension for an identity
*
* @param identity
* @param suspension
* true to suspend an account, false to enable it
*
* @throws PrincipalNotFoundException
* if identity specified by email id/principal name not present
* in active directory.
* @throws ConnectorException
* for api, connection related errors.
*/
private void identitySuspensionHelper(Identity identity, boolean suspension) {
User user = new User();
user.setAccountEnabled(!suspension);
user.setObjectId(identity.getGuid());
directory.users().update(user);
}
/**
*
* Creates directory instance for remote method invocations using Active
* Directory Graph REST API. The directory instance is created by providing
* and enabling
*
* - Giving read,write and delete access to service id, it should have
* role similar to "User Account Administrator" in the active directory.
*
* - Secret key of the service id.
* - Enabling API Access
*
*
*/
@Override
protected void onOpen(ConnectorConfigurationParameters parameters) throws ConnectorException {
configuration = (Office365Configuration) parameters;
directory = new Directory();
log.info("Directory instance created.");
try {
directory.init(configuration);
isDeletePrivilege = directory.users().isDeletePrivilege(configuration.getAppPrincipalObjectId(),
configuration.getAppDeletePrincipalRole());
log.info("Delete privilege found as " + isDeletePrivilege);
} catch (IOException e) {
throw new ConnectorException(e.getMessage(), e);
}
}
/**
* Helper utility method to adjust addition and removal of roles from an
* identity. It compares the roles currently assigned and new set of roles
* sent and finds which are to be added and which are to be removed and
* accordingly performs removal or addition action.
*
* @param identity
*
* @throws ConnectorException
* for api, connection related errors.
*/
private void adjustAdditionRemovalOfRoleOnIdentityUpdate(Identity identity) {
try {
Identity identityFromSource = getIdentityByName(identity.getPrincipalName());
Set rolesCurrentlyAssigned = new HashSet(Arrays.asList(identityFromSource.getRoles()));
Set rolesToBeAssigned = new HashSet(Arrays.asList(identity.getRoles()));
Collection newRolesToAdd = CollectionUtil.objectsNotPresentInProbeCollection(rolesToBeAssigned,
rolesCurrentlyAssigned);
Collection rolesToRemove = CollectionUtil.objectsNotPresentInProbeCollection(rolesCurrentlyAssigned,
rolesToBeAssigned);
for (Role role : newRolesToAdd) {
addRoleToUser(role.getGuid(), identity.getGuid());
}
for (Role role : rolesToRemove) {
removeRoleFromUser(role.getGuid(), identity.getGuid());
}
} catch (Exception e) {
log.error("Problem in adjusting roles " + e.getMessage(), e);
throw new ConnectorException(e.getMessage(), e);
}
}
/**
* Removes a Role(Group) from an identity.
* Refer groups by guid as all operations on groups are performed using
* only guid.
* Please refer
* Groups
* .
*
*
* @param roleName
* @param principal
*
* @throws ConnectorException
* for api, connection related errors.
*/
private void removeRoleFromUser(String guidRole, String guidUser) {
directory.groups().removeUserFromGroup(guidUser, guidRole);
}
/**
* Adds a Role(Group) to an identity.
* Refer groups by guid as all operations on groups are performed using
* only guid.
* Please refer
* Groups
* .
*
*
* @param roleName
* @param principal
*
* @throws ConnectorException
* for api, connection related errors.
*/
private void addRoleToUser(String guidRole, String guidUser) {
directory.groups().addUserToGroup(guidUser, guidRole);
}
/**
* Helper utility method which checks the presence of a Role in list of
* Roles
*
* @param roleName
* @return true if role is present else false
*/
private boolean isRolePresent(String roleName) {
Iterator roles = allRoles();
Role role = null;
while (roles.hasNext()) {
role = roles.next();
if (role.getPrincipalName().equals(roleName)) {
return true;
}
}
return false;
}
private boolean isGroupFilterInUse() {
return !configuration.getIncludedGroups().isEmpty() || !configuration.getExcludedGroups().isEmpty();
}
private boolean matchesGroups(Group group, Set groups) {
for (String g : groups) {
if (matchesGroup(group.getObjectId(), g)) {
return true;
}
}
return false;
}
private boolean matchesGroup(String groupName, String g) {
return g.equalsIgnoreCase(groupName);
}
}