/*******************************************************************************
* Cloud Foundry
* Copyright (c) [2009-2016] 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 org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.cloudfoundry.identity.uaa.audit.AuditEventType;
import org.cloudfoundry.identity.uaa.audit.event.AbstractUaaEvent;
import org.cloudfoundry.identity.uaa.audit.event.SystemDeletable;
import org.cloudfoundry.identity.uaa.resources.jdbc.AbstractQueryable;
import org.cloudfoundry.identity.uaa.resources.jdbc.JdbcPagingListFactory;
import org.cloudfoundry.identity.uaa.scim.ScimGroup;
import org.cloudfoundry.identity.uaa.scim.ScimGroupProvisioning;
import org.cloudfoundry.identity.uaa.scim.ScimMeta;
import org.cloudfoundry.identity.uaa.scim.exception.InvalidScimResourceException;
import org.cloudfoundry.identity.uaa.scim.exception.ScimResourceAlreadyExistsException;
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.cloudfoundry.identity.uaa.zone.event.IdentityZoneModifiedEvent;
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.util.Assert;
import org.springframework.util.StringUtils;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import static org.cloudfoundry.identity.uaa.zone.ZoneManagementScopes.getSystemScopes;
public class JdbcScimGroupProvisioning extends AbstractQueryable<ScimGroup>
implements ScimGroupProvisioning, SystemDeletable {
private JdbcScimGroupExternalMembershipManager externalGroupMappingManager;
private JdbcTemplate jdbcTemplate;
private JdbcScimGroupMembershipManager membershipManager;
private final Log logger = LogFactory.getLog(getClass());
@Override
public Log getLogger() {
return logger;
}
public static final String GROUP_FIELDS = "id,displayName,description,created,lastModified,version,identity_zone_id";
public static final String GROUP_TABLE = "groups";
public static final String GROUP_MEMBERSHIP_TABLE = "group_membership";
public static final String EXTERNAL_GROUP_TABLE = "external_group_mapping";
public static final String ADD_GROUP_SQL = String.format("insert into %s ( %s ) values (?,?,?,?,?,?,?)",
GROUP_TABLE,
GROUP_FIELDS);
public static final String UPDATE_GROUP_SQL = String.format(
"update %s set version=?, displayName=?, description=?, lastModified=? where id=? and version=? and identity_zone_id=?", GROUP_TABLE);
public static final String GET_GROUP_SQL = String.format("select %s from %s where id=? and identity_zone_id=?", GROUP_FIELDS, GROUP_TABLE);
public static final String ALL_GROUPS = String.format("select %s from %s", GROUP_FIELDS, GROUP_TABLE);
public static final String DELETE_GROUP_SQL = String.format("delete from %s where id=? and identity_zone_id=?", GROUP_TABLE);
public static final String DELETE_GROUP_BY_ZONE = String.format("delete from %s where identity_zone_id=?", GROUP_TABLE);
//TODO
public static final String DELETE_GROUP_MEMBERSHIP_BY_ZONE = String.format("delete from %s where group_id in (select id from %s where identity_zone_id = ?)", GROUP_MEMBERSHIP_TABLE, GROUP_TABLE);
//TODO
public static final String DELETE_EXTERNAL_GROUP_BY_ZONE = String.format("delete from %s where group_id in (select id from %s where identity_zone_id = ?)", EXTERNAL_GROUP_TABLE, GROUP_TABLE);
//TODO
public static final String DELETE_ZONE_ADMIN_MEMBERSHIP_BY_ZONE = String.format("delete from %s where group_id in (select id from %s where identity_zone_id=? and displayName like ?)", GROUP_MEMBERSHIP_TABLE, GROUP_TABLE);
//TODO
public static final String DELETE_ZONE_ADMIN_GROUPS_BY_ZONE = String.format("delete from %s where identity_zone_id=? and displayName like ?", GROUP_TABLE);
//TODO
public static final String DELETE_GROUP_MEMBERSHIP_BY_PROVIDER = String.format("delete from %s where group_id in (select id from %s where identity_zone_id = ?) and origin = ?", GROUP_MEMBERSHIP_TABLE, GROUP_TABLE);
//TODO
public static final String DELETE_EXTERNAL_GROUP_BY_PROVIDER = String.format("delete from %s where group_id in (select id from %s where identity_zone_id = ?) and origin = ?", EXTERNAL_GROUP_TABLE, GROUP_TABLE);
//TODO
public static final String DELETE_MEMBER_SQL = String.format("delete from %s where member_id=? and member_id in (select id from users where id=? and identity_zone_id=?)",GROUP_MEMBERSHIP_TABLE);
private final RowMapper<ScimGroup> rowMapper = new ScimGroupRowMapper();
public JdbcScimGroupProvisioning(JdbcTemplate jdbcTemplate, JdbcPagingListFactory pagingListFactory) {
super(jdbcTemplate, pagingListFactory, new ScimGroupRowMapper());
this.membershipManager = new JdbcScimGroupMembershipManager(jdbcTemplate, pagingListFactory);
this.membershipManager.setScimGroupProvisioning(this);
this.externalGroupMappingManager = new JdbcScimGroupExternalMembershipManager(jdbcTemplate, pagingListFactory);
this.externalGroupMappingManager.setScimGroupProvisioning(this);
Assert.notNull(jdbcTemplate);
this.jdbcTemplate = jdbcTemplate;
setQueryConverter(new ScimSearchQueryConverter());
}
private void createAndIgnoreDuplicate(final String name, final String zoneId) {
try {
create(new ScimGroup(null, name, zoneId), zoneId);
}catch (ScimResourceAlreadyExistsException ignore){
}
}
@Override
public void onApplicationEvent(AbstractUaaEvent event) {
if (event!=null && event instanceof IdentityZoneModifiedEvent) {
IdentityZoneModifiedEvent zevent = (IdentityZoneModifiedEvent)event;
if (zevent.getEventType() == AuditEventType.IdentityZoneCreatedEvent) {
final String zoneId = ((IdentityZone) event.getSource()).getId();
getSystemScopes().stream().forEach(
scope -> createAndIgnoreDuplicate(scope, zoneId)
);
}
}
SystemDeletable.super.onApplicationEvent(event);
}
@Override
protected String getBaseSqlQuery() {
return ALL_GROUPS;
}
@Override
public List<ScimGroup> query(String filter, String sortBy, boolean ascending) {
String zoneId = IdentityZoneHolder.get().getId();
return query(filter, sortBy, ascending, zoneId);
}
public List<ScimGroup> query(String filter, String sortBy, boolean ascending, final String zoneId) {
//validate syntax
getQueryConverter().convert(filter, sortBy, ascending);
if (StringUtils.hasText(filter)) {
filter = "("+ filter+ ") and";
}
filter += " identity_zone_id eq \""+zoneId+"\"";
return super.query(filter, sortBy, ascending);
}
@Override
protected String getTableName() {
return GROUP_TABLE;
}
@Override
public List<ScimGroup> retrieveAll() {
return query("id pr", "created", true);
}
public List<ScimGroup> retrieveAll(final String zoneId) {
return query("id pr", "created", true, zoneId);
}
@Override
public ScimGroup retrieve(String id) throws ScimResourceNotFoundException {
return retrieve(id, IdentityZoneHolder.get().getId());
}
public ScimGroup retrieve(String id, final String zoneId) throws ScimResourceNotFoundException {
try {
ScimGroup group = jdbcTemplate.queryForObject(GET_GROUP_SQL, rowMapper, id, zoneId);
return group;
} catch (EmptyResultDataAccessException e) {
throw new ScimResourceNotFoundException("Group " + id + " does not exist");
}
}
@Override
public ScimGroup create(final ScimGroup group) throws InvalidScimResourceException {
final String zoneId = IdentityZoneHolder.get().getId();
return create(group, zoneId);
}
public ScimGroup create(final ScimGroup group, final String zoneId) throws InvalidScimResourceException {
final String id = UUID.randomUUID().toString();
logger.debug("creating new group with id: " + id);
try {
validateGroup(group);
jdbcTemplate.update(ADD_GROUP_SQL, new PreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps) throws SQLException {
int pos = 1;
ps.setString(pos++, id);
ps.setString(pos++, group.getDisplayName());
ps.setString(pos++, group.getDescription());
ps.setTimestamp(pos++, new Timestamp(new Date().getTime()));
ps.setTimestamp(pos++, new Timestamp(new Date().getTime()));
ps.setInt(pos++, group.getVersion());
ps.setString(pos++, zoneId);
}
});
} catch (DuplicateKeyException ex) {
throw new ScimResourceAlreadyExistsException("A group with displayName: " + group.getDisplayName()
+ " already exists.");
}
return retrieve(id, zoneId);
}
@Override
public ScimGroup update(final String id, final ScimGroup group) throws InvalidScimResourceException, ScimResourceNotFoundException {
final String zoneId = IdentityZoneHolder.get().getId();
return update(id, group, zoneId);
}
public ScimGroup update(final String id, final ScimGroup group, final String zoneId) throws InvalidScimResourceException,
ScimResourceNotFoundException {
try {
validateGroup(group);
int updated = jdbcTemplate.update(UPDATE_GROUP_SQL, new PreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps) throws SQLException {
int pos = 1;
ps.setInt(pos++, group.getVersion() + 1);
ps.setString(pos++, group.getDisplayName());
ps.setString(pos++, group.getDescription());
ps.setTimestamp(pos++, new Timestamp(new Date().getTime()));
ps.setString(pos++, id);
ps.setInt(pos++, group.getVersion());
ps.setString(pos++, zoneId);
}
});
if (updated != 1) {
throw new IncorrectResultSizeDataAccessException(1, updated);
}
return retrieve(id, zoneId);
} catch (DuplicateKeyException ex) {
throw new InvalidScimResourceException("A group with displayName: " + group.getDisplayName()
+ " already exists");
}
}
@Override
public ScimGroup delete(String id, int version) throws ScimResourceNotFoundException {
ScimGroup group = retrieve(id);
membershipManager.removeMembersByGroupId(id);
externalGroupMappingManager.unmapAll(id);
int deleted;
if (version > 0) {
deleted = jdbcTemplate.update(DELETE_GROUP_SQL + " and version=?;", id, IdentityZoneHolder.get().getId(),version);
} else {
deleted = jdbcTemplate.update(DELETE_GROUP_SQL, id, IdentityZoneHolder.get().getId());
}
if (deleted != 1) {
throw new IncorrectResultSizeDataAccessException(1, deleted);
}
return group;
}
public int deleteByIdentityZone(String zoneId) {
jdbcTemplate.update(DELETE_ZONE_ADMIN_MEMBERSHIP_BY_ZONE, IdentityZone.getUaa().getId(), "zones." + zoneId + ".%");
jdbcTemplate.update(DELETE_ZONE_ADMIN_GROUPS_BY_ZONE, IdentityZone.getUaa().getId(), "zones." + zoneId + ".%");
jdbcTemplate.update(DELETE_EXTERNAL_GROUP_BY_ZONE, zoneId);
jdbcTemplate.update(DELETE_GROUP_MEMBERSHIP_BY_ZONE, zoneId);
return jdbcTemplate.update(DELETE_GROUP_BY_ZONE, zoneId);
}
public int deleteByOrigin(String origin, String zoneId) {
jdbcTemplate.update(DELETE_EXTERNAL_GROUP_BY_PROVIDER, zoneId, origin);
return jdbcTemplate.update(DELETE_GROUP_MEMBERSHIP_BY_PROVIDER, zoneId, origin);
}
@Override
public int deleteByClient(String clientId, String zoneId) {
//no op - nothing to do here
return 0;
}
@Override
public int deleteByUser(String userId, String zoneId) {
int result = jdbcTemplate.update(DELETE_MEMBER_SQL, userId, userId, zoneId);
return result;
}
protected void validateGroup(ScimGroup group) throws ScimResourceConstraintFailedException {
if (!StringUtils.hasText(group.getZoneId())) {
throw new ScimResourceConstraintFailedException("zoneId is a required field");
}
}
@Override
protected void validateOrderBy(String orderBy) throws IllegalArgumentException {
super.validateOrderBy(orderBy, GROUP_FIELDS);
}
private static final class ScimGroupRowMapper implements RowMapper<ScimGroup> {
@Override
public ScimGroup mapRow(ResultSet rs, int rowNum) throws SQLException {
int pos = 1;
String id = rs.getString(pos++);
String name = rs.getString(pos++);
String description = rs.getString(pos++);
Date created = rs.getTimestamp(pos++);
Date modified = rs.getTimestamp(pos++);
int version = rs.getInt(pos++);
String zoneId = rs.getString(pos++);
ScimGroup group = new ScimGroup(id, name, zoneId);
group.setDescription(description);
ScimMeta meta = new ScimMeta(created, modified, version);
group.setMeta(meta);
return group;
}
}
}