
org.cloudfoundry.identity.uaa.scim.jdbc.JdbcScimGroupMembershipManager Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of cloudfoundry-identity-scim Show documentation
Show all versions of cloudfoundry-identity-scim Show documentation
Cloud Foundry User Account and Authentication
/*******************************************************************************
* Cloud Foundry
* Copyright (c) [2009-2014] Pivotal Software, Inc. All Rights Reserved.
*
* This product is licensed to you under the Apache License, Version 2.0 (the "License").
* You may not use this product except in compliance with the License.
*
* This product includes a number of subcomponents with
* separate copyright notices and license terms. Your use of these
* subcomponents is subject to the terms and conditions of the
* subcomponent's license, as noted in the LICENSE file.
*******************************************************************************/
package org.cloudfoundry.identity.uaa.scim.jdbc;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.cloudfoundry.identity.uaa.rest.jdbc.AbstractQueryable;
import org.cloudfoundry.identity.uaa.rest.jdbc.JdbcPagingListFactory;
import org.cloudfoundry.identity.uaa.rest.jdbc.SearchQueryConverter;
import org.cloudfoundry.identity.uaa.scim.ScimGroup;
import org.cloudfoundry.identity.uaa.scim.ScimGroupMember;
import org.cloudfoundry.identity.uaa.scim.ScimGroupMembershipManager;
import org.cloudfoundry.identity.uaa.scim.ScimGroupProvisioning;
import org.cloudfoundry.identity.uaa.scim.ScimUserProvisioning;
import org.cloudfoundry.identity.uaa.scim.exception.InvalidScimResourceException;
import org.cloudfoundry.identity.uaa.scim.exception.MemberAlreadyExistsException;
import org.cloudfoundry.identity.uaa.scim.exception.MemberNotFoundException;
import org.cloudfoundry.identity.uaa.scim.exception.ScimResourceConstraintFailedException;
import org.cloudfoundry.identity.uaa.scim.exception.ScimResourceNotFoundException;
import org.cloudfoundry.identity.uaa.zone.IdentityZone;
import org.cloudfoundry.identity.uaa.zone.IdentityZoneHolder;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementSetter;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.SingleColumnRowMapper;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
public class JdbcScimGroupMembershipManager extends AbstractQueryable implements ScimGroupMembershipManager {
private JdbcTemplate jdbcTemplate;
private final Log logger = LogFactory.getLog(getClass());
public static final String MEMBERSHIP_FIELDS = "group_id,member_id,member_type,authorities,added,origin";
public static final String MEMBERSHIP_TABLE = "group_membership";
public static final String ADD_MEMBER_SQL = String.format("insert into %s ( %s ) values (?,?,?,?,?,?)", MEMBERSHIP_TABLE, MEMBERSHIP_FIELDS);
public static final String UPDATE_MEMBER_SQL = String.format("update %s set authorities=? where group_id=? and member_id=?", MEMBERSHIP_TABLE);
public static final String GET_MEMBERS_FILTER_SQL = String.format("select %s from %s where group_id in (select id from groups where identity_zone_id=%s)", MEMBERSHIP_FIELDS, MEMBERSHIP_TABLE, "'%s'");
public static final String GET_MEMBERS_SQL = String.format("select %s from %s where group_id in (select id from groups where id=? and identity_zone_id=?)", MEMBERSHIP_FIELDS, MEMBERSHIP_TABLE);
public static final String GET_GROUPS_BY_MEMBER_SQL = String.format("select distinct(group_id) from %s where member_id=? and group_id in (select id from groups where identity_zone_id=?)", MEMBERSHIP_TABLE);
public static final String GET_MEMBERS_WITH_AUTHORITY_SQL = String.format("select %s from %s where group_id=? and lower(authorities) like ?", MEMBERSHIP_FIELDS,MEMBERSHIP_TABLE);
public static final String GET_MEMBER_SQL = String.format("select %s from %s where member_id=? and group_id in (select id from groups where id=? and identity_zone_id=?)",MEMBERSHIP_FIELDS, MEMBERSHIP_TABLE);
public static final String DELETE_MEMBER_SQL = String.format("delete from %s where member_id=? and group_id in (select id from groups where id=? and identity_zone_id=?)",MEMBERSHIP_TABLE);
public static final String DELETE_MEMBERS_IN_GROUP_SQL = String.format("delete from %s where group_id in (select id from groups where id=? and identity_zone_id=?)",MEMBERSHIP_TABLE);
public static final String DELETE_MEMBER_IN_GROUPS_SQL_USER = String.format("delete from %s where member_id in (select id from users where id=? and identity_zone_id=?)",MEMBERSHIP_TABLE);
public static final String DELETE_MEMBER_IN_GROUPS_SQL_GROUP = String.format("delete from %s where member_id in (select id from groups where id=? and identity_zone_id=?)",MEMBERSHIP_TABLE);
private final RowMapper rowMapper = new ScimGroupMemberRowMapper();
private ScimUserProvisioning userProvisioning;
private ScimGroupProvisioning groupProvisioning;
private Map> defaultUserGroups = new ConcurrentHashMap<>();
//we do not yet support default user groups for other zones
public void setDefaultUserGroups(Set groupNames) {
Set usergroups = new HashSet<>();
for (String name : groupNames) {
List g = groupProvisioning.query(String.format("displayName co \"%s\" and identity_zone_id eq \""+IdentityZone.getUaa().getId()+"\"", name));
if (!g.isEmpty()) {
usergroups.add(g.get(0));
} else { // default group must exist, hence if not already present,
// create it
usergroups.add(groupProvisioning.create(new ScimGroup(null, name, IdentityZone.getUaa().getId())));
}
}
defaultUserGroups.put(IdentityZone.getUaa(), usergroups);
}
public Set getDefaultUserGroups(IdentityZone zone) {
Set groups = defaultUserGroups.get(zone);
if (groups==null) {
return Collections.EMPTY_SET;
}
return groups;
}
public void setScimUserProvisioning(ScimUserProvisioning userProvisioning) {
this.userProvisioning = userProvisioning;
}
public void setScimGroupProvisioning(ScimGroupProvisioning groupProvisioning) {
this.groupProvisioning = groupProvisioning;
}
public JdbcScimGroupMembershipManager(JdbcTemplate jdbcTemplate, JdbcPagingListFactory pagingListFactory) {
super(jdbcTemplate,pagingListFactory,new ScimGroupMemberRowMapper());
Assert.notNull(jdbcTemplate);
this.jdbcTemplate = jdbcTemplate;
}
@Override
protected String getBaseSqlQuery() {
return String.format(GET_MEMBERS_FILTER_SQL, IdentityZoneHolder.get().getId());
}
@Override
protected String getTableName() {
return MEMBERSHIP_TABLE;
}
@Override
public int delete(String filter) {
SearchQueryConverter.ProcessedFilter where = getQueryConverter().convert(filter, null, false);
logger.debug("Filtering groups with SQL: " + where);
try {
String completeSql = "DELETE FROM "+getTableName() + " WHERE group_id IN (SELECT id FROM groups WHERE identity_zone_id='"+IdentityZoneHolder.get().getId()+"') AND " + where.getSql();
logger.debug("delete sql: " + completeSql + ", params: " + where.getParams());
return new NamedParameterJdbcTemplate(jdbcTemplate).update(completeSql, where.getParams());
} catch (DataAccessException e) {
logger.debug("Filter '" + filter + "' generated invalid SQL", e);
throw new IllegalArgumentException("Invalid delete filter: " + filter);
}
}
@Override
public List query(String filter) {
return super.query(filter);
}
@Override
public List query(String filter, String sortBy, boolean ascending) {
return super.query(filter, sortBy, ascending);
}
@Override
protected String getQuerySQL(String filter, SearchQueryConverter.ProcessedFilter where) {
boolean containsWhereClause = getBaseSqlQuery().contains(" where ");
return filter == null || filter.trim().length()==0 ?
getBaseSqlQuery() :
getBaseSqlQuery() + (containsWhereClause ? " and " : " where ") + where.getSql();
}
public boolean isDefaultGroup(String groupId) {
for (ScimGroup g : getDefaultUserGroups(IdentityZoneHolder.get())) {
if (g.getId().equals(groupId)) {
return true;
}
}
return false;
}
@Override
public ScimGroupMember addMember(final String groupId, final ScimGroupMember member)
throws ScimResourceNotFoundException, MemberAlreadyExistsException {
if (isDefaultGroup(groupId)) {
throw new MemberAlreadyExistsException("Trying to add member to default group");
}
// first validate the supplied groupId, memberId
validateRequest(groupId, member);
final String authorities = getGroupAuthorities(member);
final String type = (member.getType() == null ? ScimGroupMember.Type.USER : member.getType()).toString();
try {
logger.debug("Associating group:"+groupId+" with member:"+member);
jdbcTemplate.update(ADD_MEMBER_SQL, new PreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps) throws SQLException {
ps.setString(1, groupId);
ps.setString(2, member.getMemberId());
ps.setString(3, type);
ps.setString(4, authorities);
ps.setTimestamp(5, new Timestamp(new Date().getTime()));
ps.setString(6, member.getOrigin());
}
});
} catch (DuplicateKeyException e) {
throw new MemberAlreadyExistsException(member.getMemberId() + " is already part of the group: " + groupId);
}
return getMemberById(groupId, member.getMemberId());
}
@Override
public List getMembers(final String groupId) throws ScimResourceNotFoundException {
List result = jdbcTemplate.query(GET_MEMBERS_SQL, new PreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps) throws SQLException {
ps.setString(1, groupId);
ps.setString(2, IdentityZoneHolder.get().getId());
}
}, rowMapper);
return result;
}
@Override
public Set getGroupsWithMember(final String memberId, boolean transitive)
throws ScimResourceNotFoundException {
List results = new ArrayList<>();
getGroupsWithMember(results, memberId, transitive);
if (isUser(memberId)) {
results.addAll(getDefaultUserGroups(IdentityZoneHolder.get()));
}
return new HashSet<>(results);
}
private void getGroupsWithMember(List results, final String memberId, boolean transitive) {
if (results == null) {
return;
}
List groupIds;
try {
groupIds = jdbcTemplate.query(GET_GROUPS_BY_MEMBER_SQL, new PreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps) throws SQLException {
ps.setString(1, memberId);
ps.setString(2, IdentityZoneHolder.get().getId());
}
}, new SingleColumnRowMapper<>(String.class));
} catch (EmptyResultDataAccessException ex) {
groupIds = Collections.EMPTY_LIST;
}
for (String groupId : groupIds) {
ScimGroup group;
try {
group = groupProvisioning.retrieve(groupId);
} catch (ScimResourceNotFoundException ex) {
continue;
}
if (!results.contains(group)) { // to ensure we don't go into
// infinite recursion caused by
// nested group cycles
results.add(group);
if (transitive) {
getGroupsWithMember(results, groupId, transitive);
}
}
}
}
@Override
public List getMembers(final String groupId, final ScimGroupMember.Role permission)
throws ScimResourceNotFoundException {
logger.debug("getting members of type: " + permission + " from group: " + groupId);
List members = new ArrayList();
members.addAll(jdbcTemplate.query(GET_MEMBERS_WITH_AUTHORITY_SQL, new PreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps) throws SQLException {
ps.setString(1, groupId);
ps.setString(2, "%" + permission.toString().toLowerCase() + "%");
}
}, rowMapper)
);
return members;
}
@Override
public ScimGroupMember getMemberById(String groupId, String memberId) throws ScimResourceNotFoundException,
MemberNotFoundException {
try {
ScimGroupMember u = jdbcTemplate.queryForObject(GET_MEMBER_SQL, rowMapper, memberId, groupId, IdentityZoneHolder.get().getId());
return u;
} catch (EmptyResultDataAccessException e) {
throw new MemberNotFoundException("Member " + memberId + " does not exist in group " + groupId);
}
}
@Override
public ScimGroupMember updateMember(final String groupId, final ScimGroupMember member)
throws ScimResourceNotFoundException, MemberNotFoundException {
validateRequest(groupId, member);
final String authorities = getGroupAuthorities(member);
int updated = jdbcTemplate.update(UPDATE_MEMBER_SQL, new PreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps) throws SQLException {
ps.setString(1, authorities);
ps.setString(2, groupId);
ps.setString(3, member.getMemberId());
}
});
if (updated != 1) {
throw new IncorrectResultSizeDataAccessException("unexpected number of members updated", 1, updated);
}
return getMemberById(groupId, member.getMemberId());
}
@Override
public List updateOrAddMembers(String groupId, List members)
throws ScimResourceNotFoundException {
List currentMembers = getMembers(groupId);
logger.debug("current-members: " + currentMembers + ", in request: " + members);
List currentMembersToRemove = new ArrayList<>(currentMembers);
currentMembersToRemove.removeAll(members);
logger.debug("removing members: " + currentMembersToRemove);
for (ScimGroupMember member : currentMembersToRemove) {
removeMemberById(groupId, member.getMemberId());
}
List newMembersToAdd = new ArrayList<>(members);
newMembersToAdd.removeAll(currentMembers);
logger.debug("adding new members: " + newMembersToAdd);
for (ScimGroupMember member : newMembersToAdd) {
addMember(groupId, member);
}
List membersToUpdate = new ArrayList<>(members);
membersToUpdate.retainAll(currentMembers);
logger.debug("updating members: " + membersToUpdate);
for (ScimGroupMember member : membersToUpdate) {
updateMember(groupId, member);
}
return getMembers(groupId);
}
@Override
public ScimGroupMember removeMemberById(final String groupId, final String memberId)
throws ScimResourceNotFoundException, MemberNotFoundException {
ScimGroupMember member = getMemberById(groupId, memberId);
int deleted = jdbcTemplate.update(DELETE_MEMBER_SQL, new PreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps) throws SQLException {
ps.setString(2, groupId);
ps.setString(1, memberId);
ps.setString(3, IdentityZoneHolder.get().getId());
}
});
if (deleted != 1) {
throw new IncorrectResultSizeDataAccessException("unexpected number of members removed", 1, deleted);
}
return member;
}
@Override
public List removeMembersByGroupId(final String groupId) throws ScimResourceNotFoundException {
List members = getMembers(groupId);
logger.debug("removing " + members + " members from group: " + groupId);
int deleted = jdbcTemplate.update(DELETE_MEMBERS_IN_GROUP_SQL, new PreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps) throws SQLException {
ps.setString(1, groupId);
ps.setString(2, IdentityZoneHolder.get().getId());
}
});
if (deleted != members.size()) {
throw new IncorrectResultSizeDataAccessException("unexpected number of members removed", members.size(),
deleted);
}
return members;
}
@Override
public Set removeMembersByMemberId(final String memberId) throws ScimResourceNotFoundException {
Set groups = getGroupsWithMember(memberId, false);
logger.debug("removing " + memberId + " from groups: " + groups);
int deleted = 0;
String sql = DELETE_MEMBER_IN_GROUPS_SQL_GROUP;
if (isUser(memberId)) {
sql = DELETE_MEMBER_IN_GROUPS_SQL_USER;
}
deleted = jdbcTemplate.update(sql, new PreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps) throws SQLException {
ps.setString(1, memberId);
ps.setString(2, IdentityZoneHolder.get().getId());
}
});
int expectedDelete = isUser(memberId) ? groups.size() - getDefaultUserGroups(IdentityZoneHolder.get()).size() : groups.size();
if (deleted != expectedDelete) {
throw new IncorrectResultSizeDataAccessException("unexpected number of members removed", expectedDelete,
deleted);
}
return groups;
}
private boolean isUser(String uuid) {
try {
userProvisioning.retrieve(uuid);
return true;
} catch (ScimResourceNotFoundException ex) {
return false;
}
}
private void validateRequest(String groupId, ScimGroupMember member) {
if (!StringUtils.hasText(groupId) ||
!StringUtils.hasText(member.getMemberId()) ||
!StringUtils.hasText(member.getOrigin())) {
throw new InvalidScimResourceException("group-id, member-id, origin and member-type must be non-empty");
}
if (groupId.equals(member.getMemberId())) { // oops! cycle detected
throw new InvalidScimResourceException("trying to nest group within itself, aborting");
}
// check if the group exists and the member-id is a valid group or user
// id
ScimGroup group = groupProvisioning.retrieve(groupId); // this will throw a ScimException
String memberZoneId;
// if the group does not exist
// this will throw a ScimException if the group or user does not exist
if (member.getType() == ScimGroupMember.Type.GROUP) {
memberZoneId = groupProvisioning.retrieve(member.getMemberId()).getZoneId();
} else {
memberZoneId = userProvisioning.retrieve(member.getMemberId()).getZoneId();
}
if (!memberZoneId.equals(group.getZoneId())) {
throw new ScimResourceConstraintFailedException("The zone of the group and the member must be the same.");
}
if (!memberZoneId.equals(IdentityZoneHolder.get().getId())) {
throw new ScimResourceConstraintFailedException("Unable to make membership changes in a different zone");
}
}
private String getGroupAuthorities(ScimGroupMember member) {
if (member.getRoles() != null && !member.getRoles().isEmpty()) {
return StringUtils.collectionToCommaDelimitedString(member.getRoles());
} else {
return StringUtils.collectionToCommaDelimitedString(ScimGroupMember.GROUP_MEMBER);
}
}
private static final class ScimGroupMemberRowMapper implements RowMapper {
@Override
public ScimGroupMember mapRow(ResultSet rs, int rowNum) throws SQLException {
String memberId = rs.getString(2);
String memberType = rs.getString(3);
String authorities = rs.getString(4);
Date added = rs.getDate(5);
String origin = rs.getString(6);
ScimGroupMember sgm = new ScimGroupMember(memberId, ScimGroupMember.Type.valueOf(memberType), getAuthorities(authorities));
sgm.setOrigin(origin);
return sgm;
}
private List getAuthorities(String authorities) {
List result = new ArrayList();
for (String a : authorities.split(",")) {
// for temporary backwards compatibility
if ("read".equalsIgnoreCase(a)) {
a = "reader";
} else if ("write".equalsIgnoreCase(a)) {
a = "writer";
}
result.add(ScimGroupMember.Role.valueOf(a.toUpperCase()));
}
return result;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy