001/**
002 * Copyright 2014 Tampere University of Technology, Pori Department
003 * 
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 * 
008 *   http://www.apache.org/licenses/LICENSE-2.0
009 * 
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package service.tut.pori.users.google;
017
018import java.util.Date;
019import java.util.Map;
020import java.util.Map.Entry;
021
022import org.apache.commons.lang3.StringUtils;
023import org.apache.log4j.Logger;
024import org.springframework.beans.factory.annotation.Autowired;
025import org.springframework.jdbc.core.JdbcTemplate;
026import org.springframework.transaction.TransactionStatus;
027import org.springframework.transaction.support.TransactionCallback;
028
029import service.tut.pori.users.UserDAO;
030import core.tut.pori.dao.clause.SQLClause.SQLType;
031import core.tut.pori.dao.SQLDAO;
032import core.tut.pori.users.ExternalAccountConnection;
033import core.tut.pori.users.UserIdentity;
034import core.tut.pori.users.ExternalAccountConnection.UserServiceType;
035import core.tut.pori.utils.NonceUtils;
036import core.tut.pori.utils.NonceUtils.Nonce;
037
038
039/**
040 * DAO for handling authorization tokens for Google User Service.
041 */
042public class GoogleUserDAO extends SQLDAO{
043  private static final Logger LOGGER = Logger.getLogger(GoogleUserDAO.class);
044  /* tables */
045  private static final String TABLE_USERS_GOOGLE = DATABASE+".users_google";
046  /* columns */
047  private static final String COLUMN_ACCESS_TOKEN = "access_token";
048  private static final String COLUMN_ACCESS_TOKEN_EXPIRES = "access_token_expires";
049  private static final String COLUMN_NONCE = "nonce";
050  private static final String COLUMN_NONCE_EXPIRES = "nonce_expires";
051  private static final String COLUMN_REFRESH_TOKEN = "refresh_token";
052  private static final String COLUMN_TOKEN_TYPE = "token_type";
053  /* sql scripts */
054  private static final String SQL_COUNT_USER_ID = "SELECT "+COLUMN_COUNT+" FROM "+TABLE_USERS_GOOGLE+" WHERE "+COLUMN_USER_ID+"=?";
055  private static final int[] SQL_COUNT_USER_ID_SQL_TYPES = new int[]{SQLType.LONG.toInt()};
056  
057  private static final String SQL_INSERT_NONCE = "INSERT INTO "+TABLE_USERS_GOOGLE+" ("+COLUMN_USER_ID+","+COLUMN_NONCE+","+COLUMN_NONCE_EXPIRES+","+COLUMN_ROW_CREATED+") VALUES (?,?,?,NOW())";
058  private static final int[] SQL_INSERT_NONCE_SQL_TYPES = new int[]{SQLType.LONG.toInt(),SQLType.STRING.toInt(),SQLType.LONG.toInt()};
059  
060  private static final String SQL_REMOVE_NONCE = "UPDATE "+TABLE_USERS_GOOGLE+" SET "+COLUMN_NONCE+"=NULL WHERE "+COLUMN_NONCE+"=? LIMIT 1";
061  private static final int[] SQL_REMOVE_NONCE_SQL_TYPES = new int[]{SQLType.STRING.toInt()};
062  
063  private static final String SQL_REMOVE_TOKEN = "DELETE FROM "+TABLE_USERS_GOOGLE+" WHERE "+COLUMN_USER_ID+"=?";
064  private static final int[] SQL_REMOVE_TOKEN_SQL_TYPES = new int[]{SQLType.LONG.toInt()};
065  
066  private static final String SQL_SELECT_USER_ID = "SELECT "+COLUMN_COUNT+", "+COLUMN_USER_ID+" FROM "+TABLE_USERS_GOOGLE+" WHERE "+COLUMN_NONCE+"=? AND "+COLUMN_NONCE_EXPIRES+">? LIMIT 1";
067  private static final int[] SQL_SELECT_USER_ID_SQL_TYPES = new int[]{SQLType.STRING.toInt(),SQLType.LONG.toInt()};
068  
069  private static final String SQL_SELECT_TOKEN = "SELECT "+COLUMN_COUNT+", "+COLUMN_ACCESS_TOKEN+", "+COLUMN_ACCESS_TOKEN_EXPIRES+","+COLUMN_REFRESH_TOKEN+","+COLUMN_TOKEN_TYPE+" FROM "+TABLE_USERS_GOOGLE+" WHERE "+COLUMN_USER_ID+"=? AND ("+COLUMN_ACCESS_TOKEN_EXPIRES+">? OR "+COLUMN_REFRESH_TOKEN+" IS NOT NULL) LIMIT 1";
070  private static final int[] SQL_SELECT_TOKEN_SQL_TYPES = new int[]{SQLType.LONG.toInt(),SQLType.LONG.toInt()}; 
071  
072  /** tokens generated by google should be unique, so the unique column constraint on access token should never match on ON DUPLICATE KEY UPDATE for two different users, the match will be solely done on user id */
073  private static final String SQL_SET_TOKEN = "INSERT INTO "+TABLE_USERS_GOOGLE+" ("+COLUMN_USER_ID+","+COLUMN_ACCESS_TOKEN+","+COLUMN_ACCESS_TOKEN_EXPIRES+","+COLUMN_REFRESH_TOKEN+","+COLUMN_TOKEN_TYPE+","+COLUMN_ROW_CREATED+") VALUES (?,?,?,?,?,NOW()) ON DUPLICATE KEY UPDATE "+COLUMN_ACCESS_TOKEN+"=VALUES("+COLUMN_ACCESS_TOKEN+"), "+COLUMN_ACCESS_TOKEN_EXPIRES+"=VALUES("+COLUMN_ACCESS_TOKEN_EXPIRES+"), "+COLUMN_REFRESH_TOKEN+"=VALUES("+COLUMN_REFRESH_TOKEN+"), "+COLUMN_TOKEN_TYPE+"=VALUES("+COLUMN_TOKEN_TYPE+")";
074  private static final int[] SQL_SET_TOKEN_SQL_TYPES = new int[]{SQLType.LONG.toInt(),SQLType.STRING.toInt(),SQLType.LONG.toInt(),SQLType.STRING.toInt(),SQLType.STRING.toInt()};
075  
076  private static final String SQL_UPDATE_NONCE = "UPDATE "+TABLE_USERS_GOOGLE+" SET "+COLUMN_NONCE+"=?,"+COLUMN_NONCE_EXPIRES+"=? WHERE "+COLUMN_USER_ID+"=?";
077  private static final int[] SQL_UPDATE_NONCE_SQL_TYPES = new int[]{SQLType.STRING.toInt(),SQLType.LONG.toInt(),SQLType.LONG.toInt()};
078  
079  @Autowired
080  private UserDAO _userDAO = null;
081  
082  /**
083   * 
084   * @param googleUserId
085   * @param token
086   * @param userId
087   * @return true on success
088   */
089  public boolean setToken(final String googleUserId, final OAuth2Token token, final UserIdentity userId){
090    if(!UserIdentity.isValid(userId) || !token.isValid() || StringUtils.isBlank(googleUserId)){
091      LOGGER.debug("Invalid userId or token.");
092      return false;
093    }
094
095    return getTransactionTemplate().execute(new TransactionCallback<Boolean>() {
096
097      @Override
098      public Boolean doInTransaction(TransactionStatus status) {
099        JdbcTemplate t = getJdbcTemplate();
100        
101        String oldGoogleUserId = getGoogleUserId(userId);
102        if(oldGoogleUserId == null){
103          LOGGER.debug("Creating a new external account connection for user, id: "+userId.getUserId()+", google user id: "+googleUserId);
104          _userDAO.insertExternalAccountConnection(new ExternalAccountConnection(googleUserId, UserServiceType.GOOGLE), userId);
105        }else if(!oldGoogleUserId.equals(googleUserId)){  // do not allow changing the google user, this may cause undefined behavior on other parts of the system
106          LOGGER.warn("The given google user Id "+googleUserId+" did not match the old google user id: "+oldGoogleUserId);
107          return false;
108        }
109        
110        t.update(SQL_SET_TOKEN, new Object[]{userId.getUserId(),token.getAccessToken(),token.getExpires().getTime(),token.getRefreshToken(),token.getType()}, SQL_SET_TOKEN_SQL_TYPES);
111        return true;
112      }
113    });
114  }
115  
116  /**
117   * 
118   * @param userId non-null, valid userid
119   * @return google user id for the given userId, if known by the system, otherwise returns null
120   */
121  public String getGoogleUserId(UserIdentity userId){
122    ExternalAccountConnection connection = _userDAO.getExternalAccountConnection(userId, UserServiceType.GOOGLE);
123    return (connection == null ? null : connection.getExternalId());
124  }
125  
126  /**
127   * Removes the given nonce if such exists
128   * 
129   * @param nonce
130   */
131  public void removeNonce(String nonce){
132    if(getJdbcTemplate().update(SQL_REMOVE_NONCE, new Object[]{nonce}, SQL_REMOVE_NONCE_SQL_TYPES) < 1){
133      LOGGER.debug("No nonce was cleared.");
134    }else{
135      LOGGER.debug("Nonce cleared.");
136    }
137  }
138  
139  /**
140   * 
141   * @param userId
142   * @return token for the given user or null if none available
143   */
144  public OAuth2Token getToken(UserIdentity userId){
145    if(!UserIdentity.isValid(userId)){
146      LOGGER.debug("Invalid userId.");
147      return null;
148    }
149  
150    OAuth2Token token = new OAuth2Token();
151    Map<String, Object> row = getJdbcTemplate().queryForMap(SQL_SELECT_TOKEN, new Object[]{userId.getUserId(), System.currentTimeMillis()}, SQL_SELECT_TOKEN_SQL_TYPES);
152    for(Entry<String,Object> entry : row.entrySet()){
153      String column = entry.getKey();
154      switch(column){
155        case COLUMN_ACCESS_TOKEN:
156          token.setAccessToken((String) entry.getValue());
157          break;
158        case COLUMN_ACCESS_TOKEN_EXPIRES:
159          token.setExpires(new Date((Long) entry.getValue()));
160          break;
161        case COLUMN_REFRESH_TOKEN:
162          token.setRefreshToken((String) entry.getValue());
163          break;
164        case COLUMN_TOKEN_TYPE:
165          token.setType((String) entry.getValue());
166          break;
167        default:
168          if(checkCountColumn(column, entry.getValue()) < 1){ // if we have only one column, check what it is (should be count)
169            LOGGER.debug("Unknown column name, or no results.");
170            return null;
171          }
172          break;
173      }
174    }
175    return token;
176  }
177  
178  /**
179   * This generates a new nonce from the given UserId, replacing the old one if one was present
180   * 
181   * @param userId
182   * @return new nonce generated for the given userId
183   */
184  public String generateNonce(UserIdentity userId){
185    if(!UserIdentity.isValid(userId)){
186      LOGGER.debug("Invalid userId.");
187      return null;
188    }
189    Nonce nonce = NonceUtils.generateNonce(); // in theory, it is extremely unlikely that the generated nonce will collide with previously generated one, but...
190    if(recordExists(userId.getUserId())){ // ...just in case do not use INSERT ... ON DUPLICATE KEY UPDATE for updating the nonce (as nonce itself is an unique column)
191      getJdbcTemplate().update(SQL_UPDATE_NONCE, new Object[]{nonce.getNonce(),nonce.getExpires(),userId.getUserId()}, SQL_UPDATE_NONCE_SQL_TYPES); // this throw an exception on duplicate nonce if one ever occurs
192    }else{
193      getJdbcTemplate().update(SQL_INSERT_NONCE, new Object[]{userId.getUserId(),nonce.getNonce(),nonce.getExpires()}, SQL_INSERT_NONCE_SQL_TYPES); // this throw an exception on duplicate nonce if one ever occurs
194    }
195    return nonce.getNonce();
196  }
197  
198  /**
199   * remove token from the given userIdentity if one exists
200   * 
201   * @param userId
202   */
203  public void removeToken(UserIdentity userId){
204    if(!UserIdentity.isValid(userId)){
205      LOGGER.debug("Invalid userId.");
206      return;
207    }
208    Long id = userId.getUserId();
209    if(getJdbcTemplate().update(SQL_REMOVE_TOKEN,new Object[]{id},SQL_REMOVE_TOKEN_SQL_TYPES) < 1){
210      LOGGER.debug("No tokens removed for userId: "+id);
211    }else{
212      LOGGER.debug("Token removed for userId: "+id);
213    }
214  }
215  
216  /**
217   * 
218   * @param nonce
219   * @return owner of the given nonce, or null if no such nonce exists or the nonce has expired
220   */
221  public UserIdentity getUser(String nonce){
222    Long userId = (Long) getJdbcTemplate().queryForMap(SQL_SELECT_USER_ID, new Object[]{nonce,System.currentTimeMillis()}, SQL_SELECT_USER_ID_SQL_TYPES).get(COLUMN_USER_ID);
223    return (userId == null ? null : new UserIdentity(userId));
224  }
225  
226  /**
227   * 
228   * @param userId
229   * @return true if a record exists for the given user id
230   */
231  private boolean recordExists(Long userId){
232    return (getJdbcTemplate().queryForObject(SQL_COUNT_USER_ID, new Object[]{userId}, SQL_COUNT_USER_ID_SQL_TYPES, Long.class) > 0);
233  }
234}