/*******************************************************************************
* Cloud Foundry
* Copyright (c) [2009-2016] Pivotal Software, Inc. All Rights Reserved.
*
* This product is licensed to you under the Apache License, Version 2.0 (the "License").
* You may not use this product except in compliance with the License.
*
* This product includes a number of subcomponents with
* separate copyright notices and license terms. Your use of these
* subcomponents is subject to the terms and conditions of the
* subcomponent's license, as noted in the LICENSE file.
*******************************************************************************/
package org.cloudfoundry.identity.uaa.scim;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.cloudfoundry.identity.uaa.approval.Approval;
import org.cloudfoundry.identity.uaa.impl.JsonDateSerializer;
import org.cloudfoundry.identity.uaa.scim.impl.ScimUserJsonDeserializer;
import org.springframework.util.Assert;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import static java.util.Optional.ofNullable;
import static org.springframework.util.StringUtils.hasText;
/**
* Object to hold SCIM data for Jackson to map to and from JSON
*
* See the <a
* href="http://www.simplecloud.info/specs/draft-scim-core-schema-02.html">SCIM
* user schema</a>.
*
* @author Luke Taylor
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonDeserialize(using = ScimUserJsonDeserializer.class)
public class ScimUser extends ScimCore<ScimUser> {
@JsonInclude(JsonInclude.Include.NON_NULL)
public static final class Group {
String value;
String display;
public static enum Type {
DIRECT, INDIRECT
};
Type type;
public Type getType() {
return type;
}
public void setType(Type type) {
this.type = type;
}
public Group() {
this(null, null);
}
public Group(String value, String display) {
this(value, display, Type.DIRECT);
}
public Group(String value, String display, Type type) {
this.value = value;
this.display = display;
this.type = type;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public String getDisplay() {
return display;
}
public void setDisplay(String display) {
this.display = display;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((display == null) ? 0 : display.hashCode());
result = prime * result + ((value == null) ? 0 : value.hashCode());
result = prime * result + ((type == null) ? 0 : type.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Group other = (Group) obj;
if (display == null) {
if (other.display != null)
return false;
}
else if (!display.equals(other.display))
return false;
if (value == null) {
if (other.value != null)
return false;
}
else if (!value.equals(other.value))
return false;
return type == other.type;
}
@Override
public String toString() {
return String.format("(id: %s, name: %s, type: %s)", value, display, type);
}
}
@JsonInclude(JsonInclude.Include.NON_NULL)
public static final class Name {
String formatted;
String familyName;
String givenName;
String middleName;
String honorificPrefix;
String honorificSuffix;
public Name() {
}
public Name(String givenName, String familyName) {
this.givenName = givenName;
this.familyName = familyName;
this.formatted = givenName + " " + familyName;
}
public String getFormatted() {
return formatted;
}
public void setFormatted(String formatted) {
this.formatted = formatted;
}
public String getFamilyName() {
return familyName;
}
public void setFamilyName(String familyName) {
this.familyName = familyName;
}
public String getGivenName() {
return givenName;
}
public void setGivenName(String givenName) {
this.givenName = givenName;
}
public String getMiddleName() {
return middleName;
}
public void setMiddleName(String middleName) {
this.middleName = middleName;
}
public String getHonorificPrefix() {
return honorificPrefix;
}
public void setHonorificPrefix(String honorificPrefix) {
this.honorificPrefix = honorificPrefix;
}
public String getHonorificSuffix() {
return honorificSuffix;
}
public void setHonorificSuffix(String honorificSuffix) {
this.honorificSuffix = honorificSuffix;
}
}
@JsonInclude(JsonInclude.Include.NON_NULL)
public static final class Email {
private String value;
// this should probably be an enum
private String type;
private boolean primary = false;
public String getValue() {
return value;
}
public void setValue(String value) {
Assert.notNull(value);
this.value = value;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public void setPrimary(boolean primary) {
this.primary = primary;
}
public boolean isPrimary() {
return primary;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Email email = (Email) o;
if (primary != email.primary) return false;
if (type != null ? !type.equals(email.type) : email.type != null) return false;
if (value != null ? !value.equals(email.value) : email.value != null) return false;
return true;
}
@Override
public int hashCode() {
int result = value != null ? value.hashCode() : 0;
result = 31 * result + (type != null ? type.hashCode() : 0);
result = 31 * result + (primary ? 1 : 0);
return result;
}
}
@JsonInclude(JsonInclude.Include.NON_NULL)
public static final class PhoneNumber {
private String value;
// this should probably be an enum
private String type;
public PhoneNumber(String phoneNumber) {
this.value = phoneNumber;
}
public PhoneNumber() {}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
}
private String userName;
private Name name;
private List<Email> emails;
private Set<Group> groups;
private Set<Approval> approvals;
private List<PhoneNumber> phoneNumbers;
private String displayName;
private String nickName;
private String profileUrl;
private String title;;
private String userType;
private String preferredLanguage;
private String locale;
private String timezone;
private boolean active = true;
private boolean verified = true;
private String origin = "";
private String externalId = "";
private String zoneId = null;
private String salt = null;
private Date passwordLastModified = null;
private Long previousLogonTime = null;
private Long lastLogonTime = null;
@JsonProperty
private String password;
public ScimUser() {
}
public ScimUser(String id, String userName, String givenName, String familyName) {
super(id);
this.userName = userName;
this.name = new Name(givenName, familyName);
}
public String getUserName() {
return userName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public void setUserName(String userName) {
this.userName = userName;
}
public Name getName() {
return name;
}
public void setName(Name name) {
this.name = name;
}
public List<Email> getEmails() {
return emails;
}
public void setEmails(List<Email> emails) {
this.emails = emails;
}
public Set<Approval> getApprovals() {
return approvals;
}
public void setApprovals(Set<Approval> approvals) {
this.approvals = approvals;
}
public Set<Group> getGroups() {
return groups;
}
public void setGroups(Collection<Group> groups) {
this.groups = new LinkedHashSet<Group>(groups);
}
public List<PhoneNumber> getPhoneNumbers() {
return phoneNumbers;
}
public void setPhoneNumbers(List<PhoneNumber> phoneNumbers) {
if (phoneNumbers!=null && phoneNumbers.size()>0) {
ArrayList<PhoneNumber> list = new ArrayList<PhoneNumber>();
list.addAll(phoneNumbers);
for (int i=(list.size()-1); i>=0; i--) {
PhoneNumber pn = list.get(i);
if (pn==null || (!hasText(pn.getValue()))) {
list.remove(i);
}
}
phoneNumbers = list;
}
this.phoneNumbers = phoneNumbers;
}
public String getDisplayName() {
return displayName;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
public String getNickName() {
return nickName;
}
public void setNickName(String nickName) {
this.nickName = nickName;
}
public String getProfileUrl() {
return profileUrl;
}
public void setProfileUrl(String profileUrl) {
this.profileUrl = profileUrl;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getUserType() {
return userType;
}
public void setUserType(String userType) {
this.userType = userType;
}
public String getPreferredLanguage() {
return preferredLanguage;
}
public void setPreferredLanguage(String preferredLanguage) {
this.preferredLanguage = preferredLanguage;
}
public String getLocale() {
return locale;
}
public void setLocale(String locale) {
this.locale = locale;
}
public String getTimezone() {
return timezone;
}
public void setTimezone(String timezone) {
this.timezone = timezone;
}
public boolean isActive() {
return active;
}
public void setActive(boolean active) {
this.active = active;
}
public boolean isVerified() {
return verified;
}
public void setVerified(boolean verified) {
this.verified = verified;
}
public String getOrigin() {
return origin;
}
public void setOrigin(String origin) {
this.origin = origin;
}
public String getExternalId() {
return externalId;
}
public ScimUser setExternalId(String externalId) {
this.externalId = externalId;
return this;
}
public String getZoneId() {
return zoneId;
}
public void setZoneId(String zoneId) {
this.zoneId = zoneId;
}
public String getSalt() {
return salt;
}
public void setSalt(String salt) {
this.salt = salt;
}
@JsonSerialize(using = JsonDateSerializer.class, include = JsonSerialize.Inclusion.NON_NULL)
public Date getPasswordLastModified() {
if (passwordLastModified!=null) {
return passwordLastModified;
} else if (getId()!=null) {
return getMeta().getCreated();
}
return null;
}
public void setPasswordLastModified(Date passwordLastModified) {
this.passwordLastModified = passwordLastModified;
}
public Long getLastLogonTime() {
return lastLogonTime;
}
public ScimUser setLastLogonTime(Long lastLogonTime) {
this.lastLogonTime = lastLogonTime;
return this;
}
public Long getPreviousLogonTime() {
return previousLogonTime;
}
public ScimUser setPreviousLogonTime(Long previousLogonTime) {
this.previousLogonTime = previousLogonTime;
return this;
}
@JsonIgnore
public String getPrimaryEmail() {
if (getEmails() == null || getEmails().isEmpty()) {
return null;
}
Email primaryEmail = null;
for (Email email : getEmails()) {
if (email.isPrimary()) {
primaryEmail = email;
break;
}
}
if (primaryEmail == null) {
primaryEmail = getEmails().get(0);
}
return primaryEmail.getValue();
}
public void setPrimaryEmail(String value) {
Assert.notNull(value);
Email newPrimaryEmail = new Email();
newPrimaryEmail.setPrimary(true);
newPrimaryEmail.setValue(value);
if (emails == null) {
emails = new ArrayList<>(1);
}
else {
emails = new ArrayList<>(getEmails());
}
Email currentPrimaryEmail = null;
for (Email email : emails) {
if (email.isPrimary()) {
currentPrimaryEmail = email;
break;
}
}
if (currentPrimaryEmail != null) {
emails.remove(currentPrimaryEmail);
}
emails.add(0, newPrimaryEmail);
}
@JsonIgnore
public String getGivenName() {
return name == null ? null : name.getGivenName();
}
@JsonIgnore
public String getFamilyName() {
return name == null ? null : name.getFamilyName();
}
/**
* Adds a new email address, ignoring "type" and "primary" fields, which we
* don't need yet
*/
public void addEmail(String newEmail) {
Assert.hasText(newEmail, "Attempted to add null or empty email string to user.");
if (emails == null) {
emails = new ArrayList<>(1);
}
for (Email email : emails) {
if (email.value.equals(newEmail)) {
throw new IllegalArgumentException("Already contains email " + newEmail);
}
}
Email e = new Email();
e.setValue(newEmail);
emails.add(e);
}
/**
* Adds a new phone number with null type.
*
* @param newPhoneNumber
*/
public void addPhoneNumber(String newPhoneNumber) {
if (newPhoneNumber==null || newPhoneNumber.trim().length()==0) {
return;
}
if (phoneNumbers == null) {
phoneNumbers = new ArrayList<>(1);
}
for (PhoneNumber phoneNumber : phoneNumbers) {
if (phoneNumber.value.equals(newPhoneNumber) && phoneNumber.getType() == null) {
throw new IllegalArgumentException("Already contains phoneNumber " + newPhoneNumber);
}
}
PhoneNumber number = new PhoneNumber();
number.setValue(newPhoneNumber);
phoneNumbers.add(number);
}
/**
* Creates a word list from the user data for use in password checking
* implementations
*/
public List<String> wordList() {
List<String> words = new ArrayList<String>();
if (userName != null) {
words.add(userName);
}
if (name != null) {
if (name.givenName != null) {
words.add(name.givenName);
}
if (name.familyName != null) {
words.add(name.familyName);
}
if (nickName != null) {
words.add(nickName);
}
}
if (emails != null) {
for (Email email : emails) {
words.add(email.getValue());
}
}
return words;
}
@Override
public void patch(ScimUser patch) {
//Delete Attributes specified in Meta.attributes
String[] attributes = ofNullable(patch.getMeta().getAttributes()).orElse(new String[0]);
for (String attribute : attributes) {
switch (attribute.toUpperCase()) {
case "USERNAME":
if (!hasText(patch.getUserName())) {
throw new IllegalArgumentException("Can not drop username, field is required.");
}
setUserName(null);
break;
case "EMAILS":
setEmails(new ArrayList<>());
break;
case "PHONENUMBERS":
setPhoneNumbers(new ArrayList<>());
break;
case "DISPLAYNAME":
setDisplayName(null);
break;
case "NICKNAME":
setNickName(null);
break;
case "PROFILEURL":
setProfileUrl(null);
break;
case "TITLE":
setTitle(null);
break;
case "PREFERREDLANGUAGE":
setPreferredLanguage(null);
break;
case "LOCALE":
setLocale(null);
break;
case "TIMEZONE":
setTimezone(null);
break;
case "NAME":
setName(new Name());
break;
case "NAME.FAMILYNAME":
ofNullable(getName()).ifPresent(name -> name.setFamilyName(null));
break;
case "NAME.GIVENNAME":
ofNullable(getName()).ifPresent(name -> name.setGivenName(null));
break;
case "NAME.FORMATTED":
ofNullable(getName()).ifPresent(name -> name.setFormatted(null));
break;
case "NAME.HONORIFICPREFIX":
ofNullable(getName()).ifPresent(name -> name.setHonorificPrefix(null));
break;
case "NAME.HONORIFICSUFFIX":
ofNullable(getName()).ifPresent(name -> name.setHonorificSuffix(null));
break;
case "NAME.MIDDLENAME":
ofNullable(getName()).ifPresent(name -> name.setMiddleName(null));
break;
default:
throw new IllegalArgumentException(String.format("Attribute %s cannot be removed using \"Meta.attributes\"", attribute));
}
}
//Merge simple Attributes, that are stored
ofNullable(patch.getUserName()).ifPresent(p -> setUserName(p));
setActive(patch.isActive());
setVerified(patch.isVerified());
//Merge complex attributes
ScimUser.Name patchName = patch.getName();
if (patchName != null) {
ScimUser.Name currentName = ofNullable(getName()).orElse(new Name());
ofNullable(patchName.getFamilyName()).ifPresent(n -> currentName.setFamilyName(n));
ofNullable(patchName.getGivenName()).ifPresent(n -> currentName.setGivenName(n));
ofNullable(patchName.getMiddleName()).ifPresent(n -> currentName.setMiddleName(n));
ofNullable(patchName.getFormatted()).ifPresent(n -> currentName.setFormatted(n));
ofNullable(patchName.getHonorificPrefix()).ifPresent(n -> currentName.setHonorificPrefix(n));
ofNullable(patchName.getHonorificSuffix()).ifPresent(n -> currentName.setHonorificSuffix(n));
setName(currentName);
}
ofNullable(patch.getDisplayName()).ifPresent(
s -> setDisplayName(s)
);
ofNullable(patch.getNickName()).ifPresent(s -> setNickName(s));
ofNullable(patch.getTimezone()).ifPresent(s -> setTimezone(s));
ofNullable(patch.getTitle()).ifPresent(s -> setTitle(s));
ofNullable(patch.getProfileUrl()).ifPresent(s -> setProfileUrl(s));
ofNullable(patch.getLocale()).ifPresent(s -> setLocale(s));
ofNullable(patch.getPreferredLanguage()).ifPresent(s -> setPreferredLanguage(s));
//Only one email stored, use Primary or first.
if (patch.getEmails() != null && patch.getEmails().size()>0) {
ScimUser.Email primary = null;
for (ScimUser.Email email : patch.getEmails()) {
if (email.isPrimary()) {
primary = email;
break;
}
}
List<Email> currentEmails = ofNullable(getEmails()).orElse(new ArrayList());
if (primary != null) {
for (Email e : currentEmails) {
e.setPrimary(false);
}
}
currentEmails.addAll(patch.getEmails());
setEmails(currentEmails);
}
//Only one PhoneNumber stored, use first, as primary does not exist
if (patch.getPhoneNumbers() != null && patch.getPhoneNumbers().size()>0) {
List<PhoneNumber> current = ofNullable(getPhoneNumbers()).orElse(new ArrayList<>());
for (int index=0; index<patch.getPhoneNumbers().size(); index++) {
current.add(index, patch.getPhoneNumbers().get(index));
}
setPhoneNumbers(current);
}
}
}