/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.smscserver.usermanager.impl;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import org.apache.smscserver.SmscServerConfigurationException;
import org.apache.smscserver.smsclet.Authentication;
import org.apache.smscserver.smsclet.AuthenticationFailedException;
import org.apache.smscserver.smsclet.Authority;
import org.apache.smscserver.smsclet.SmscException;
import org.apache.smscserver.smsclet.User;
import org.apache.smscserver.usermanager.PasswordEncryptor;
import org.apache.smscserver.usermanager.PropertiesUserManagerFactory;
import org.apache.smscserver.usermanager.UsernamePasswordAuthentication;
import org.apache.smscserver.util.BaseProperties;
import org.apache.smscserver.util.IoUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* <strong>Internal class, do not use directly.</strong>
*
* <p>
* Properties file based <code>UserManager</code> implementation. We use <code>users.properties</code> file to store user
* data.
* </p>
*
* </p>The file will use the following properties for storing users:</p>
* <table>
* <tr>
* <th>Property</th>
* <th>Documentation</th>
* <tr>
* <td>smscserver.user.{systemid}.userpassword</td>
* <td>The password for the user. Can be in clear text, MD5 hash or salted SHA hash based on the configuration on the
* user manager</td>
* </tr>
* <tr>
* <td>smscserver.user.{systemid}.enableflag</td>
* <td>true if the user is enabled, false otherwise</td>
* </tr>
* <tr>
* <td>smscserver.user.{systemid}.idletime</td>
* <td>The number of seconds the user is allowed to be idle before disconnected. 0 disables the idle timeout</td>
* </tr>
* <tr>
* <td>smscserver.user.{systemid}.maxbindnumber</td>
* <td>The maximum number of concurrent binds by the user. 0 disables the check.</td>
* </tr>
* <tr>
* <td>smscserver.user.{systemid}.maxbindperip</td>
* <td>The maximum number of concurrent binds from the same IP address by the user. 0 disables the check.</td>
* </tr>
* </table>
*
* <p>
* Example:
* </p>
*
* <pre>
* smscserver.user.admin.userpassword=admin
* smscserver.user.admin.enableflag=true
* smscserver.user.admin.idletime=0
* smscserver.user.admin.maxbindnumber=0
* smscserver.user.admin.maxbindperip=0
* </pre>
*
* @author hceylan
*/
public class PropertiesUserManager extends AbstractUserManager {
private static final Logger LOG = LoggerFactory.getLogger(PropertiesUserManager.class);
private final static String PREFIX = "smscserver.user.";
private BaseProperties userDataProp;
private File userDataFile;
private URL userUrl;
/**
* Internal constructor, do not use directly. Use {@link PropertiesUserManagerFactory} instead.
*/
public PropertiesUserManager(PasswordEncryptor passwordEncryptor, File userDataFile, String adminName) {
super(adminName, passwordEncryptor);
this.loadFromFile(userDataFile);
}
/**
* Internal constructor, do not use directly. Use {@link PropertiesUserManagerFactory} instead.
*/
public PropertiesUserManager(PasswordEncryptor passwordEncryptor, URL userDataPath, String adminName) {
super(adminName, passwordEncryptor);
this.loadFromUrl(userDataPath);
}
/**
* Delete an user. Removes all this user entries from the properties. After removing the corresponding from the
* properties, save the data.
*/
public void delete(String usrName) throws SmscException {
// remove entries from properties
String thisPrefix = PropertiesUserManager.PREFIX + usrName;
Enumeration<?> propNames = this.userDataProp.propertyNames();
ArrayList<String> remKeys = new ArrayList<String>();
while (propNames.hasMoreElements()) {
String thisKey = propNames.nextElement().toString();
if (thisKey.startsWith(thisPrefix)) {
remKeys.add(thisKey);
}
}
Iterator<String> remKeysIt = remKeys.iterator();
while (remKeysIt.hasNext()) {
this.userDataProp.remove(remKeysIt.next());
}
this.saveUserData();
}
/**
* Close the user manager - remove existing entries.
*/
public synchronized void dispose() {
if (this.userDataProp != null) {
this.userDataProp.clear();
this.userDataProp = null;
}
}
/**
* User existance check
*/
public boolean doesExist(String name) {
String key = PropertiesUserManager.PREFIX + name;
return this.userDataProp.containsKey(key);
}
/**
* Get all user names.
*/
public String[] getAllUserNames() {
// get all user names
String suffix = '.' + AbstractUserManager.ATTR_SYSTEM_ID;
ArrayList<String> ulst = new ArrayList<String>();
Enumeration<?> allKeys = this.userDataProp.propertyNames();
int prefixlen = PropertiesUserManager.PREFIX.length();
int suffixlen = suffix.length();
while (allKeys.hasMoreElements()) {
String key = (String) allKeys.nextElement();
if (key.endsWith(suffix)) {
String name = key.substring(prefixlen);
int endIndex = name.length() - suffixlen;
name = name.substring(0, endIndex);
ulst.add(name);
}
}
Collections.sort(ulst);
return ulst.toArray(new String[0]);
}
/**
* Retrive the file backing this user manager
*
* @return The file
*/
public File getFile() {
return this.userDataFile;
}
/**
* Get user password. Returns the encrypted value.
*
* <pre>
* If the password value is not null
* password = new password
* else
* if user does exist
* password = old password
* else
* password = ""
* </pre>
*/
private String getPassword(User usr) {
String name = usr.getName();
String password = usr.getPassword();
if (password != null) {
password = this.getPasswordEncryptor().encrypt(password);
} else {
String blankPassword = this.getPasswordEncryptor().encrypt("");
if (this.doesExist(name)) {
String key = PropertiesUserManager.PREFIX + name + '.' + AbstractUserManager.ATTR_PASSWORD;
password = this.userDataProp.getProperty(key, blankPassword);
} else {
password = blankPassword;
}
}
return password;
}
/**
* Load user data.
*/
public User getUserByName(String userName) {
if (!this.doesExist(userName)) {
return null;
}
String baseKey = PropertiesUserManager.PREFIX + userName + '.';
BaseUser user = new BaseUser();
user.setName(userName);
user.setEnabled(this.userDataProp.getBoolean(baseKey + AbstractUserManager.ATTR_ENABLE, true));
List<Authority> authorities = new ArrayList<Authority>();
int maxBind = this.userDataProp.getInteger(baseKey + AbstractUserManager.ATTR_MAX_BIND_NUMBER, 0);
int maxBindPerIP = this.userDataProp.getInteger(baseKey + AbstractUserManager.ATTR_MAX_BIND_PER_IP, 0);
authorities.add(new ConcurrentBindPermission(maxBind, maxBindPerIP));
user.setAuthorities(authorities);
user.setMaxIdleTime(this.userDataProp.getInteger(baseKey + AbstractUserManager.ATTR_MAX_IDLE_TIME, 0));
return user;
}
@Override
protected User internalAuthenticate(Authentication authentication) throws AuthenticationFailedException {
if (authentication instanceof UsernamePasswordAuthentication) {
UsernamePasswordAuthentication upauth = (UsernamePasswordAuthentication) authentication;
String username = upauth.getUsername();
String password = upauth.getPassword();
if (username == null) {
throw new AuthenticationFailedException("Authentication failed");
}
if (password == null) {
password = "";
}
String storedPassword = this.userDataProp.getProperty(PropertiesUserManager.PREFIX + username + '.'
+ AbstractUserManager.ATTR_PASSWORD);
if (storedPassword == null) {
// user does not exist
throw new AuthenticationFailedException("Authentication failed");
}
if (this.getPasswordEncryptor().matches(password, storedPassword)) {
User user = this.getUserByName(username);
this.authorizeConcurency(authentication, user);
return user;
} else {
throw new AuthenticationFailedException("Authentication failed");
}
} else {
throw new IllegalArgumentException("Authentication not supported by this user manager");
}
}
private void loadFromFile(File userDataFile) {
try {
this.userDataProp = new BaseProperties();
if (userDataFile != null) {
PropertiesUserManager.LOG.debug("File configured, will try loading");
if (userDataFile.exists()) {
this.userDataFile = userDataFile;
PropertiesUserManager.LOG.debug("File found on file system");
FileInputStream fis = null;
try {
fis = new FileInputStream(userDataFile);
this.userDataProp.load(fis);
} finally {
IoUtils.close(fis);
}
} else {
// try loading it from the classpath
PropertiesUserManager.LOG.debug("File not found on file system, try loading from classpath");
InputStream is = this.getClass().getClassLoader().getResourceAsStream(userDataFile.getPath());
if (is != null) {
try {
this.userDataProp.load(is);
} finally {
IoUtils.close(is);
}
} else {
throw new SmscServerConfigurationException(
"User data file specified but could not be located, "
+ "neither on the file system or in the classpath: " + userDataFile.getPath());
}
}
}
this.sanitize();
} catch (IOException e) {
throw new SmscServerConfigurationException("Error loading user data file : " + userDataFile, e);
}
}
private void loadFromUrl(URL userDataPath) {
try {
this.userDataProp = new BaseProperties();
if (userDataPath != null) {
PropertiesUserManager.LOG.debug("URL configured, will try loading");
this.userUrl = userDataPath;
InputStream is = null;
is = userDataPath.openStream();
try {
this.userDataProp.load(is);
} finally {
IoUtils.close(is);
}
}
this.sanitize();
} catch (IOException e) {
throw new SmscServerConfigurationException("Error loading user data resource : " + userDataPath, e);
}
}
/**
* Reloads the contents of the users.properties file. This allows any manual modifications to the file to be
* recognised by the running server.
*/
public void refresh() {
synchronized (this.userDataProp) {
if (this.userDataFile != null) {
PropertiesUserManager.LOG.debug("Refreshing user manager using file: "
+ this.userDataFile.getAbsolutePath());
this.loadFromFile(this.userDataFile);
} else {
// file is null, must have been created using URL
PropertiesUserManager.LOG.debug("Refreshing user manager using URL: " + this.userUrl.toString());
this.loadFromUrl(this.userUrl);
}
}
}
private void sanitize() {
@SuppressWarnings("unchecked")
Enumeration<String> e = (Enumeration<String>) this.userDataProp.propertyNames();
while (e.hasMoreElements()) {
String property = e.nextElement();
property = property.substring(PropertiesUserManager.PREFIX.length());
String[] split = property.split("\\.");
if (split.length == 1) {
continue;
}
property = PropertiesUserManager.PREFIX + split[0];
if (!this.userDataProp.containsKey(property)) {
this.userDataProp.put(property, new String());
}
}
}
/**
* Save user data. Store the properties.
*/
public synchronized void save(User usr) throws SmscException {
// null value check
if (usr.getName() == null) {
throw new NullPointerException("User name is null.");
}
String thisPrefix = PropertiesUserManager.PREFIX + usr.getName();
// save the username
this.userDataProp.put(thisPrefix, new String());
thisPrefix = thisPrefix + ".";
// set other properties
this.userDataProp.setProperty(thisPrefix + AbstractUserManager.ATTR_PASSWORD, this.getPassword(usr));
this.userDataProp.setProperty(thisPrefix + AbstractUserManager.ATTR_ENABLE, usr.getEnabled());
this.userDataProp.setProperty(thisPrefix + AbstractUserManager.ATTR_MAX_IDLE_TIME, usr.getMaxIdleTime());
// request that always will succeed
ConcurrentBindRequest concurrentBindRequest = new ConcurrentBindRequest(0, 0);
concurrentBindRequest = (ConcurrentBindRequest) usr.authorize(concurrentBindRequest);
if (concurrentBindRequest != null) {
this.userDataProp.setProperty(thisPrefix + AbstractUserManager.ATTR_MAX_BIND_NUMBER,
concurrentBindRequest.getMaxConcurrentBinds());
this.userDataProp.setProperty(thisPrefix + AbstractUserManager.ATTR_MAX_BIND_PER_IP,
concurrentBindRequest.getMaxConcurrentBindsPerIP());
} else {
this.userDataProp.remove(thisPrefix + AbstractUserManager.ATTR_MAX_BIND_NUMBER);
this.userDataProp.remove(thisPrefix + AbstractUserManager.ATTR_MAX_BIND_PER_IP);
}
this.saveUserData();
}
/**
* @throws SmscException
*/
private void saveUserData() throws SmscException {
if (this.userDataFile == null) {
return;
}
File dir = this.userDataFile.getAbsoluteFile().getParentFile();
if ((dir != null) && !dir.exists() && !dir.mkdirs()) {
String dirName = dir.getAbsolutePath();
throw new SmscServerConfigurationException("Cannot create directory for user data file : " + dirName);
}
// save user data
FileOutputStream fos = null;
try {
fos = new FileOutputStream(this.userDataFile);
this.userDataProp.store(fos, "Generated file - don't edit (please)");
} catch (IOException ex) {
PropertiesUserManager.LOG.error("Failed saving user data", ex);
throw new SmscException("Failed saving user data", ex);
} finally {
IoUtils.close(fos);
}
}
}