User.java

package jasper.domain;

import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.IdClass;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import jasper.domain.proj.HasOrigin;
import jasper.domain.proj.Tag;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.compress.utils.Sets;
import org.hibernate.annotations.Formula;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.type.SqlTypes;
import org.hibernate.validator.constraints.Length;
import org.springframework.data.annotation.LastModifiedDate;

import java.time.Instant;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;

import static jasper.security.AuthoritiesConstants.ADMIN;
import static jasper.security.AuthoritiesConstants.ANONYMOUS;
import static jasper.security.AuthoritiesConstants.BANNED;
import static jasper.security.AuthoritiesConstants.EDITOR;
import static jasper.security.AuthoritiesConstants.MOD;
import static jasper.security.AuthoritiesConstants.USER;
import static jasper.security.AuthoritiesConstants.VIEWER;
import static java.util.Optional.*;
import static java.util.stream.Stream.concat;
import static org.apache.commons.collections4.ListUtils.emptyIfNull;

@Entity
@Getter
@Setter
@IdClass(TagId.class)
@Table(name = "users")
public class User implements Tag {
	public static final String REGEX = "[_+]user(?:/[a-z0-9]+(?:[./][a-z0-9]+)*)?";
	public static final String ROLE_REGEX = "\\w*";
	public static final String QTAG_REGEX = REGEX + HasOrigin.REGEX;
	public static final int NAME_LEN = 512;
	public static final int ROLE_LEN = 32;
	public static final int AUTHORIZED_KEYS_LEN = 65000;

	/**
	 * Valid roles for the User entities.
	 */
	public static final Set<String> ROLES = Sets.newHashSet(ADMIN, MOD, EDITOR, USER, VIEWER, BANNED);

	@Id
	@Column(updatable = false)
	@NotBlank
	@Pattern(regexp = REGEX)
	@Length(max = TAG_LEN)
	private String tag;

	@Id
	@Column(updatable = false)
	@Pattern(regexp = HasOrigin.REGEX)
	@Length(max = ORIGIN_LEN)
	private String origin = "";

	@Length(max = ROLE_LEN)
	@Pattern(regexp = ROLE_REGEX)
	private String role = "";

	@Formula("tag || origin")
	@Setter(AccessLevel.NONE)
	private String qualifiedTag;

	@Length(max = NAME_LEN)
	private String name;

	@JdbcTypeCode(SqlTypes.JSON)
	private List<@NotBlank @Length(max = TAG_LEN) @Pattern(regexp = Tag.REGEX) String> readAccess;

	@JdbcTypeCode(SqlTypes.JSON)
	private List<@NotBlank @Length(max = TAG_LEN) @Pattern(regexp = Tag.REGEX) String> writeAccess;

	@JdbcTypeCode(SqlTypes.JSON)
	private List<@NotBlank @Length(max = TAG_LEN) @Pattern(regexp = Tag.REGEX) String> tagReadAccess;

	@JdbcTypeCode(SqlTypes.JSON)
	private List<@NotBlank @Length(max = TAG_LEN) @Pattern(regexp = Tag.REGEX) String> tagWriteAccess;

	@LastModifiedDate
	@Column(nullable = false)
	private Instant modified = Instant.now();

	private byte[] key;

	private byte[] pubKey;

	@Size(max = AUTHORIZED_KEYS_LEN)
	private String authorizedKeys;

	@JdbcTypeCode(SqlTypes.JSON)
	private External external;

	@JsonIgnore
	public String getQualifiedTag() {
		return getTag() + getOrigin();
	}

	@JsonIgnore
	public User addReadAccess(List<String> toAdd) {
		if (toAdd == null) return this;
		if (readAccess == null) {
			readAccess = toAdd;
		} else {
			for (var t : toAdd) {
				if (!readAccess.contains(t)) {
					readAccess.add(t);
				}
			}
		}
		return this;
	}

	@JsonIgnore
	public User addWriteAccess(List<String> toAdd) {
		if (toAdd == null) return this;
		if (writeAccess == null) {
			writeAccess = toAdd;
		} else {
			for (var t : toAdd) {
				if (!writeAccess.contains(t)) {
					writeAccess.add(t);
				}
			}
		}
		return this;
	}

	@Override
	public boolean equals(Object o) {
		if (this == o) return true;
		if (o == null || getClass() != o.getClass()) return false;
		User user = (User) o;
		return tag.equals(user.tag) && origin.equals(user.origin);
	}

	@Override
	public int hashCode() {
		return Objects.hash(tag, origin);
	}

	public static boolean isUser(String t) {
		return t.startsWith("+user") ||
			t.startsWith("_user") ||
			t.startsWith("+user/") ||
			t.startsWith("_user/");
	}

	@JsonIgnore
	public boolean hasExternalId() {
		if (external == null) return false;
		if (external.getIds() == null) return false;
		return !external.getIds().isEmpty();
	}

	@JsonIgnore
	public boolean hasExternalId(String id) {
		if (external == null) return false;
		if (external.getIds() == null) return false;
		return external.getIds().contains(id);
	}

	public static Optional<User> merge(List<User> users) {
		if (users.isEmpty()) return empty();
		var result = new User();
		result.setTag(users.getFirst().getTag());
		result.setOrigin(users.getFirst().getOrigin());
		result.setName(users.getFirst().getName());
		result.setKey(users.getFirst().getKey());
		result.setPubKey(users.getFirst().getPubKey());
		result.setAuthorizedKeys(users.getFirst().getAuthorizedKeys());
		result.setExternal(users.getFirst().getExternal());
		result.setRole(users.stream().map(User::getRole).max((a, b) -> {
			if (ADMIN.equals(a)) return 1;
			if (ADMIN.equals(b)) return -1;
			if (MOD.equals(a)) return 1;
			if (MOD.equals(b)) return -1;
			if (EDITOR.equals(a)) return 1;
			if (EDITOR.equals(b)) return -1;
			if (USER.equals(a)) return 1;
			if (USER.equals(b)) return -1;
			if (VIEWER.equals(a)) return 1;
			if (VIEWER.equals(b)) return -1;
			return 0;
		}).orElse(ANONYMOUS));
		result.setReadAccess(users.stream().flatMap(u -> emptyIfNull(u.getReadAccess()).stream()).distinct().toList());
		result.setWriteAccess(concat(
			users.stream().skip(1).map(User::getTag),
			users.stream().flatMap(u -> emptyIfNull(u.getWriteAccess()).stream())
		).distinct().toList());
		result.setTagReadAccess(users.stream().flatMap(u -> emptyIfNull(u.getTagReadAccess()).stream()).distinct().toList());
		result.setTagWriteAccess(users.stream().flatMap(u -> emptyIfNull(u.getTagWriteAccess()).stream()).distinct().toList());
		return of(result);
	}
}