func()

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
}