package com.intrbiz.bergamot.model;
import java.io.Serializable;
import java.security.Principal;
import java.sql.Timestamp;
import java.util.Collection;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.mindrot.jbcrypt.BCrypt;
import com.intrbiz.Util;
import com.intrbiz.bergamot.config.model.ContactCfg;
import com.intrbiz.bergamot.data.BergamotDB;
import com.intrbiz.bergamot.model.message.ContactMO;
import com.intrbiz.data.db.compiler.meta.SQLColumn;
import com.intrbiz.data.db.compiler.meta.SQLTable;
import com.intrbiz.data.db.compiler.meta.SQLUnique;
import com.intrbiz.data.db.compiler.meta.SQLVersion;
@SQLTable(schema = BergamotDB.class, name = "contact", since = @SQLVersion({ 1, 0, 0 }))
@SQLUnique(name = "name_unq", columns = { "site_id", "name" })
public class Contact extends SecuredObject<ContactMO, ContactCfg> implements Principal, Serializable
{
private static final long serialVersionUID = 1L;
public static final int BCRYPT_WORK_FACTOR = 12;
public static enum LockOutReason
{
ADMINISTRATIVE("Administratively locked"),
AUTOMATIC("Automatically locked");
private final String description;
private LockOutReason(String description)
{
this.description = description;
}
public String getDescription()
{
return this.description;
}
}
@SQLColumn(index = 1, name = "team_ids", type = "UUID[]", since = @SQLVersion({ 1, 0, 0 }))
private List<UUID> teamIds = new LinkedList<UUID>();
@SQLColumn(index = 2, name = "password_hash", since = @SQLVersion({ 1, 0, 0 }))
private String passwordHash;
@SQLColumn(index = 3, name = "email", since = @SQLVersion({ 1, 0, 0 }))
@SQLUnique(name = "email_unq", columns = { "site_id", "email" })
private String email;
@SQLColumn(index = 4, name = "pager", since = @SQLVersion({ 1, 0, 0 }))
private String pager;
@SQLColumn(index = 5, name = "mobile", since = @SQLVersion({ 1, 0, 0 }))
private String mobile;
@SQLColumn(index = 6, name = "phone", since = @SQLVersion({ 1, 0, 0 }))
private String phone;
@SQLColumn(index = 7, name = "granted_permissions", type = "TEXT[]", since = @SQLVersion({ 1, 0, 0 }))
private List<String> grantedPermissions = new LinkedList<String>();
@SQLColumn(index = 8, name = "revoked_permissions", type = "TEXT[]", since = @SQLVersion({ 1, 0, 0 }))
private List<String> revokedPermissions = new LinkedList<String>();
// account security flags
/**
* Should the password be changed on next login
*/
@SQLColumn(index = 9, name = "force_password_change", since = @SQLVersion({ 1, 0, 0 }))
private boolean forcePasswordChange = false;
/**
* Is this account locked in any way, IE: reject authentication even with valid credentials
*/
@SQLColumn(index = 10, name = "locked", since = @SQLVersion({ 1, 0, 0 }))
private boolean locked = false;
/**
* Why the account was locked
*/
@SQLColumn(index = 11, name = "locked_reason", since = @SQLVersion({ 1, 0, 0 }))
private LockOutReason lockedReason;
/**
* When this account was locked last
*/
@SQLColumn(index = 12, name = "locked_at", since = @SQLVersion({ 1, 0, 0 }))
private Timestamp lockedAt = null;
public Contact()
{
super();
}
@Override
public void configure(ContactCfg configuration, ContactCfg resolvedConfiguration)
{
super.configure(configuration, resolvedConfiguration);
// email
this.email = resolvedConfiguration.getEmail();
// phones
this.mobile = resolvedConfiguration.getMobile();
this.pager = resolvedConfiguration.getPager();
this.phone = resolvedConfiguration.getPhone();
// permissions
this.grantedPermissions.clear();
this.grantedPermissions.addAll(resolvedConfiguration.getGrantedPermissions());
this.revokedPermissions.clear();
this.revokedPermissions.addAll(resolvedConfiguration.getRevokedPermissions());
}
public List<UUID> getTeamIds()
{
return this.teamIds;
}
public void setTeamIds(List<UUID> teamIds)
{
this.teamIds = teamIds;
}
public List<Team> getTeams()
{
List<Team> r = new LinkedList<Team>();
try (BergamotDB db = BergamotDB.connect())
{
for (UUID id : this.getTeamIds())
{
r.add(db.getTeam(id));
}
}
return r;
}
public String getPasswordHash()
{
return passwordHash;
}
public void setPasswordHash(String passwordHash)
{
this.passwordHash = passwordHash;
}
public void hashPassword(String plainPassword)
{
this.passwordHash = BCrypt.hashpw(plainPassword, BCrypt.gensalt(BCRYPT_WORK_FACTOR));
// reset as we've updated the password
this.forcePasswordChange = false;
}
public boolean verifyPassword(String plainPassword)
{
if (plainPassword == null || this.passwordHash == null) return false;
return BCrypt.checkpw(plainPassword, this.passwordHash);
}
public String getEmail()
{
return email;
}
public void setEmail(String email)
{
this.email = email;
}
public String getPager()
{
return pager;
}
public void setPager(String pager)
{
this.pager = pager;
}
public String getMobile()
{
return mobile;
}
public void setMobile(String mobile)
{
this.mobile = mobile;
}
public String getPhone()
{
return phone;
}
public void setPhone(String phone)
{
this.phone = phone;
}
public Notifications getNotifications()
{
try (BergamotDB db = BergamotDB.connect())
{
return db.getNotifications(this.getId());
}
}
public void setNotifications(Notifications notifications)
{
try (BergamotDB db = BergamotDB.connect())
{
notifications.setId(this.getId());
db.setNotifications(notifications);
}
}
public List<String> getGrantedPermissions()
{
return grantedPermissions;
}
public void setGrantedPermissions(List<String> grantedPermissions)
{
this.grantedPermissions = grantedPermissions;
}
public List<String> getRevokedPermissions()
{
return revokedPermissions;
}
public void setRevokedPermissions(List<String> revokedPermissions)
{
this.revokedPermissions = revokedPermissions;
}
public boolean isForcePasswordChange()
{
return forcePasswordChange;
}
public void setForcePasswordChange(boolean forcePasswordChange)
{
this.forcePasswordChange = forcePasswordChange;
}
public boolean isLocked()
{
return locked;
}
public void setLocked(boolean locked)
{
this.locked = locked;
}
public LockOutReason getLockedReason()
{
return lockedReason;
}
public void setLockedReason(LockOutReason lockedReason)
{
this.lockedReason = lockedReason;
}
public Timestamp getLockedAt()
{
return lockedAt;
}
public void setLockedAt(Timestamp lockedAt)
{
this.lockedAt = lockedAt;
}
public Contact forcePasswordChange()
{
this.forcePasswordChange = true;
return this;
}
public Contact resetPassword()
{
this.passwordHash = null;
this.forcePasswordChange = true;
return this;
}
public Contact lock(LockOutReason reason)
{
this.locked = true;
this.lockedReason = reason;
this.lockedAt = new Timestamp(System.currentTimeMillis());
return this;
}
public Contact unlock()
{
this.locked = false;
this.lockedReason = null;
this.lockedAt = null;
return this;
}
public List<APIToken> getAPITokens()
{
try (BergamotDB db = BergamotDB.connect())
{
return db.getAPITokensForContact(this.getId());
}
}
public List<ContactU2FDeviceRegistration> getU2FDeviceRegistrations()
{
try (BergamotDB db = BergamotDB.connect())
{
return db.getU2FDeviceRegistrationsForContact(this.getId());
}
}
public List<ContactHOTPRegistration> getHOTPRegistrations()
{
try (BergamotDB db = BergamotDB.connect())
{
return db.getHOTPRegistrationsForContact(this.getId());
}
}
public List<ContactBackupCode> getBackupCodes()
{
try (BergamotDB db = BergamotDB.connect())
{
return db.getBackupCodesForContact(this.getId());
}
}
public void generateMoreBackupCodes()
{
try (BergamotDB db = BergamotDB.connect())
{
// home many active codes do we have?
int count = (int) this.getBackupCodes().stream().filter((bc) -> ! bc.isUsed()).count();
// generate 5 mode backup codes
for (int i = 0; i < (10 - count); i++)
{
db.setBackupCode(new ContactBackupCode(this));
}
}
}
public boolean isTwoFactorConfigured()
{
return ! (this.getU2FDeviceRegistrations().isEmpty() && this.getHOTPRegistrations().isEmpty());
}
public List<AccessControl> getAccessControls()
{
try (BergamotDB db = BergamotDB.connect())
{
return db.getAccessControlsForRole(this.getId());
}
}
public AccessControl getAccessControl(SecurityDomain domain)
{
try (BergamotDB db = BergamotDB.connect())
{
return db.getAccessControl(domain.getId(), this.getId());
}
}
// permissions handling
public boolean hasPermission(Permission permission)
{
return this.hasPermission(permission.toString());
}
public boolean hasPermission(String permission)
{
try (BergamotDB db = BergamotDB.connect())
{
return db.hasPermission(this.getId(), permission);
}
}
public boolean hasPermission(Permission permission, SecurityDomain domain)
{
return this.hasPermission(permission.toString(), domain);
}
public boolean hasPermission(String permission, SecurityDomain domain)
{
try (BergamotDB db = BergamotDB.connect())
{
return db.hasPermissionForSecurityDomain(this.getId(), domain.getId(), permission);
}
}
public boolean hasPermission(Permission permission, SecuredObject<?,?> overObject)
{
return this.hasPermission(permission.toString(), overObject);
}
public boolean hasPermission(Permission permission, UUID overObjectId)
{
return this.hasPermission(permission.toString(), overObjectId);
}
public boolean hasPermission(String permission, SecuredObject<?,?> overObject)
{
try (BergamotDB db = BergamotDB.connect())
{
return db.hasPermissionForObject(this.getId(), overObject.getId(), permission);
}
}
public boolean hasPermission(String permission, UUID overObjectId)
{
try (BergamotDB db = BergamotDB.connect())
{
return db.hasPermissionForObject(this.getId(), overObjectId, permission);
}
}
public <T extends SecuredObject<?,?>> List<T> hasPermission(Permission permission, Collection<T> overObjects)
{
List<T> filtered = new LinkedList<T>();
for (T overObject : overObjects)
{
if (this.hasPermission(permission, overObject))
{
filtered.add(overObject);
}
}
return filtered;
}
public <T extends SecuredObject<?,?>> Set<T> hasPermission(Permission permission, Set<T> overObjects)
{
Set<T> filtered = new HashSet<T>();
for (T overObject : overObjects)
{
if (this.hasPermission(permission, overObject))
{
filtered.add(overObject);
}
}
return filtered;
}
public <T extends SecuredObject<?,?>> List<T> hasPermission(String permission, Collection<T> overObjects)
{
return this.hasPermission(Permission.of(permission), overObjects);
}
public <T extends SecuredObject<?,?>> Set<T> hasPermission(String permission, Set<T> overObjects)
{
return this.hasPermission(Permission.of(permission), overObjects);
}
public <T extends SecuredObject<?,?>> List<T> hasPermission(Function<T,Permission> permission, Collection<T> overObjects)
{
List<T> filtered = new LinkedList<T>();
for (T overObject : overObjects)
{
if (this.hasPermission(permission.apply(overObject), overObject))
{
filtered.add(overObject);
}
}
return filtered;
}
public <T extends SecuredObject<?,?>> Set<T> hasPermission(Function<T, Permission> permission, Set<T> overObjects)
{
Set<T> filtered = new HashSet<T>();
for (T overObject : overObjects)
{
if (this.hasPermission(permission.apply(overObject), overObject))
{
filtered.add(overObject);
}
}
return filtered;
}
public boolean isGlobalAdmin()
{
try (BergamotDB db = BergamotDB.connect())
{
GlobalSetting globalAdmins = db.getGlobalSetting(GlobalSetting.NAME.GLOBAL_ADMINS);
if (globalAdmins != null)
{
return "true".equals(globalAdmins.getParameter(this.id.toString()));
}
}
return false;
}
public String toString()
{
return "Contact {" + this.getName() + "}";
}
@Override
public ContactMO toMO(Contact contact, EnumSet<MOFlag> options)
{
ContactMO mo = new ContactMO();
super.toMO(mo, contact, options);
mo.setEmail(this.getEmail());
mo.setMobile(this.getMobile());
mo.setPager(this.getPager());
mo.setPhone(this.getPhone());
mo.setGrantedPermissions(this.getGrantedPermissions());
mo.setRevokedPermissions(this.getRevokedPermissions());
if (options.contains(MOFlag.TEAMS)) mo.setTeams(this.getTeams().stream().filter((x) -> contact == null || contact.hasPermission("read", x)).map((x) -> x.toStubMO(contact)).collect(Collectors.toList()));
if (options.contains(MOFlag.NOTIFICATIONS)) mo.setNotifications(Util.nullable(this.getNotifications(), (x) -> x.toStubMO(contact)));
return mo;
}
}