/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (the "License"). You may not use this file except in compliance * with the License. * * You can obtain a copy of the license at * docs/licenses/cddl.txt * or http://www.opensource.org/licenses/cddl1.php. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at * docs/licenses/cddl.txt. If applicable, * add the following below this CDDL HEADER, with the fields enclosed * by brackets "[]" replaced with your own identifying information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * * Copyright 2013-2016 UnboundID Corp. */ package com.unboundid.directory.sdk.examples; import com.unboundid.directory.sdk.broker.api.StoreAdapter; import com.unboundid.directory.sdk.broker.config.StoreAdapterConfig; import com.unboundid.directory.sdk.broker.types.IdentityBrokerContext; import com.unboundid.directory.sdk.broker.types.StoreCreateRequest; import com.unboundid.directory.sdk.broker.types.StoreDeleteRequest; import com.unboundid.directory.sdk.broker.types.StoreSearchRequest; import com.unboundid.directory.sdk.broker.types.StoreUpdateRequest; import com.unboundid.directory.sdk.common.types.LogSeverity; import com.unboundid.ldap.sdk.LDAPException; import com.unboundid.scim.data.AttributeValueResolver; import com.unboundid.scim.data.BaseResource; import com.unboundid.scim.data.Meta; import com.unboundid.scim.marshal.json.JsonMarshaller; import com.unboundid.scim.marshal.json.JsonUnmarshaller; import com.unboundid.scim.schema.ResourceDescriptor; import com.unboundid.scim.sdk.AttributePath; import com.unboundid.scim.sdk.Diff; import com.unboundid.scim.sdk.InvalidResourceException; import com.unboundid.scim.sdk.PageParameters; import com.unboundid.scim.sdk.ResourceNotFoundException; import com.unboundid.scim.sdk.Resources; import com.unboundid.scim.sdk.SCIMException; import com.unboundid.scim.sdk.SCIMFilter; import com.unboundid.scim.sdk.SCIMFilterType; import com.unboundid.scim.sdk.SCIMObject; import com.unboundid.scim.sdk.SCIMQueryAttributes; import com.unboundid.scim.sdk.ServerErrorException; import com.unboundid.util.StaticUtils; import com.unboundid.util.Validator; import com.unboundid.util.args.ArgumentException; import com.unboundid.util.args.ArgumentParser; import com.unboundid.util.args.FileArgument; import com.unboundid.util.args.StringArgument; import org.json.JSONException; import org.json.JSONObject; import java.io.BufferedReader; import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.net.URI; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.LinkedList; import java.util.List; import java.util.UUID; import static com.unboundid.scim.sdk.SCIMConstants.SCHEMA_URI_CORE; import static com.unboundid.scim.data.BaseResource.BASE_RESOURCE_FACTORY; import static com.unboundid.scim.schema.CoreSchema.USER_DESCRIPTOR; /** * Example implementation of a Dataview StoreAdapter persisting to a Java jdbc * RDBMS backing store. This adapter depends on a jdbc driver jar which must be * copied into server's lib directory. The defaults in this example work with * Apache * derby 10.10.1.1 */ public class ExampleJDBCStoreAdapter extends StoreAdapter { /** * The name of the argument that will be used to load the jdbc driver class. */ public static final String ARG_NAME_JDBC_DRIVER = "jdbc-driver-class"; /** * The name of the argument that will be used to specify the jdbc url to the * database instance used by this adapter. */ public static final String ARG_NAME_JDBC_URL = "jdbc-url"; /** * The name of the argument that will be used to specify the file name to * initialize the database schema used by this adapter. */ public static final String ARG_NAME_INIT_SQL_SCHEMA_FILE = "init-sql-schema-path"; // JDBC driver for the store adapter. private String jdbcDriverClass; // JDBC url for the store adapter. private String jdbcUrl; // Connection to JDBC database. private Connection connection; // Handle to the ServerContext object. private IdentityBrokerContext serverContext; private static final AttributeValueResolver SINGULAR = AttributeValueResolver.STRING_RESOLVER; /** * {@inheritDoc} */ @Override public String getExtensionName() { return "Simple JDBC Store Adapter"; } /** * {@inheritDoc} */ @Override public String[] getExtensionDescription() { return new String[] { "Implements a simple Store Adapter for a Broker DataView which is " + "persisted in an embeddable JDBC backing store. This example persists " + "user schema to a single table structure in user-defined tablespace." }; } /** * {@inheritDoc} */ @Override public void defineConfigArguments(final ArgumentParser parser) throws ArgumentException { StringArgument jdbcDriverClassStringArg = new StringArgument(null, ARG_NAME_JDBC_DRIVER, true, 1, "{jdbc-driver-class}", "The JDBC driver class name used by this store adapter.", "org.apache.derby.jdbc.EmbeddedDriver"); parser.addArgument(jdbcDriverClassStringArg); StringArgument jdbcUrlStringArg = new StringArgument(null, ARG_NAME_JDBC_URL, true, 1, "{jdbc-url}", "The JDBC url to the database instance used by this store adapter.", "jdbc:derby:storeadapter;create=true"); parser.addArgument(jdbcUrlStringArg); final FileArgument initSqlSchemaFileArg = new FileArgument(null, ARG_NAME_INIT_SQL_SCHEMA_FILE, true, 1, "{init-sql-schema-path}", "The path to the file containing the create table command used to " + "initialize the database.", false, true, true, false, Arrays.asList( new File("resource/example-jdbc-store-adapter/create-scim-table.sql") )); parser.addArgument(initSqlSchemaFileArg); } /** * {@inheritDoc} */ @Override public void initializeStoreAdapter(final IdentityBrokerContext serverContext, final StoreAdapterConfig config, final ArgumentParser parser) throws IOException, LDAPException, SQLException { this.serverContext = serverContext; StringArgument jdbcDriverArgument = (StringArgument) parser.getNamedArgument(ARG_NAME_JDBC_DRIVER); jdbcDriverClass = jdbcDriverArgument.getValue(); Validator.ensureNotNull(jdbcDriverClass); try { // Some JDBC drivers require their static and default constructors to be // called for the driver to be properly registered with the DriverManager. Class.forName(jdbcDriverClass).newInstance(); } catch (Exception e) { throw new RuntimeException(e); } StringArgument jdbcUrlArgument = (StringArgument) parser.getNamedArgument(ARG_NAME_JDBC_URL); jdbcUrl = jdbcUrlArgument.getValue(); Validator.ensureNotNull(jdbcUrl); connection = DriverManager.getConnection(jdbcUrl); // check to see if the schema exists FileArgument schemaFileArg = (FileArgument) parser.getNamedArgument(ARG_NAME_INIT_SQL_SCHEMA_FILE); File sqlSchemaFile = schemaFileArg.getValue(); if(!sqlSchemaFile.isAbsolute()) { sqlSchemaFile = new File(serverContext.getServerRoot(), sqlSchemaFile.getPath()); } File schemaCreatedFlag = new File(sqlSchemaFile.getParentFile(), ".example-jdbc-schema-created"); if (!schemaCreatedFlag.exists()) { BufferedReader reader = new BufferedReader(new FileReader(sqlSchemaFile)); StringBuffer buffer = new StringBuffer(); String line; while ((line = reader.readLine()) != null) { if (line.trim().startsWith("--")) { continue; } buffer.append(line); } Statement createSql = connection.createStatement(); createSql.execute(buffer.toString()); schemaCreatedFlag.createNewFile(); } } /** * {@inheritDoc} */ @Override public boolean isAvailable() { if (connection != null && serverContext != null) { try { return connection.isValid(10); } catch (SQLException e) { serverContext.logMessage(LogSeverity.SEVERE_ERROR, "Cannot connect to database: " + e.getMessage()); return false; } } else { return false; } } /** * {@inheritDoc} */ @Override public ResourceDescriptor getNativeSchema() { return USER_DESCRIPTOR; } /** * {@inheritDoc} */ @Override public Resources search( final StoreSearchRequest request) throws SCIMException { final SCIMFilter filter = request.getSCIMFilter(); final AttributePath idAttrPath = new AttributePath(SCHEMA_URI_CORE, "id", null); try { if (filter != null && filter.getFilterType().equals(SCIMFilterType.EQUALITY) && filter.getFilterAttribute().toString().equals(idAttrPath.toString())) { //This is a search by SCIM ID ResultSet resultSet = queryScimResourceById(filter.getFilterValue(), true); if (resultSet.next()) { BaseResource resource = loadScimResourceFromResultSet(resultSet); removeSensitiveAttributes(resource.getScimObject()); return new Resources( Collections.singletonList(resource)); } else { return new Resources( Collections.emptyList()); } } else { final PageParameters pageParams = request.getPageParameters(); final List resultList = new LinkedList(); int index = 0; Statement statement = connection.createStatement(); ResultSet resultSet = statement.executeQuery(ScimSqlStatement.SELECT_ALL .getStatement()); while (resultSet.next()) { BaseResource resource = loadScimResourceFromResultSet(resultSet); if (filter == null || resource.getScimObject().matchesFilter(filter)) { index++; if (pageParams != null && pageParams.getStartIndex() > index) { continue; } if (pageParams != null && index > pageParams.getCount()) { break; } resultList.add(createParedResource(resource.getScimObject(), request.getSCIMQueryAttributes())); } } return new Resources(resultList); } } catch (final SQLException se) { serverContext.debugCaught(se); throw new ServerErrorException(StaticUtils.getExceptionMessage(se)); } } /** * {@inheritDoc} */ @Override public BaseResource create(final StoreCreateRequest request) throws SCIMException { try { //Populate the SCIM 'id' and 'meta' attributes final BaseResource resourceToCreate = request.getResource(); resourceToCreate.setId(UUID.randomUUID().toString()); final URI location = URI.create("/" + resourceToCreate.getId()); final Meta meta = new Meta(new Date(), null, location, null); resourceToCreate.setMeta(meta); List parameters = getParametersFromResource(resourceToCreate); PreparedStatement preparedStatement = ScimSqlStatement.INSERT .getPreparedStatement(connection, parameters .toArray(new String[parameters.size()])); preparedStatement.executeUpdate(); return createParedResource(resourceToCreate.getScimObject(), request.getSCIMQueryAttributes()); } catch (final Exception e) { serverContext.debugCaught(e); throw new ServerErrorException(StaticUtils.getExceptionMessage(e)); } } /** * {@inheritDoc} */ @Override public BaseResource update(final StoreUpdateRequest request) throws SCIMException { checkIfIdExists(request.getId()); try { BaseResource resource = loadScimResourceById(request.getId()); final Diff diff = Diff.fromPartialResource( request.getPartialResource(), false); //Apply the PATCH modifications to the resource BaseResource resourceToUpdate = diff.apply(resource, BASE_RESOURCE_FACTORY); //Update the 'meta.lastModified' attribute final Meta meta = resource.getMeta(); meta.setLastModified(new Date()); resource.setMeta(meta); Validator.ensureNotNull(resourceToUpdate); final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); try { new JsonMarshaller().marshal(resourceToUpdate, outputStream); List parameters = getParametersFromResource(resource); parameters.remove(0); parameters.add(resourceToUpdate.getId()); PreparedStatement preparedStatement = ScimSqlStatement.UPDATE .getPreparedStatement(connection, parameters.toArray( new String[parameters.size()])); preparedStatement.executeUpdate(); } finally { safeCloseStream(outputStream); } return createParedResource(resourceToUpdate.getScimObject(), request.getSCIMQueryAttributes()); } catch (final Exception e) { serverContext.debugCaught(e); throw new ServerErrorException(StaticUtils.getExceptionMessage(e)); } } /** * {@inheritDoc} */ @Override public void delete(final StoreDeleteRequest request) throws SCIMException { checkIfIdExists(request.getId()); try { PreparedStatement preparedStatement = ScimSqlStatement.DELETE .getPreparedStatement(connection, request.getId()); preparedStatement.execute(); } catch (final SQLException se) { serverContext.debugCaught(se); throw new ServerErrorException(StaticUtils.getExceptionMessage(se)); } } /** * Checks to see if a resource exists. * * @param id to check * @throws ResourceNotFoundException if a resource was not found */ private void checkIfIdExists(final String id) throws ResourceNotFoundException { ResultSet resultSet = queryScimResourceById(id, false); try { if (!resultSet.next()) { throw new ResourceNotFoundException(String.format( "No resource found with ID '%s'", id)); } } catch (final SQLException se) { serverContext.debugCaught(se); throw new ResourceNotFoundException(StaticUtils.getExceptionMessage(se)); } } /** * Loads a scim resource by id. * * @param id to load from the database * @return BaseResource from the database * @throws ResourceNotFoundException if the resource cannot be found * @throws InvalidResourceException if the resource is invalid */ private BaseResource loadScimResourceById(final String id) throws ResourceNotFoundException, InvalidResourceException { ResultSet resultSet = queryScimResourceById(id, true); try { if (!resultSet.next()) { throw new ResourceNotFoundException(String.format( "No resource found with ID '%s'", id)); } return loadScimResourceFromResultSet(resultSet); } catch (final SQLException se) { serverContext.debugCaught(se); throw new ResourceNotFoundException(StaticUtils.getExceptionMessage(se)); } } /** * Loads a scim resource from the current database ResultSet. * * @param resultSet to load a resource from * @return BaseResource loaded from the ResultSet * @throws InvalidResourceException if the resource is invalid * @throws SQLException if a problem occurred */ private BaseResource loadScimResourceFromResultSet(final ResultSet resultSet) throws InvalidResourceException, SQLException { final ResourceDescriptor schema = getNativeSchema(); final JsonUnmarshaller jsonUnmarshaller = new JsonUnmarshaller(); return jsonUnmarshaller.unmarshal(resultSet.getAsciiStream("json"), schema, BASE_RESOURCE_FACTORY); } /** * Queries the database for a scim resource by id. * * @param id to query in the database * @param loadRecord {@code true} to query full record, * {@code false} otherwise * @return ResultSet referencing a found scim resource * @throws ResourceNotFoundException if the resource cannot be found */ private ResultSet queryScimResourceById(final String id, final boolean loadRecord) throws ResourceNotFoundException { try { Statement statement = connection.createStatement(); PreparedStatement preparedStatement; if (loadRecord) { preparedStatement = ScimSqlStatement.SELECT_CONTENT .getPreparedStatement(connection, id); } else { preparedStatement = ScimSqlStatement.SELECT_ID .getPreparedStatement(connection, id); } return preparedStatement.executeQuery(); } catch (final SQLException se) { serverContext.debugCaught(se); throw new ResourceNotFoundException(StaticUtils.getExceptionMessage(se)); } } /** * Creates a new resource from a scim object with specific attributes. * * @param scimObject to create a resource from * @param attributes to include in the resource * @return BaseResource created from the scim object */ private BaseResource createParedResource(final SCIMObject scimObject, final SCIMQueryAttributes attributes) { //Pare the returned resource down to the attributes that were requested final SCIMObject paredObject = attributes.pareObject(scimObject); removeSensitiveAttributes(paredObject); final BaseResource newResource = new BaseResource( getNativeSchema(), paredObject); return newResource; } /** * Removes any sensitive attributes from a scim object. * * @param scimObject to process */ private void removeSensitiveAttributes(final SCIMObject scimObject) { scimObject.removeAttribute(SCHEMA_URI_CORE, "password"); } /** * Returns a resource as a series of sql parameters. * * @param resource to parameterize * @return resource as parameters * @throws SCIMException if problem occurs */ private List getParametersFromResource(final BaseResource resource) throws SCIMException { final List parameters = new ArrayList(); final JsonMarshaller marshaller = new JsonMarshaller(); ByteArrayOutputStream outputStream = null; try { parameters.add(resource.getId()); parameters.add(resource.getExternalId()); parameters.add(resource.getMeta().toString()); parameters.add(getSingularAttribute(resource, "userName")); parameters.add(getSingularAttribute(resource, "name.formatted")); parameters.add(getSingularAttribute(resource, "name.familyName")); parameters.add(getSingularAttribute(resource, "name.givenName")); parameters.add(getSingularAttribute(resource, "name.middleName")); parameters.add(getSingularAttribute(resource, "name.honorificPrefix")); parameters.add(getSingularAttribute(resource, "name.honorificSuffix")); parameters.add(getSingularAttribute(resource, "displayName")); parameters.add(getSingularAttribute(resource, "nickName")); parameters.add(getSingularAttribute(resource, "profileUrl")); parameters.add(getSingularAttribute(resource, "title")); parameters.add(getSingularAttribute(resource, "preferredLanguage")); parameters.add(getSingularAttribute(resource, "locale")); parameters.add(getSingularAttribute(resource, "timezone")); outputStream = new ByteArrayOutputStream(); marshaller.marshal(resource, outputStream); JSONObject jsonObject = null; try { jsonObject = new JSONObject(outputStream.toString()); parameters.add(getComplexAttribute(resource, jsonObject, "emails")); parameters.add(getComplexAttribute(resource, jsonObject, "addresses")); parameters.add(getComplexAttribute(resource, jsonObject, "phonenumbers")); parameters.add(getComplexAttribute(resource, jsonObject, "photos")); parameters.add(getComplexAttribute(resource, jsonObject, "groups")); parameters.add(getComplexAttribute(resource, jsonObject, "entitlements")); parameters.add(getComplexAttribute(resource, jsonObject, "roles")); } catch (final JSONException je) { throw new RuntimeException(je); } parameters.add(getSingularAttribute(resource, "website")); parameters.add(getSingularAttribute(resource, "gender")); parameters.add(outputStream.toString()); } finally { safeCloseStream(outputStream); } return parameters; } /** * Retrieves a single attribute from a resource. * * @param resource to access * @param attribute to retrieve * @return attribute value */ private String getSingularAttribute(final BaseResource resource, final String attribute) { String value = null; if (attribute.contains(".")) { AttributePath attributePath = AttributePath.parse(attribute); String parentAttributeName = attributePath.getAttributeName(); SCIMObject object = resource.getScimObject(); if (object.hasAttribute(SCHEMA_URI_CORE, parentAttributeName)) { String subAttribute = attributePath.getSubAttributeName(); value = object.getAttribute(SCHEMA_URI_CORE, parentAttributeName) .getValue().getSubAttributeValue(subAttribute, SINGULAR); } } else { value = resource.getSingularAttributeValue(SCHEMA_URI_CORE, attribute, SINGULAR); } return value == null ? "" : value; } /** * Retrieves a complex or multi-value attribute from a resource. * * @param resource to access * @param object resource in json format * @param attribute to retrieve * @return attribute content in json * @throws JSONException if a problem occurs */ private String getComplexAttribute(final BaseResource resource, final JSONObject object, final String attribute) throws JSONException { String value = ""; if (resource.getScimObject().hasAttribute(SCHEMA_URI_CORE, attribute)) { value = object.getJSONObject(attribute).toString(); } return value; } /** * Closes a stream quietly. * * @param stream to close */ private void safeCloseStream(final Closeable stream) { if (stream != null) { try { stream.close(); } catch (final IOException ioe) { } } } /** * Enumerated templates for sql commands. These commands could be moved to * resources for customization. */ enum ScimSqlStatement { /** * Insert into scim resources. */ INSERT("insert into %s (id, externalid, meta, username, name, " + "familyname, givenname, middlename, honorificprefix, " + "honorificsuffix, displayname, nickname, profileurl, title, " + "preferredlanguage, locale, timezone, emails, addresses, " + "phonenumbers, photos, groups, entitlements, roles, website, " + "gender, json) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, " + "?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"), /** * Update scim resource by id. */ UPDATE("update %s set " + "externalid = ?, meta = ?, username = ?, name = ?, familyname = ?, " + "givenname = ?, middlename = ?, honorificprefix = ?, " + "honorificsuffix = ?, displayname = ?, nickname = ?, profileurl = ?, " + "title = ?, preferredlanguage = ?, locale = ?, timezone = ?, " + "emails = ?, addresses = ?, phonenumbers = ?, photos = ?, groups = ?, " + "entitlements = ?, roles = ?, website = ?, gender = ?, json = ? " + "where id = ?"), /** * Delete scim resource by id. */ DELETE("delete from %s where id = ?"), /** * Select all scim resources. */ SELECT_ALL("select * from %s"), /** * Load scim resource by id. */ SELECT_CONTENT("select id, json from %s where id = ?"), /** * Check for scim resource only by id. */ SELECT_ID("select id from %s where id = ?"); /** * Single table containing scim resources. This table is created by the * sql statement in config/create-scim-table.sql. */ public static final String TABLE = "SCIM_RESOURCES"; private final String statement; /** * enum constructor. * * @param statement embedded statement */ ScimSqlStatement(final String statement) { this.statement = statement; } /** * Returns a rendered sql statement. * * @return String containing the rendered sql statement */ public String getStatement() { return String.format(statement, TABLE); } /** * Returns a rendered prepared statement from the supplied parameters. * * @param connection sql connection * @param parameters parameters used for substitutions * @return PreparedStatement rendered result * @throws SQLException if a problem occurs */ public PreparedStatement getPreparedStatement(final Connection connection, final String... parameters) throws SQLException { final PreparedStatement preparedStatement = connection.prepareStatement(getStatement()); if (parameters != null) { int parameterIndex = 1; for (final String parameter : parameters) { preparedStatement.setString(parameterIndex++, parameter); } } return preparedStatement; } } }