/* * Copyright 2004 - 2008 Christian Sprajc. All rights reserved. * * This file is part of PowerFolder. * * PowerFolder is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation. * * PowerFolder is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with PowerFolder. If not, see <http://www.gnu.org/licenses/>. * * $Id$ */ package de.dal33t.powerfolder.security; import java.io.IOException; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.logging.Level; import java.util.logging.Logger; import javax.persistence.Column; import javax.persistence.Embedded; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.JoinTable; import javax.persistence.ManyToMany; import javax.persistence.ManyToOne; import javax.persistence.Transient; import org.hibernate.annotations.BatchSize; import org.hibernate.annotations.Cache; import org.hibernate.annotations.CacheConcurrencyStrategy; import org.hibernate.annotations.Cascade; import org.hibernate.annotations.CascadeType; import org.hibernate.annotations.CollectionOfElements; import org.hibernate.annotations.Fetch; import org.hibernate.annotations.FetchMode; import org.hibernate.annotations.Index; import org.hibernate.annotations.IndexColumn; import org.hibernate.annotations.LazyCollection; import org.hibernate.annotations.LazyCollectionOption; import org.hibernate.annotations.Type; import org.hibernate.annotations.TypeDef; import de.dal33t.powerfolder.Constants; import de.dal33t.powerfolder.Controller; import de.dal33t.powerfolder.disk.Folder; import de.dal33t.powerfolder.disk.SyncProfile; import de.dal33t.powerfolder.light.AccountInfo; import de.dal33t.powerfolder.light.FolderInfo; import de.dal33t.powerfolder.light.MemberInfo; import de.dal33t.powerfolder.light.ServerInfo; import de.dal33t.powerfolder.util.Format; import de.dal33t.powerfolder.util.IdGenerator; import de.dal33t.powerfolder.util.LoginUtil; import de.dal33t.powerfolder.util.Reject; import de.dal33t.powerfolder.util.StringUtils; import de.dal33t.powerfolder.util.db.PermissionUserType; /** * A access to the system indentified by username & password. * * @author <a href="mailto:totmacher@powerfolder.com">Christian Sprajc</a> * @version $Revision: 1.5 $ */ @TypeDef(name = "permissionType", typeClass = PermissionUserType.class) @Entity @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) public class Account implements Serializable { private static final Logger LOG = Logger.getLogger(Account.class.getName()); private static final long serialVersionUID = 100L; // Properties public static final String PROPERTYNAME_OID = "oid"; public static final String PROPERTYNAME_USERNAME = "username"; public static final String PROPERTYNAME_PASSWORD = "password"; public static final String PROPERTYNAME_LDAPDN = "ldapDN"; public static final String PROPERTYNAME_LANGUAGE = "language"; public static final String PROPERTYNAME_PERMISSIONS = "permissions"; public static final String PROPERTYNAME_REGISTER_DATE = "registerDate"; public static final String PROPERTYNAME_LAST_LOGIN_DATE = "lastLoginDate"; public static final String PROPERTYNAME_LAST_LOGIN_FROM = "lastLoginFrom"; public static final String PROPERTYNAME_NEWSLETTER = "newsLetter"; public static final String PROPERTYNAME_PRO_USER = "proUser"; public static final String PROPERTYNAME_NOTES = "notes"; public static final String PROPERTYNAME_SERVER = "server"; public static final String PROPERTYNAME_DEFAULT_SYNCHRONIZED_FOLDER = "defaultSynchronizedFolder"; public static final String PROPERTYNAME_OS_SUBSCRIPTION = "osSubscription"; public static final String PROPERTYNAME_LICENSE_KEY_FILES = "licenseKeyFiles"; public static final String PROPERTYNAME_COMPUTERS = "computers"; public static final String PROPERTYNAME_GROUPS = "groups"; @Id private String oid; @Index(name = "IDX_USERNAME") @Column(nullable = false, unique = true) private String username; private String password; private String language; @Index(name = "IDX_LDAPDN") @Column(length = 512) private String ldapDN; private Date registerDate; private Date lastLoginDate; @ManyToOne @JoinColumn(name = "lastLoginFrom_id") private MemberInfo lastLoginFrom; private boolean proUser; // PFS-605 private String custom1; private String custom2; private String custom3; @Column(length = 1024) private String notes; /** * The list of computers associated with this account. */ @ManyToMany @JoinTable(name = "Account_Computers", joinColumns = @JoinColumn(name = "oid"), inverseJoinColumns = @JoinColumn(name = "id")) @BatchSize(size = 1337) @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) @LazyCollection(LazyCollectionOption.FALSE) private Collection<MemberInfo> computers; /** * Server where the folders of this account are hosted on. */ @ManyToOne @JoinColumn(name = "serverInfo_id") private ServerInfo server; @Deprecated @Transient private Collection<String> licenseKeyFiles; /** * The possible license key files of this account. * <code>AccountService.getValidLicenseKey</code>. */ @CollectionOfElements @IndexColumn(name = "IDX_LICENSE", base = 0, nullable = false) @Cascade(value = {CascadeType.ALL}) @BatchSize(size = 1337) @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) @LazyCollection(LazyCollectionOption.FALSE) private List<String> licenseKeyFileList; /** * The maximum number of devices to automatically re-license if required */ private int autoRenewDevices; /** * Don't auto re-license after that date. */ private Date autoRenewTill; /** * The default-synced folder of the user. May be null. * <p> * TRAC #991. */ @ManyToOne @JoinColumn(name = "defaultSyncFolder_id") private FolderInfo defaultSynchronizedFolder; @CollectionOfElements @Type(type = "permissionType") @BatchSize(size = 1337) @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) @LazyCollection(LazyCollectionOption.FALSE) private Collection<Permission> permissions; @ManyToMany @JoinTable(name = "Account_Groups", joinColumns = @JoinColumn(name = "Account_oid"), inverseJoinColumns = @JoinColumn(name = "AGroup_oid")) @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) @LazyCollection(LazyCollectionOption.FALSE) private Collection<Group> groups; /** * The possible email address of this account. */ @CollectionOfElements @IndexColumn(name = "IDX_EMAIL", base = 0, nullable = false) @Cascade(value = {CascadeType.ALL}) @BatchSize(size = 1337) @Column(name = "element", length = 512) @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) @LazyCollection(LazyCollectionOption.FALSE) private List<String> emails; @Embedded @Fetch(FetchMode.JOIN) private OnlineStorageSubscription osSubscription; Account() { // Generate unique id this(IdGenerator.makeId()); } Account(String oid) { Reject.ifBlank(oid, "OID"); this.oid = oid; this.permissions = new CopyOnWriteArrayList<Permission>(); this.osSubscription = new OnlineStorageSubscription(); this.licenseKeyFiles = new CopyOnWriteArrayList<String>(); this.computers = new CopyOnWriteArrayList<MemberInfo>(); this.licenseKeyFileList = new CopyOnWriteArrayList<String>(); this.groups = new CopyOnWriteArrayList<Group>(); this.emails = new CopyOnWriteArrayList<String>(); } /** * @return a leightweight/reference object to this account. */ public AccountInfo createInfo() { return new AccountInfo(oid, username, getDisplayName()); } // Basic permission stuff ************************************************* public synchronized void grant(Permission... newPermissions) { Reject.ifNull(newPermissions, "Permission is null"); for (Permission p : newPermissions) { if (hasPermission(p)) { // Skip continue; } if (p instanceof FolderPermission) { FolderInfo foInfo = ((FolderPermission) p).getFolder(); revokeAllFolderPermission(foInfo); if (foInfo.isMetaFolder()) { LOG.severe(this + ": Not allowed to grant permissions " + foInfo); continue; } } permissions.add(p); } LOG.fine("Granted permission to " + this + ": " + Arrays.asList(newPermissions)); } public synchronized void revoke(Permission... revokePermissions) { Reject.ifNull(revokePermissions, "Permission is null"); for (Permission p : revokePermissions) { if (permissions.remove(p)) { LOG.fine("Revoked permission from " + this + ": " + p); } } } /** * Revokes any permission to a folders. * * @param foInfo * the folder. */ public void revokeAllFolderPermission(FolderInfo foInfo) { revoke(FolderPermission.read(foInfo), FolderPermission.readWrite(foInfo), FolderPermission.admin(foInfo), FolderPermission.owner(foInfo)); } /** * Revokes permission on ALL folders */ public void revokeAllFolderPermission() { // Revokes permission on ALL folders for (Permission p : permissions) { if (p instanceof FolderPermission) { revoke(p); } } } public synchronized void revokeAllPermissions() { if (LOG.isLoggable(Level.FINE)) { LOG.fine("Revoking all permission from " + this + ": " + permissions); } permissions.clear(); } public boolean hasPermission(Permission permission) { Reject.ifNull(permission, "Permission is null"); if (permissions == null) { LOG.severe("Illegal account " + username + ", permissions is null"); return false; } for (Permission p : permissions) { if (p == null) { LOG.severe("Got null permission on " + this); continue; } if (p.equals(permission)) { return true; } if (p.implies(permission)) { return true; } } for (Group g : groups) { if (g.hasPermission(permission)) { return true; } } return false; } public Collection<Permission> getPermissions() { return Collections.unmodifiableCollection(permissions); } /** * @param folder * @return the permission on the given folder. AccessMode.NO_ACCESS for no * access. */ public AccessMode getAllowedAccess(FolderInfo folder) { if (hasPermission(FolderPermission.owner(folder))) { return FolderPermission.owner(folder).getMode(); } else if (hasPermission(FolderPermission.admin(folder))) { return FolderPermission.admin(folder).getMode(); } else if (hasPermission(FolderPermission.readWrite(folder))) { return FolderPermission.readWrite(folder).getMode(); } else if (hasPermission(FolderPermission.read(folder))) { return FolderPermission.read(folder).getMode(); } return AccessMode.NO_ACCESS; } /** * @return the list of folders this account gets charged for. */ public Collection<FolderInfo> getFoldersCharged() { if (permissions.isEmpty()) { return Collections.emptyList(); } Collection<FolderInfo> folders = new ArrayList<FolderInfo>( permissions.size()); for (Permission p : permissions) { if (p instanceof FolderOwnerPermission) { FolderPermission fp = (FolderPermission) p; folders.add(fp.getFolder()); } } for (Group g : groups) { for (Permission p : g.getPermissions()) { if (p instanceof FolderOwnerPermission) { FolderPermission fp = (FolderPermission) p; folders.add(fp.getFolder()); } } } return folders; } /** * @return all folders the account has directly at folder read permission * granted. */ public Collection<FolderInfo> getFolders() { List<FolderInfo> folderInfos = new ArrayList<FolderInfo>( permissions.size()); for (Permission permission : permissions) { if (permission instanceof FolderPermission) { FolderPermission fp = (FolderPermission) permission; folderInfos.add(fp.getFolder()); } } for (Group g : groups) { for (Permission p : g.getPermissions()) { if (p instanceof FolderPermission) { FolderPermission fp = (FolderPermission) p; folderInfos.add(fp.getFolder()); } } } return folderInfos; } public void internFolderInfos() { for (Permission permission : permissions) { if (permission instanceof FolderPermission) { FolderPermission fp = (FolderPermission) permission; fp.folder = fp.folder.intern(); } } } public void addGroup(Group... group) { Reject.ifNull(group, "Group is null"); for (Group g : group) { if (!groups.contains(g)) { groups.add(g); } } } public void removeGroup(Group... group) { Reject.ifNull(group, "Group is null"); for (Group g : group) { groups.remove(g); } } // Accessing / API ******************************************************** /** * @return true if this is a valid account */ public boolean isValid() { return username != null; } public String getOID() { return oid; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getDisplayName() { // TODO Rework completely if (authByShibboleth() && !emails.isEmpty()) { return emails.get(0); } return username; } public String getPassword() { return password; } public void setPasswordPlain(String prefix, String password) { this.password = prefix + password; } public void setPasswordSalted(String password) { this.password = LoginUtil.hashAndSalt(password); } /** * @param pwCandidate * @return true if the password candidate matches the password of the * account. */ public boolean passwordMatches(char[] pwCandidate) { return LoginUtil.matches(pwCandidate, password); } /** * setLanguage Set account language * * @return Selected language */ public String getLanguage() { return this.language; } /** * setLanguage Set account language * * @param lang * New language */ public void setLanguage(String lang) { this.language = lang; } public String getLdapDN() { return ldapDN; } public void setLdapDN(String ldapDN) { System.err.println("Setting LDAP DN to: " + ldapDN); this.ldapDN = ldapDN; } public Date getRegisterDate() { return registerDate; } public void setRegisterDate(Date registerDate) { this.registerDate = registerDate; } public OnlineStorageSubscription getOSSubscription() { if (osSubscription == null) { // osSubscription = new OnlineStorageSubscription(); // osSubscription.setType(OnlineStorageSubscriptionType.TRIAL_5GB); } return osSubscription; } public void setOSSubscription(OnlineStorageSubscription osSubscription) { this.osSubscription = osSubscription; } public String getNotes() { return notes; } public void setNotes(String notes) { this.notes = notes; } public String getCustom1() { return custom1; } public void setCustom1(String custom1) { this.custom1 = custom1; } public String getCustom2() { return custom2; } public void setCustom2(String custom2) { this.custom2 = custom2; } public String getCustom3() { return custom3; } public void setCustom3(String custom3) { this.custom3 = custom3; } public boolean authByShibboleth() { // Fine a better way: return username.contains(Constants.SHIBBOLETH_USERNAME_SEPARATOR); } public boolean authByLDAP() { return StringUtils.isNotBlank(ldapDN); } public boolean authByRADIUS() { // Fine a better way: return notes != null && notes.toLowerCase().contains("radius"); } public boolean authByDatabase() { return !authByLDAP() && !authByRADIUS() && !authByShibboleth(); } // PFS-742: TODO Add EXTRA Field for this later public boolean isSendEmail() { if (StringUtils.isBlank(custom2)) { return true; } return !custom2.toUpperCase().contains("NOEMAIL"); } public void setSendEmail(boolean sendEmail) { if (!sendEmail) { custom2 = "NOEMAIL"; } else { custom2 = null; } } /** * Adds a line of info with the current date to the notes of that account. * * @param infoText */ public void addNotesWithDate(String infoText) { if (StringUtils.isBlank(infoText)) { return; } String infoLine = Format.formatDateCanonical(new Date()); infoLine += ": "; infoLine += infoText; if (StringUtils.isBlank(notes)) { setNotes(infoLine); } else { setNotes(notes + "\n" + infoLine); } } public ServerInfo getServer() { return server; } public void setServer(ServerInfo server) { this.server = server; } public FolderInfo getDefaultSynchronizedFolder() { return defaultSynchronizedFolder; } public void setDefaultSynchronizedFolder( FolderInfo defaultSynchronizedFolder) { this.defaultSynchronizedFolder = defaultSynchronizedFolder; } public MemberInfo getLastLoginFrom() { return lastLoginFrom; } /** * @param lastLoginFrom * @return true if this is a new computer. false if not. */ public boolean setLastLoginFrom(MemberInfo lastLoginFrom) { this.lastLoginFrom = lastLoginFrom; // Set login date touchLogin(); // Ensure initialization getComputers(); if (lastLoginFrom != null && !computers.contains(lastLoginFrom)) { computers.add(lastLoginFrom); return true; } return false; } public Date getLastLoginDate() { return lastLoginDate; } /** * Sets the last login date to NOW. */ public void touchLogin() { lastLoginDate = new Date(); } /** * @return the computers this account is associated with. */ public Collection<MemberInfo> getComputers() { return computers; } public Collection<Group> getGroups() { return Collections.unmodifiableCollection(groups); } public void addLicenseKeyFile(String filename) { if (licenseKeyFileList.contains(filename)) { return; } licenseKeyFileList.add(filename); } public List<String> getLicenseKeyFiles() { return licenseKeyFileList; } public boolean addEmail(String email) { Reject.ifBlank(email, "Email"); email = email.trim().toLowerCase(); if (emails.contains(email)) { return false; } emails.add(email); return true; } public boolean removeEmail(String email) { Reject.ifBlank(email, "Email"); return emails.remove(email.toLowerCase().trim()); } public List<String> getEmails() { return Collections.unmodifiableList(emails); } public int getAutoRenewDevices() { return autoRenewDevices; } public Date getAutoRenewTill() { return autoRenewTill; } public boolean isProUser() { return proUser; } public void setAutoRenew(int autoRenewDevices, Date autoRenewTill, boolean proUser) { this.autoRenewDevices = autoRenewDevices; this.autoRenewTill = autoRenewTill; this.proUser = proUser; } public boolean willAutoRenew() { if (autoRenewDevices <= 0) { return false; } if (autoRenewTill == null) { return true; } return autoRenewTill.after(new Date()); } /** * @return the days since the user has registered */ public int getDaysSinceRegistration() { if (registerDate == null) { return -1; } long daysSinceRegistration = (System.currentTimeMillis() - registerDate .getTime()) / (1000L * 60 * 60 * 24); return (int) daysSinceRegistration; } public String toString() { return "Account '" + username + "', " + permissions.size() + " permissions"; } public String toDetailString() { return toString() + ", pro? " + proUser + ", regdate: " + Format.formatDateShort(registerDate) + ", licenses: " + (licenseKeyFileList != null ? licenseKeyFileList.size() : "n/a") + ", " + osSubscription; } // Convenience/Applogic *************************************************** /** * Enables the selected account: * <p> * The Online Storage subscription * <P> * Sets all folders to SyncProfile.BACKUP_TARGET. * <p> * FIXME: Does only set the folders hosted on the CURRENT server to backup. * <p> * Account needs to be stored afterwards!! * * @param controller * the controller */ public void enable(Controller controller) { Reject.ifNull(controller, "Controller is null"); getOSSubscription().setWarnedUsageDate(null); getOSSubscription().setDisabledUsageDate(null); getOSSubscription().setWarnedExpirationDate(null); getOSSubscription().setDisabledExpirationDate(null); enableSync(controller); } /** * Sets all folders that have SyncProfile.DISABLED to * SyncProfile.BACKUP_TARGET_NO_CHANGE_DETECT. * * @param controller * @return the number of folder the sync was re-enabled. */ public int enableSync(Controller controller) { Reject.ifNull(controller, "Controller is null"); int n = 0; for (FolderInfo foInfo : getFoldersCharged()) { Folder f = foInfo.getFolder(controller); if (f == null) { continue; } if (f.getSyncProfile().equals(SyncProfile.DISABLED)) { n++; SyncProfile p = SyncProfile.getDefault(controller); f.setSyncProfile(p); } } return n; } /** * Sets all folders that don't have SyncProfile.DISABLED to * SyncProfile.DISABLED. * * @param controller * @return the number of folder the sync was disabled. */ public int disableSync(Controller controller) { Reject.ifNull(controller, "Controller is null"); int nNewDisabled = 0; for (FolderInfo foInfo : getFoldersCharged()) { Folder folder = foInfo.getFolder(controller); if (folder == null) { continue; } if (LOG.isLoggable(Level.FINER)) { LOG.finer("Disable download of new files for folder: " + folder + " for " + getUsername()); } if (!folder.getSyncProfile().equals(SyncProfile.DISABLED)) { folder.setSyncProfile(SyncProfile.DISABLED); nNewDisabled++; } } if (nNewDisabled > 0) { LOG.info("Disabled " + nNewDisabled + " folder for " + getUsername()); } return nNewDisabled; } // Permission convenience ************************************************ /** * Answers if the user is allowed to read the folder contents. * * @param foInfo * the folder to check * @return true if the user is allowed to read the folder contents */ public boolean hasReadPermissions(FolderInfo foInfo) { Reject.ifNull(foInfo, "Folder info is null"); return hasPermission(FolderPermission.read(foInfo)); } /** * Answers if the user is allowed to write into the folder. * * @param foInfo * the folder to check * @return true if the user is allowed to write into the folder. */ public boolean hasReadWritePermissions(FolderInfo foInfo) { Reject.ifNull(foInfo, "Folder info is null"); return hasPermission(FolderPermission.readWrite(foInfo)); } /** * Answers if the user is allowed to write into the folder. * * @param foInfo * the folder to check * @return true if the user is allowed to write into the folder. */ public boolean hasWritePermissions(FolderInfo foInfo) { return hasReadWritePermissions(foInfo); } /** * @param foInfo * @return true if the user is admin of the folder. */ public boolean hasAdminPermission(FolderInfo foInfo) { Reject.ifNull(foInfo, "Folder info is null"); return hasPermission(FolderPermission.admin(foInfo)); } /** * @param foInfo * @return true if the user is owner of the folder. */ public boolean hasOwnerPermission(FolderInfo foInfo) { Reject.ifNull(foInfo, "Folder info is null"); return hasPermission(FolderPermission.owner(foInfo)); } public boolean equals(Object obj) { if (obj == this) { return true; } if (obj == null || !(obj instanceof Account)) { return false; } Account otherAccount = (Account) obj; return (this.oid.equals(otherAccount.oid)); } public synchronized void convertCollections() { if (!(permissions instanceof CopyOnWriteArrayList<?>)) { Collection<Permission> newPermissions = new CopyOnWriteArrayList<Permission>( permissions); permissions = newPermissions; } if (!(groups instanceof CopyOnWriteArrayList<?>)) { Collection<Group> newGroups = new CopyOnWriteArrayList<Group>( groups); groups = newGroups; for (Group group : groups) { group.convertCollections(); } } if (!(computers instanceof CopyOnWriteArrayList<?>)) { Collection<MemberInfo> newComputers = new CopyOnWriteArrayList<MemberInfo>( computers); computers = newComputers; } if (!(licenseKeyFileList instanceof CopyOnWriteArrayList<?>)) { List<String> newLicenseKeyFileList = new CopyOnWriteArrayList<String>( licenseKeyFileList); licenseKeyFileList = newLicenseKeyFileList; } if (!(emails instanceof CopyOnWriteArrayList<?>)) { List<String> newEmails = new CopyOnWriteArrayList<String>(emails); emails = newEmails; } } public void migrate() { if (licenseKeyFiles != null) { licenseKeyFileList = new CopyOnWriteArrayList<String>( licenseKeyFiles); } if (groups == null) { groups = new CopyOnWriteArrayList<Group>(); } if (server != null) { server.migrateId(); } } private void writeObject(java.io.ObjectOutputStream stream) throws IOException { licenseKeyFiles = new CopyOnWriteArrayList<String>(licenseKeyFileList); stream.defaultWriteObject(); } private void readObject(java.io.ObjectInputStream stream) throws IOException, ClassNotFoundException { stream.defaultReadObject(); if (oid == null) { // Migration. oid = IdGenerator.makeId(); } if (groups == null) { groups = new CopyOnWriteArrayList<Group>(); } if (emails == null) { emails = new CopyOnWriteArrayList<String>(); } } }