in lib/scim/scim_user.go [322:468]
func (h *scimUser) Patch(r *http.Request, name string) (proto.Message, error) {
h.save = &cpb.Account{}
proto.Merge(h.save, h.item)
for i, patch := range h.input.Operations {
src := patchSource(patch.Value)
var dst *string
path := patch.Path
// When updating a photo from the list, always update the photo in the primary profile.
if photoPathRE.MatchString(path) {
path = "photo"
} else if emailPathRE.MatchString(path) {
path = "emails"
}
switch path {
case "active":
// TODO: support for boolean input for "active" field instead of strings
switch {
case (patch.Op == "remove" && len(src) == 0) || (patch.Op == "replace" && src == "false"):
h.save.State = storage.StateDisabled
case src == "true" && (patch.Op == "add" || patch.Op == "replace"):
h.save.State = storage.StateActive
default:
return nil, errutil.NewIndexError(codes.InvalidArgument, errutil.ErrorPath("scim", "user", "profile", path), i, fmt.Sprintf("invalid active operation %q or value %q", patch.Op, patch.Value))
}
case "name.formatted":
dst = &h.save.Profile.FormattedName
if patch.Op == "remove" || len(src) == 0 {
return nil, errutil.NewIndexError(codes.InvalidArgument, errutil.ErrorPath("scim", "user", "profile", path), i, fmt.Sprintf("cannot set %q to an empty value", path))
}
case "name.familyName":
dst = &h.save.Profile.FamilyName
case "name.givenName":
dst = &h.save.Profile.GivenName
case "name.middleName":
dst = &h.save.Profile.MiddleName
case "displayName":
dst = &h.save.Profile.Name
if patch.Op == "remove" || len(src) == 0 {
return nil, errutil.NewIndexError(codes.InvalidArgument, errutil.ErrorPath("scim", "user", "profile", path), i, fmt.Sprintf("cannot set %q to an empty value", path))
}
case "preferredLanguage":
dst = &h.save.Profile.Language
if len(src) > 0 && !timeutil.IsLocale(src) {
return nil, errutil.NewIndexError(codes.InvalidArgument, errutil.ErrorPath("scim", "user", "profile", path), i, fmt.Sprintf("%q is not a recognized locale", path))
}
case "profileUrl":
dst = &h.save.Profile.Profile
case "locale":
dst = &h.save.Profile.Locale
if len(src) > 0 && !timeutil.IsLocale(src) {
return nil, errutil.NewIndexError(codes.InvalidArgument, errutil.ErrorPath("scim", "user", "profile", path), i, fmt.Sprintf("%q is not a recognized locale", path))
}
case "timezone":
dst = &h.save.Profile.ZoneInfo
if len(src) > 0 && !timeutil.IsTimeZone(src) {
return nil, errutil.NewIndexError(codes.InvalidArgument, errutil.ErrorPath("scim", "user", "profile", path), i, fmt.Sprintf("%q is not a recognized time zone", src))
}
case "emails":
if patch.Op == "add" {
// SCIM extension for linking accounts.
if src != auth.LinkAuthorizationHeader {
return nil, errutil.NewIndexError(codes.InvalidArgument, errutil.ErrorPath("scim", "user", "profile", path), i, fmt.Sprintf("%q must be set to %q", src, auth.LinkAuthorizationHeader))
}
if err := h.linkEmail(r); err != nil {
return nil, errutil.NewIndexError(codes.InvalidArgument, errutil.ErrorPath("scim", "user", "profile", path), i, err.Error())
}
break
}
// Standard SCIM email functionality.
link, match, err := selectLink(patch.Path, emailPathRE, scimEmailFilterMap, h.save)
if err != nil {
return nil, errutil.NewIndexError(codes.InvalidArgument, errutil.ErrorPath("scim", "user", "profile", path), i, err.Error())
}
dst = nil // operation can be skipped by logic after this switch block (i.e. no destination to write)
if link == nil {
break
}
if len(match[2]) == 0 {
// When match[2] is empty, the operation applies to the entire email object.
if patch.Op != "remove" {
return nil, errutil.NewIndexError(codes.InvalidArgument, errutil.ErrorPath("scim", "user", "profile", path), i, fmt.Sprintf("path %q only supported for remove", path))
}
if len(h.save.ConnectedAccounts) < 2 {
return nil, errutil.NewIndexError(codes.InvalidArgument, errutil.ErrorPath("scim", "user", "profile", path), i, fmt.Sprintf("cannot unlink the only email address for a given account"))
}
// Unlink account
for idx, connect := range h.save.ConnectedAccounts {
if connect.Properties.Subject == link.Properties.Subject {
h.save.ConnectedAccounts = append(h.save.ConnectedAccounts[:idx], h.save.ConnectedAccounts[idx+1:]...)
if err := h.s.RemoveAccountLookup(link.LinkRevision, getRealm(r), link.Properties.Subject, r, h.auth.ID, h.tx); err != nil {
return nil, errutil.NewIndexError(codes.InvalidArgument, errutil.ErrorPath("scim", "user", "profile", path), i, fmt.Sprintf("service dependencies not available; try again later"))
}
break
}
}
} else {
// This logic is valid for all patch.Op operations.
primary := strings.ToLower(src) == "true" && patch.Op != "remove"
if primary {
// Make all entries not primary, then set the primary below
for _, entry := range h.save.ConnectedAccounts {
entry.Primary = false
}
}
link.Primary = primary
}
case "photo":
dst = &h.save.Profile.Picture
if !strutil.IsImageURL(src) {
return nil, errutil.NewIndexError(codes.InvalidArgument, errutil.ErrorPath("scim", "user", "profile", path), i, fmt.Sprintf("invalid photo URL %q", src))
}
default:
return nil, errutil.NewIndexError(codes.InvalidArgument, errutil.ErrorPath("scim", "user", "profile", path), i, fmt.Sprintf("invalid path %q", path))
}
if patch.Op != "remove" && len(src) == 0 {
return nil, fmt.Errorf("operation %d: cannot set an empty value", i)
}
if dst == nil {
continue
}
switch patch.Op {
case "add":
fallthrough
case "replace":
*dst = src
case "remove":
*dst = ""
default:
return nil, errutil.NewIndexError(codes.InvalidArgument, errutil.ErrorPath("scim", "user", "profile", path), i, fmt.Sprintf("invalid op %q", patch.Op))
}
}
return newScimUser(h.save, getRealm(r), h.domainURL, h.userPath), nil
}