Java tutorial
/* * Copyright (C) 2014 University of Washington * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package org.opendatakit.persistence.table; import java.util.Collection; import java.util.List; import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.opendatakit.constants.SecurityConsts; import org.opendatakit.context.CallingContext; import org.opendatakit.odktables.FileManifestManager; import org.opendatakit.odktables.ODKTablesTaskLockType; import org.opendatakit.odktables.OdkTablesLockTemplate; import org.opendatakit.odktables.exception.PermissionDeniedException; import org.opendatakit.odktables.security.OdkTablesUserInfo; import org.opendatakit.persistence.CommonFieldsBase; import org.opendatakit.persistence.DataField; import org.opendatakit.persistence.Datastore; import org.opendatakit.persistence.Query; import org.opendatakit.persistence.Query.FilterOperation; import org.opendatakit.persistence.exception.ODKDatastoreException; import org.opendatakit.persistence.exception.ODKEntityPersistException; import org.opendatakit.persistence.exception.ODKOverQuotaException; import org.opendatakit.persistence.exception.ODKTaskLockException; import org.opendatakit.security.User; import org.opendatakit.security.common.GrantedAuthorityName; import org.springframework.security.access.hierarchicalroles.RoleHierarchy; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; /** * This table holds the ODK Tables-specific settings for a user. * <p> * In particular, it holds the mapping between the internal ODK Aggregate uriUser and the external * ODK Tables-specific USER_ID_EXTERNAL * * @author mitchellsundt@gmail.com * */ public class OdkTablesUserInfoTable extends CommonFieldsBase implements OdkTablesUserInfo { /** * The name of the table into which this data is persisted. */ private static final String TABLE_NAME = "_odktables_user_info"; /** * URI_USER is the PK of the user in the RegisteredUsersTable */ private static final DataField URI_USER = new DataField("URI_USER", DataField.DataType.STRING, true); /** * This is the either the user's username (with username: prefix) or email address (with mailto: * prefix). (whichever is not null) It is configured as the synchronizing account on the device * (devices never see the internal URI (PK) held in ODK Aggregate). */ private static final DataField ODK_TABLES_USER_ID = new DataField("ODK_TABLES_USER_ID", DataField.DataType.STRING, true); /** * This is the phone number of for user's phone. */ private static final DataField PHONE_NUMBER = new DataField("PHONE_NUMBER", DataField.DataType.STRING, true); /** * Additional bearer code that should be sent on authentication line to confirm that the user is * still allowed to access the server. This enables alternative authentication for email accounts * when the internet is not reachable. * * TODO: wire this up */ private static final DataField X_BEARER_CODE = new DataField("X_BEARER_CODE", DataField.DataType.STRING, true); /** * TODO: permissions and permission groups granted to this user */ /** * Construct a relation prototype. It will load a table into the data store layer. * * @param databaseSchema * @param tableName */ private OdkTablesUserInfoTable(String schemaName) { super(schemaName, TABLE_NAME); fieldList.add(URI_USER); fieldList.add(ODK_TABLES_USER_ID); fieldList.add(PHONE_NUMBER); fieldList.add(X_BEARER_CODE); } /** * Construct an empty entity. Only called via {@link #getEmptyRow(User)} * * @param ref * @param user */ protected OdkTablesUserInfoTable(OdkTablesUserInfoTable ref, User user) { super(ref, user); } /** * I'm pretty sure this is returning the prototype, or the empty row. * */ @Override public CommonFieldsBase getEmptyRow(User user) { return new OdkTablesUserInfoTable(this, user); } /** * I copied this from ServerPreferences. I believe this is how you actually add the data into the * table, making it "persist." * * @param cc so you have information about the call * @throws ODKEntityPersistException * @throws ODKOverQuotaException */ public void persist(CallingContext cc) throws ODKEntityPersistException, ODKOverQuotaException { Datastore ds = cc.getDatastore(); User user = cc.getCurrentUser(); ds.putEntity(this, user); } /** * This is the actual prototype. This is the canonical empty row for this table that essentially * serves as the schema. Therefore the table is nothing without this relation being initiated. * * For that reason, it is important to always access the prototype by calling assertRelation() * before trying to manipulate the table. Otherwise you might end up with a table that is empty * and shapeless. */ private static OdkTablesUserInfoTable relation = null; public static void resetSingletonReference() { relation = null; } /** * This must be called to ensure that the datamodel for the table has been initiated. * * @param cc calling context that allows the datastore and user to be determined * @return the prototype, eg the canonical empty row for the table * @throws ODKDatastoreException */ public static synchronized final OdkTablesUserInfoTable assertRelation(CallingContext cc) throws ODKDatastoreException { if (relation == null) { OdkTablesUserInfoTable relationPrototype; Datastore ds = cc.getDatastore(); User user = cc.getUserService().getDaemonAccountUser(); relationPrototype = new OdkTablesUserInfoTable(ds.getDefaultSchemaName()); ds.assertRelation(relationPrototype, user); // may throw exception... // at this point, the prototype has become fully populated relation = relationPrototype; // set static variable only upon success... } return relation; } public static final OdkTablesUserInfoTable getCurrentUserInfo(String uriUser, CallingContext cc) throws ODKDatastoreException { OdkTablesUserInfoTable prototype = OdkTablesUserInfoTable.assertRelation(cc); Datastore ds = cc.getDatastore(); // query for the users Query query = ds.createQuery(prototype, "OdkTablesUserInfoTable.getUserData", cc.getCurrentUser()); query.addFilter(URI_USER, FilterOperation.EQUAL, uriUser); List<? extends CommonFieldsBase> results = query.executeQuery(); if (results.size() == 0) { return null; } if (results.size() == 1) { return (OdkTablesUserInfoTable) results.get(0); } throw new ODKDatastoreException( "Unexpected state: " + results.size() + " OdkTablesUserInfoTable records matching " + uriUser); } public static final boolean deleteOdkTablesUser(String uriUser, CallingContext cc) throws ODKDatastoreException { OdkTablesUserInfoTable userToDelete = OdkTablesUserInfoTable.getCurrentUserInfo(uriUser, cc); cc.getDatastore().deleteEntity(userToDelete.getEntityKey(), cc.getCurrentUser()); // TODO: delete the ACLs for this user??? return true; } public static synchronized final OdkTablesUserInfoTable getOdkTablesUserInfo(String uriUser, Set<GrantedAuthority> grants, CallingContext callingContext) throws ODKDatastoreException, ODKTaskLockException, ODKEntityPersistException, ODKOverQuotaException, PermissionDeniedException { Datastore ds = callingContext.getDatastore(); OdkTablesUserInfoTable prototype = OdkTablesUserInfoTable.assertRelation(callingContext); Log log = LogFactory.getLog(FileManifestManager.class); log.info("TablesUserPermissionsImpl: " + uriUser); RoleHierarchy roleHierarchy = (RoleHierarchy) callingContext.getHierarchicalRoleRelationships(); Collection<? extends GrantedAuthority> roles = roleHierarchy.getReachableGrantedAuthorities(grants); boolean hasSynchronize = roles .contains(new SimpleGrantedAuthority(GrantedAuthorityName.ROLE_SYNCHRONIZE_TABLES.name())); boolean hasSuperUser = roles .contains(new SimpleGrantedAuthority(GrantedAuthorityName.ROLE_SUPER_USER_TABLES.name())); boolean hasAdminister = roles .contains(new SimpleGrantedAuthority(GrantedAuthorityName.ROLE_ADMINISTER_TABLES.name())); if (hasSynchronize || hasSuperUser || hasAdminister) { String uriForUser = null; String externalUID = null; if (uriUser.equals(User.ANONYMOUS_USER)) { externalUID = User.ANONYMOUS_USER; uriForUser = User.ANONYMOUS_USER; } else { RegisteredUsersTable user = RegisteredUsersTable.getUserByUri(uriUser, ds, callingContext.getCurrentUser()); // Determine the external UID that will identify this user externalUID = null; if (user.getUsername() != null) { externalUID = SecurityConsts.USERNAME_COLON + user.getUsername(); } uriForUser = uriUser; } OdkTablesUserInfoTable odkTablesUserInfo = null; odkTablesUserInfo = OdkTablesUserInfoTable.getCurrentUserInfo(uriForUser, callingContext); if (odkTablesUserInfo == null) { // // GAIN LOCK OdkTablesLockTemplate tablesUserPermissions = new OdkTablesLockTemplate(externalUID, ODKTablesTaskLockType.TABLES_USER_PERMISSION_CREATION, OdkTablesLockTemplate.DelayStrategy.SHORT, callingContext); try { tablesUserPermissions.acquire(); // attempt to re-fetch the record. // If this succeeds, then we had multiple suitors; the other one beat // us. odkTablesUserInfo = OdkTablesUserInfoTable.getCurrentUserInfo(uriForUser, callingContext); if (odkTablesUserInfo != null) { return odkTablesUserInfo; } // otherwise, create a record odkTablesUserInfo = ds.createEntityUsingRelation(prototype, callingContext.getCurrentUser()); odkTablesUserInfo.setUriUser(uriForUser); odkTablesUserInfo.setOdkTablesUserId(externalUID); odkTablesUserInfo.persist(callingContext); return odkTablesUserInfo; } finally { tablesUserPermissions.release(); } } else { return odkTablesUserInfo; } } else { throw new PermissionDeniedException("User does not have access to ODK Tables"); } } /** * Get the aggregate userid. */ public String getUriUser() { return getStringField(URI_USER); } @Override public String getOdkTablesUserId() { return getStringField(ODK_TABLES_USER_ID); } @Override public String getPhoneNumber() { return getStringField(PHONE_NUMBER); } @Override public String getXBearerCode() { return getStringField(X_BEARER_CODE); } /** * Set the uriUser. * * @ throws IllegalArgumentException if the value cannot be set */ public void setUriUser(String uriUser) { if (!setStringField(URI_USER, uriUser)) { throw new IllegalArgumentException("overflow uriUser"); } } /** * Set the ODK Tables user id. * * @throws IllegalArgumentException if the value cannot be set, most likely due to overflow. */ public void setOdkTablesUserId(String odkTablesUserId) { if (!(odkTablesUserId.startsWith(SecurityConsts.USERNAME_COLON) || odkTablesUserId.equals(User.ANONYMOUS_USER))) { throw new IllegalArgumentException("ODK Tables User Id does not start with " + SecurityConsts.USERNAME_COLON + " or is not " + User.ANONYMOUS_USER); } if (!setStringField(ODK_TABLES_USER_ID, odkTablesUserId)) { throw new IllegalArgumentException("overflow external odkTablesUserId"); } } /** * Set the Phone Number * * @ throws IllegalArgumentException if the value cannot be set */ public void setPhoneNumber(String phoneNumber) { if (!setStringField(PHONE_NUMBER, phoneNumber)) { throw new IllegalArgumentException("overflow phoneNumber"); } } /** * Set the X-Bearer-Code * * @ throws IllegalArgumentException if the value cannot be set */ public void setXBearerCode(String xBearerCode) { if (!setStringField(X_BEARER_CODE, xBearerCode)) { throw new IllegalArgumentException("overflow XBearerCode"); } } }