/* * ==================== * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * * Copyright 2008-2009 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://opensource.org/licenses/cddl1.php * 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 http://opensource.org/licenses/cddl1.php. * 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]" * ==================== */ package org.identityconnectors.db2; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collection; import java.util.EnumSet; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import org.identityconnectors.common.StringUtil; import org.identityconnectors.common.logging.Log; import org.identityconnectors.common.security.GuardedString; import org.identityconnectors.dbcommon.DatabaseQueryBuilder; import org.identityconnectors.dbcommon.FilterWhereBuilder; import org.identityconnectors.dbcommon.SQLUtil; import org.identityconnectors.framework.common.exceptions.AlreadyExistsException; import org.identityconnectors.framework.common.exceptions.ConnectorException; import org.identityconnectors.framework.common.exceptions.InvalidCredentialException; import org.identityconnectors.framework.common.exceptions.UnknownUidException; 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.AttributeInfo.Flags; import org.identityconnectors.framework.common.objects.AttributeInfoBuilder; import org.identityconnectors.framework.common.objects.AttributeUtil; import org.identityconnectors.framework.common.objects.ConnectorObject; import org.identityconnectors.framework.common.objects.ConnectorObjectBuilder; import org.identityconnectors.framework.common.objects.Name; import org.identityconnectors.framework.common.objects.ObjectClass; import org.identityconnectors.framework.common.objects.OperationOptions; 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.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.SchemaOp; import org.identityconnectors.framework.spi.operations.SearchOp; import org.identityconnectors.framework.spi.operations.TestOp; import org.identityconnectors.framework.spi.operations.UpdateAttributeValuesOp; /** * Connector to DB2 database. DB2Connector is main class of connector contract * when DB2 database is target resource. DB2 uses external authentication * provider and internal authorization service. DB2 stores authorization for * users of DB2 objects : * database,schema,table,index,procedure,package,server,tablespace. It supports * just one ObjectClass : ObjectClass.ACCOUNT. DB2 connector is case * insensitive, DB2 stores uppercase values in system tables. <br/> * * DB2 connector uses following attributes : * <ol> * <li>Name : is name of user</li> * <li>grants : is multivalue attribute that means list of grants user has</li> * </ol> * * DB2 connector supports following operations : * <ol> * <li>AuthenticateOp : We try to create JDBC connection to authenticate</li> * <li>CreateOp : We store passed user's grants in DB2 system tables, actually * we perform 'execute' on passed grants. At least user is granted 'CONNECT ON * DATABASE'.</li> * <li>SearchOp : Natively we support search by user name. Search by grants is * also supported, but in this case we return all users with grants and * framework does the filtering.</li> * <li>DeleteOp : We delete all users's grants</li> * <li>UpdateAttributeValuesOp : We update user grants * <ul> * <li>For update we replace existing grants with passed ones</li> * <li>For addAttributeValues we add passed grants to existing grants</li> * <li>For removeAttributeValues we revoke passed grants</li> * </ul> * </li> * <li>TestOp : We test whether connection to DB2 is still alive</li> * </ol> * * DB2 connector implements AttributeNormalizer to uppercase passed user name, * uid,grants, because DB2 stores users and grants in uppercase. * * Look at {@link DB2Configuration} for more information about configuration * properties. * * * @author kitko * */ @ConnectorClass(displayNameKey = DB2Messages.DB2_CONNECTOR_DISPLAY, configurationClass = DB2Configuration.class, messageCatalogPaths = { "org/identityconnectors/dbcommon/Messages", "org/identityconnectors/db2/Messages" }) public class DB2Connector implements AuthenticateOp, SchemaOp, CreateOp, SearchOp<FilterWhereBuilder>, DeleteOp, UpdateAttributeValuesOp, TestOp, PoolableConnector, AttributeNormalizer { private final static Log LOG = Log.getLog(DB2Connector.class); private final static String USER_EXITS_QUERY = "SELECT GRANTEE FROM SYSIBM.SYSDBAUTH WHERE GRANTEETYPE = 'U' AND CONNECTAUTH = 'Y' AND TRIM(GRANTEE) = ?"; private final static String ALL_USER_QUERY = "SELECT GRANTEE FROM SYSIBM.SYSDBAUTH WHERE GRANTEETYPE = 'U' AND CONNECTAUTH = 'Y'"; private Connection adminConn; private DB2Configuration cfg; static final String USER_AUTH_GRANTS = "grants"; private String testSQL; /** * Default constructor called using reflection from framework */ public DB2Connector() { } /** * Authenticates user in DB2 database. Here we create new SQL connection * with passed credentials to authenticate user. We check SQL state and * return code to verify that possibly thrown exception really means * user/password is invalid. When we are able to get connection using passed * credentials, we consider that authenticate passed. */ public Uid authenticate(ObjectClass objectClass, String username, GuardedString password, OperationOptions options) { LOG.info("Authenticate user: {0}", username); // just try to create connection with passed credentials Connection conn = null; try { conn = createConnection(username, password); } catch (RuntimeException e) { if (e.getCause() instanceof SQLException) { SQLException sqlE = (SQLException) e.getCause(); if ("28000".equals(sqlE.getSQLState()) && -4214 == sqlE.getErrorCode()) { // Wrong user or password, log it here and rethrow LOG.info(e, "DB2.authenticate : Invalid user/passord for user: {0}", username); throw new InvalidCredentialException(cfg.getConnectorMessages().format( DB2Messages.AUTHENTICATE_INVALID_CREDENTIALS, null), e.getCause()); } } throw e; } finally { SQLUtil.closeQuietly(conn); } LOG.info("User authenticated : {0}", username); return new Uid(username.toUpperCase()); } public Schema schema() { // The Name is supported attribute Set<AttributeInfo> attrInfoSet = new HashSet<AttributeInfo>(); attrInfoSet.add(AttributeInfoBuilder.build(Name.NAME, String.class, EnumSet.of( Flags.NOT_UPDATEABLE, Flags.REQUIRED))); /* * AttributeInfo password = AttributeInfoBuilder.build( * OperationalAttributes.PASSWORD_NAME, GuardedString.class, * EnumSet.of(Flags * .NOT_READABLE,Flags.NOT_RETURNED_BY_DEFAULT,Flags.NOT_CREATABLE)); * attrInfoSet.add(password); */ AttributeInfoBuilder grantsBuilder = new AttributeInfoBuilder(); grantsBuilder.setName(USER_AUTH_GRANTS).setCreateable(true).setUpdateable(true) .setRequired(true).setReadable(true).setMultiValued(true) .setReturnedByDefault(true); attrInfoSet.add(grantsBuilder.build()); // Use SchemaBuilder to build the schema. Currently, only ACCOUNT type // is supported. SchemaBuilder schemaBld = new SchemaBuilder(getClass()); schemaBld.defineObjectClass(ObjectClass.ACCOUNT_NAME, attrInfoSet); return schemaBld.build(); } private String getTestSQL() { if (testSQL != null) { return testSQL; } testSQL = DB2Specifics.findTestSQL(adminConn); return testSQL; } public void checkAlive() { DB2Specifics.testConnection(adminConn, getTestSQL()); } public void dispose() { SQLUtil.closeQuietly(adminConn); } public DB2Configuration getConfiguration() { return cfg; } public void init(Configuration cfg) { this.cfg = (DB2Configuration) cfg; this.adminConn = createAdminConnection(); } private Connection createAdminConnection() { final Connection conn = cfg.createAdminConnection(); // switch off auto commit, but not when connecting using datasource. // Probably connection from DS would throw exception when trying to // change autocommit if (!DB2Configuration.ConnectionType.DATASOURCE.equals(cfg.getConnType())) { try { conn.setAutoCommit(false); } catch (SQLException e) { throw new ConnectorException("Cannot switch off autocommit", e); } } return conn; } private Connection createConnection(String user, GuardedString password) { final Connection conn = cfg.createConnection(user, password); return conn; } List<String> buildAuthorityAttributeValue(String userName) { DB2AuthorityReader dB2AuthorityReader = new DB2AuthorityReader(adminConn); Collection<DB2Authority> allAuths = null; try { allAuths = dB2AuthorityReader.readAllAuthorities(userName); } catch (SQLException e) { throw new ConnectorException("Error reading db2 authorities", e); } List<String> result = new ArrayList<String>(2); for (DB2Authority authority : allAuths) { final DB2AuthorityTable authorityTable = DB2Specifics.authType2DB2AuthorityTable(authority.authorityType); String grantString = authorityTable.generateGrant(authority); result.add(grantString); } return result; } public FilterTranslator<FilterWhereBuilder> createFilterTranslator(ObjectClass oclass, OperationOptions options) { return new DB2FilterTranslator(oclass, options); } /** * Executes search using DB2 system table. We support searching by UID/Name * and grants. When searching by uid/name we use * <code>SYSIBM.SYSDBAUTH table</code> to execute search. We always include * grants attribute by reading all grants from system tables. Then when * searching by grants, we will return now all users and framework will * filter users that have filtered grants. */ public void executeQuery(ObjectClass oclass, FilterWhereBuilder where, ResultsHandler handler, OperationOptions options) { checkObjectClass(oclass); // Read users from SYSIBM.SYSDBAUTH table // DB2 stores users in UPPERCASE , we must do UPPER(TRIM(GRANTEE)) = // upper('john') // Database query builder will create SQL query. // if where == null then all users are returned final DatabaseQueryBuilder query = new DatabaseQueryBuilder(ALL_USER_QUERY); query.setWhere(where); final String sql = query.getSQL(); LOG.info("Executing search query : {0}", sql); ResultSet result = null; PreparedStatement statement = null; try { statement = adminConn.prepareStatement(sql); SQLUtil.setParams(statement, query.getParams()); result = statement.executeQuery(); while (result.next()) { ConnectorObjectBuilder bld = new ConnectorObjectBuilder(); final String userName = result.getString("GRANTEE").trim(); // if(options.getAttributesToGet() != null && // Arrays.asList(options.getAttributesToGet()).contains(USER_AUTH_GRANTS)){ List<String> authStrings = buildAuthorityAttributeValue(userName); bld.addAttribute(USER_AUTH_GRANTS, authStrings); // } bld.setUid(new Uid(userName)); bld.setName(userName); // No other attributes are now supported. // Password can be encoded and it is not provided as an // attribute // only deals w/ accounts.. bld.setObjectClass(ObjectClass.ACCOUNT); // create the connector object.. ConnectorObject ret = bld.build(); if (!handler.handle(ret)) { break; } } } catch (SQLException e) { String detailMsg = new SQLMsgRetriever().retrieveMsg(e); throw new ConnectorException(cfg.getConnectorMessages().format( DB2Messages.SEARCH_FAILED, null, detailMsg), e); } finally { SQLUtil.closeQuietly(result); SQLUtil.closeQuietly(statement); } } private void checkObjectClass(ObjectClass oclass) { if (!ObjectClass.ACCOUNT.equals(oclass)) { throw new IllegalArgumentException(cfg.getConnectorMessages().format( DB2Messages.UNSUPPORTED_OBJECT_CLASS, null, oclass)); } } /** * Create user in case of DB2 means only storing passed grants. We will * actually not create any new user, DB2 uses externally authentication * provider to do real authentication, default to underlying OS. So here we * just verify name of user and verify if same user is not already stored in * DB2 system tables. Then we store passed user grants using grant * statement. */ public Uid create(ObjectClass oclass, Set<Attribute> attrs, OperationOptions options) { checkObjectClass(oclass); checkCreateAttributes(attrs); Name user = AttributeUtil.getNameFromAttributes(attrs); if (user == null || StringUtil.isBlank(user.getNameValue())) { throw new IllegalArgumentException(cfg.getConnectorMessages().format( DB2Messages.NAME_IS_NULL_OR_EMPTY, null)); } final String userName = user.getNameValue(); LOG.info("Creating user : {0}", userName); checkUserNotExist(userName); checkDB2Validity(userName); try { updateAuthority(userName, attrs, UpdateType.ADD); adminConn.commit(); LOG.info("User created : {0}", userName); } catch (Exception e) { SQLUtil.rollbackQuietly(adminConn); String detailMsg = new SQLMsgRetriever().retrieveMsg(e); throw new ConnectorException(cfg.getConnectorMessages().format( DB2Messages.CREATE_OF_USER_FAILED, null, userName, detailMsg), e); } return new Uid(userName); } private void checkCreateAttributes(Set<Attribute> attrs) { for (Attribute attribute : attrs) { if (attribute.is(Name.NAME)) { } else if (attribute.is(USER_AUTH_GRANTS)) { } else { throw new IllegalArgumentException(MessageFormat.format( "Unrecognized argument [{0}] in DB2 create operation", attribute.getName())); } } } private void checkUpdateAttributes(Set<Attribute> attrs) { for (Attribute attribute : attrs) { if (attribute.is(Uid.NAME)) { } else if (attribute.is(USER_AUTH_GRANTS)) { } else { throw new IllegalArgumentException(MessageFormat.format( "Unrecognized argument [{0}] in DB2 update operation", attribute.getName())); } } } private void checkUserNotExist(String user) { boolean userExist = userExist(user); if (userExist) { throw new AlreadyExistsException(cfg.getConnectorMessages().format( DB2Messages.USER_ALREADY_EXISTS, null, user)); } } private void checkUserExist(String user) { boolean userExist = userExist(user); if (!userExist) { throw new UnknownUidException(new Uid(user), ObjectClass.ACCOUNT); } } private boolean userExist(String user) { PreparedStatement st = null; ResultSet rs = null; try { st = adminConn.prepareStatement(USER_EXITS_QUERY); st.setString(1, user.toUpperCase()); rs = st.executeQuery(); return rs.next(); } catch (SQLException e) { throw new ConnectorException("Cannot test whether user exist", e); } finally { SQLUtil.closeQuietly(rs); SQLUtil.closeQuietly(st); } } /** * Applies resources grants and revokes to the passed user. Updates occurs * in a transaction. Assumes connection is already open. */ @SuppressWarnings("unchecked") private void updateAuthority(String user, Set<Attribute> attrs, UpdateType type) throws SQLException { checkAdminConnection(); Attribute wsAttr = AttributeUtil.find(USER_AUTH_GRANTS, attrs); Collection<String> grants = (Collection<String>) (wsAttr != null ? new ArrayList<Object>(wsAttr.getValue()) : new ArrayList<String>(3)); switch (type) { case ADD: { addMandatoryConnect(grants); executeGrants(grants, user); break; } case REPLACE: { addMandatoryConnect(grants); revokeAllGrants(user); executeGrants(grants, user); break; } case DELETE: { removeMandatoryRevoke(grants); executeRevokes(grants, user); break; } } } private void addMandatoryConnect(Collection<String> grants) { boolean addConnect = true; for (String grant : grants) { if (grant.trim().equalsIgnoreCase("CONNECT ON DATABASE")) { addConnect = false; } } if (addConnect) { grants.add("CONNECT ON DATABASE"); } } private void removeMandatoryRevoke(Collection<String> grants) { for (Iterator<String> i = grants.iterator(); i.hasNext();) { if (i.next().trim().equalsIgnoreCase("CONNECT ON DATABASE")) { i.remove(); } } } private void checkAdminConnection() { if (adminConn == null) { throw new IllegalStateException("No admin connection present"); } } /** * Checks a given account id to make sure they follow DB2 rules for * validity. The rules are given in the DB2 SQL Reference Manual. They * include length limits, forbidden prefixes, and forbidden keywords. Throws * and exception if the name or password are invalid. */ void checkDB2Validity(String accountID) { if (accountID.length() > DB2Specifics.MAX_NAME_SIZE) { throw new IllegalArgumentException(cfg.getConnectorMessages().format( DB2Messages.USERNAME_LONG, null, DB2Specifics.MAX_NAME_SIZE, accountID)); } if (DB2Specifics.containsIllegalDB2Chars(accountID.toCharArray())) { throw new IllegalArgumentException(cfg.getConnectorMessages().format( DB2Messages.USERNAME_CONTAINS_ILLEGAL_CHARACTERS, null, accountID)); } if (DB2Specifics.isReservedName(accountID.toUpperCase())) { throw new IllegalArgumentException(cfg.getConnectorMessages().format( DB2Messages.USERNAME_IS_RESERVED_WORD, null, accountID)); } } /** * Removes all grants for a user on the resource. Effectively deletes them * from the resource. */ private void revokeAllGrants(String user) throws SQLException { checkDB2Validity(user); Collection<DB2Authority> allAuthorities = new DB2AuthorityReader(adminConn).readAllAuthorities(user); revokeGrants(allAuthorities); } /** * For a given grant type and user, revokes the passed collection of grant * objects from the resource. */ private void revokeGrants(Collection<DB2Authority> db2AuthoritiesToRevoke) throws SQLException { for (DB2Authority auth : db2AuthoritiesToRevoke) { DB2AuthorityTable authTable = DB2Specifics.authType2DB2AuthorityTable(auth.authorityType); String revokeSQL = authTable.generateRevokeSQL(auth); executeSQL(revokeSQL); } } private void executeSQL(String sql) throws SQLException { checkAdminConnection(); Statement statement = null; try { statement = adminConn.createStatement(); statement.execute(sql); } catch (SQLException e) { LOG.error(e, "Error executing sql {0}", sql); throw e; } finally { SQLUtil.closeQuietly(statement); } } /** * Executes a set of sql GRANT statements built using an sql prefix, a * collection of grant objects, a postfix, and a user. Throws if anything * goes wrong. */ private void executeGrants(Collection<String> grants, String user) throws SQLException { for (String grant : grants) { String sql = "GRANT " + grant + " TO USER " + user.toUpperCase(); executeSQL(sql); } } /** * Executes a set of sql REVOKE statements built using an sql prefix, a * collection of grant objects, a postfix, and a user. Throws if anything * goes wrong. */ private void executeRevokes(Collection<String> grants, String user) throws SQLException { for (String grant : grants) { String sql = "REVOKE " + grant + " FROM USER " + user.toUpperCase(); executeSQL(sql); } } /** * Removes all associated grants from user, so do all revoke statement. */ public void delete(ObjectClass objClass, Uid uid, OperationOptions options) { checkObjectClass(objClass); final String uidValue = uid.getUidValue(); checkUserExist(uidValue); LOG.info("Deleting user : {0}", uidValue); try { revokeAllGrants(uidValue); adminConn.commit(); LOG.info("User deleted : {0}", uidValue); } catch (Exception e) { SQLUtil.rollbackQuietly(adminConn); String detailMsg = new SQLMsgRetriever().retrieveMsg(e); throw new ConnectorException(cfg.getConnectorMessages().format( DB2Messages.DELETE_OF_USER_FAILED, null, uidValue, detailMsg), e); } } /** * Test of configuration and validity of connection */ public void test() { cfg.validate(); DB2Specifics.testConnection(adminConn, getTestSQL()); } /** * Replaces value of grants attribute. */ public Uid update(ObjectClass objclass, Uid uid, Set<Attribute> attrs, OperationOptions options) { if (cfg.isReplaceAllGrantsOnUpdate()) { return update(UpdateType.REPLACE, objclass, AttributeUtil.addUid(attrs, uid), options); } else { return update(UpdateType.ADD, objclass, AttributeUtil.addUid(attrs, uid), options); } } /** * Add grants to existing grants of user */ public Uid addAttributeValues(ObjectClass objclass, Uid uid, Set<Attribute> valuesToAdd, OperationOptions options) { return update(UpdateType.ADD, objclass, AttributeUtil.addUid(valuesToAdd, uid), options); } /** * Removes grants from user */ public Uid removeAttributeValues(ObjectClass objclass, Uid uid, Set<Attribute> valuesToRemove, OperationOptions options) { return update(UpdateType.DELETE, objclass, AttributeUtil.addUid(valuesToRemove, uid), options); } private Uid update(UpdateType type, ObjectClass objclass, Set<Attribute> attrs, OperationOptions options) { checkObjectClass(objclass); checkUpdateAttributes(attrs); Name name = AttributeUtil.getNameFromAttributes(attrs); if (name != null) { throw new IllegalArgumentException(cfg.getConnectorMessages().format( DB2Messages.NAME_IS_NOT_UPDATABLE, null)); } Uid uid = AttributeUtil.getUidAttribute(attrs); if (uid == null || StringUtil.isBlank(uid.getUidValue())) { throw new IllegalArgumentException(cfg.getConnectorMessages().format( DB2Messages.UPDATE_UID_CANNOT_BE_NULL_OR_EMPTY, null)); } final String uidValue = uid.getUidValue(); checkUserExist(uidValue); try { LOG.info("Update user : {0}", uidValue); updateAuthority(uidValue, attrs, type); adminConn.commit(); LOG.info("User updated : {0}", uidValue); } catch (Exception e) { SQLUtil.rollbackQuietly(adminConn); String detailMsg = new SQLMsgRetriever().retrieveMsg(e); throw new ConnectorException(cfg.getConnectorMessages().format( DB2Messages.UPDATE_OF_USER_FAILED, null, uidValue, detailMsg), e); } return uid; } public Attribute normalizeAttribute(ObjectClass oclass, Attribute attribute) { if (attribute.is(Name.NAME)) { String value = (String) attribute.getValue().get(0); return new Name(value.trim().toUpperCase()); } else if (attribute.is(Uid.NAME)) { String value = (String) attribute.getValue().get(0); return new Uid(value.trim().toUpperCase()); } else if (attribute.is(USER_AUTH_GRANTS)) { List<Object> grants = attribute.getValue(); List<String> upGrants = new ArrayList<String>(grants.size()); for (Object grant : grants) { upGrants.add(grant.toString().toUpperCase()); } return AttributeBuilder.build(USER_AUTH_GRANTS, upGrants); } return attribute; } private enum UpdateType { ADD, REPLACE, DELETE } }