diff --git a/onyx-api/.gitignore b/onyx-api/.gitignore index 549e00a..b8163f5 100644 --- a/onyx-api/.gitignore +++ b/onyx-api/.gitignore @@ -31,3 +31,6 @@ build/ ### VS Code ### .vscode/ + +*.mv.db +*.db diff --git a/onyx-api/README.md b/onyx-api/README.md index 2238a08..b630734 100644 --- a/onyx-api/README.md +++ b/onyx-api/README.md @@ -19,4 +19,4 @@ Therefore, you can think of onyx as a more robust, secure digital drive for your ## Authentication -Because each onyx node operates independently, there is no central authentication service, but instead each node maintains a set of users, and issues short-lived tokens granting users access to secure resources. When a user needs to access resources from a networked onyx node, their home node will send the user's id and the home node's own thing. +Because each onyx node operates independently, there is no central authentication service, but instead each node maintains a set of users, and issues short-lived tokens granting users access to secure resources. When a user needs to access resources from a networked onyx node, they'll use their own token, which the networked node will independently verify was issued recently by the user's home node. diff --git a/onyx-api/src/main/java/com/andrewlalis/onyx/content/ContentAccessLevel.java b/onyx-api/src/main/java/com/andrewlalis/onyx/content/ContentAccessLevel.java new file mode 100644 index 0000000..e15983a --- /dev/null +++ b/onyx-api/src/main/java/com/andrewlalis/onyx/content/ContentAccessLevel.java @@ -0,0 +1,26 @@ +package com.andrewlalis.onyx.content; + +/** + * The different levels of access that can be set for content in an onyx node. + */ +public enum ContentAccessLevel { + /** + * An access level that does not permit reading or editing the content. + */ + NONE, + + /** + * An access level that permits reading content, but not editing. + */ + VIEW, + + /** + * An access level that permits reading and editing content. + */ + EDIT, + + /** + * An access level that takes the parent node's access level. + */ + INHERIT +} diff --git a/onyx-api/src/main/java/com/andrewlalis/onyx/content/ContentAccessRules.java b/onyx-api/src/main/java/com/andrewlalis/onyx/content/ContentAccessRules.java new file mode 100644 index 0000000..14d8249 --- /dev/null +++ b/onyx-api/src/main/java/com/andrewlalis/onyx/content/ContentAccessRules.java @@ -0,0 +1,37 @@ +package com.andrewlalis.onyx.content; + +import jakarta.persistence.*; +import lombok.Getter; + +/** + * An entity meant to be attached to a {@link ContentNode}, which contains the + * access levels for the different ways in which a content node can be accessed. + */ +@Entity +@Table(name = "content_access_rules") +@Getter +public class ContentAccessRules { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + /** + * The content node that these access rules apply to. + */ + @OneToOne(mappedBy = "accessInfo", fetch = FetchType.LAZY) + private ContentNode contentNode; + + /** The access level that applies to users from outside this onyx network. */ + @Enumerated(EnumType.ORDINAL) @Column(nullable = false, columnDefinition = "TINYINT NOT NULL") + private ContentAccessLevel publicAccessLevel = ContentAccessLevel.INHERIT; + + /** The access level that applies to users from within this onyx network. */ + @Enumerated(EnumType.ORDINAL) @Column(nullable = false, columnDefinition = "TINYINT NOT NULL") + private ContentAccessLevel networkAccessLevel = ContentAccessLevel.INHERIT; + + /** The access level that applies to users within only this onyx node. */ + @Enumerated(EnumType.ORDINAL) @Column(nullable = false, columnDefinition = "TINYINT NOT NULL") + private ContentAccessLevel nodeAccessLevel = ContentAccessLevel.INHERIT; + + // TODO: Add a user allowlist. + +} diff --git a/onyx-api/src/main/java/com/andrewlalis/onyx/content/ContentContainerNode.java b/onyx-api/src/main/java/com/andrewlalis/onyx/content/ContentContainerNode.java new file mode 100644 index 0000000..68eea77 --- /dev/null +++ b/onyx-api/src/main/java/com/andrewlalis/onyx/content/ContentContainerNode.java @@ -0,0 +1,19 @@ +package com.andrewlalis.onyx.content; + +import jakarta.persistence.*; + +import java.util.Set; + +/** + * A type of content node that contains a list of children, which could + * themselves be any type of content node. + */ +@Entity +@Table(name = "content_node_container") +public final class ContentContainerNode extends ContentNode { + /** + * The set of children that belong to this container. + */ + @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "parentContainer") + private Set children; +} diff --git a/onyx-api/src/main/java/com/andrewlalis/onyx/content/ContentDocumentNode.java b/onyx-api/src/main/java/com/andrewlalis/onyx/content/ContentDocumentNode.java new file mode 100644 index 0000000..d8a5382 --- /dev/null +++ b/onyx-api/src/main/java/com/andrewlalis/onyx/content/ContentDocumentNode.java @@ -0,0 +1,22 @@ +package com.andrewlalis.onyx.content; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Lob; +import jakarta.persistence.Table; + +/** + * A type of content node that contains a single document. + */ +@Entity +@Table(name = "content_node_document") +public class ContentDocumentNode extends ContentNode { + @Column(nullable = false, updatable = false, length = 127) + private String type; + + /** + * The raw file content for this document. + */ + @Lob @Column(nullable = false) + private byte[] content; +} diff --git a/onyx-api/src/main/java/com/andrewlalis/onyx/content/ContentNode.java b/onyx-api/src/main/java/com/andrewlalis/onyx/content/ContentNode.java new file mode 100644 index 0000000..1fc31e6 --- /dev/null +++ b/onyx-api/src/main/java/com/andrewlalis/onyx/content/ContentNode.java @@ -0,0 +1,56 @@ +package com.andrewlalis.onyx.content; + +import com.andrewlalis.onyx.content.history.ContentNodeHistory; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +/** + * The abstract model that represents all nodes in the system's hierarchical + * content tree, including both containers and documents. + */ +@Entity +@Table(name = "content_node") +@Inheritance(strategy = InheritanceType.JOINED) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public abstract class ContentNode { + public static final int MAX_NAME_LENGTH = 127; + + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @Column(nullable = false, length = MAX_NAME_LENGTH) + private String name; + + @Enumerated(EnumType.ORDINAL) @Column(columnDefinition = "TINYINT NOT NULL") + private Type type; + + @OneToOne(fetch = FetchType.LAZY, optional = false, orphanRemoval = true, cascade = CascadeType.ALL) + private ContentAccessRules accessInfo; + + /** + * The container node that this one belongs to. This will be null only + * in the case of the root node, which is a special, hidden container node + * that acts as the top-level of the content hierarchy. + */ + @ManyToOne(fetch = FetchType.LAZY) + private ContentContainerNode parentContainer; + + @OneToOne(fetch = FetchType.LAZY, optional = false, orphanRemoval = true, cascade = CascadeType.ALL) + private ContentNodeHistory history; + + @Column(nullable = false) + private boolean archived; + + public enum Type { + CONTAINER, + DOCUMENT + } + + public ContentNode(String name, Type type, ContentContainerNode parentContainer) { + this.name = name; + this.type = type; + this.parentContainer = parentContainer; + this.history = new ContentNodeHistory(this); + } +} diff --git a/onyx-api/src/main/java/com/andrewlalis/onyx/content/history/ContentNodeHistory.java b/onyx-api/src/main/java/com/andrewlalis/onyx/content/history/ContentNodeHistory.java new file mode 100644 index 0000000..dccc5da --- /dev/null +++ b/onyx-api/src/main/java/com/andrewlalis/onyx/content/history/ContentNodeHistory.java @@ -0,0 +1,37 @@ +package com.andrewlalis.onyx.content.history; + +import com.andrewlalis.onyx.content.ContentNode; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.CreationTimestamp; + +import java.time.LocalDateTime; +import java.util.HashSet; +import java.util.Set; + +/** + * An entity that represents the complete history of a single content node, + * storing an ordered list of entries detailing how that node has changed. + */ +@Entity +@Table(name = "content_node_history") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ContentNodeHistory { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @OneToOne(mappedBy = "history") + private ContentNode contentNode; + + @CreationTimestamp + private LocalDateTime createdAt; + + @OneToMany(fetch = FetchType.LAZY, orphanRemoval = true, cascade = CascadeType.ALL, mappedBy = "history") + private Set entries; + + public ContentNodeHistory(ContentNode contentNode) { + this.contentNode = contentNode; + this.entries = new HashSet<>(); + } +} diff --git a/onyx-api/src/main/java/com/andrewlalis/onyx/content/history/ContentNodeHistoryEntry.java b/onyx-api/src/main/java/com/andrewlalis/onyx/content/history/ContentNodeHistoryEntry.java new file mode 100644 index 0000000..5f55dd9 --- /dev/null +++ b/onyx-api/src/main/java/com/andrewlalis/onyx/content/history/ContentNodeHistoryEntry.java @@ -0,0 +1,35 @@ +package com.andrewlalis.onyx.content.history; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.CreationTimestamp; + +import java.time.LocalDateTime; + +/** + * The base class for all types of content node history entries. It defines + * some basic properties that all entries should have. + */ +@Entity +@Table(name = "content_node_history_entry") +@Inheritance(strategy = InheritanceType.JOINED) +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public abstract class ContentNodeHistoryEntry { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + private long id; + + @CreationTimestamp + private LocalDateTime timestamp; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + private ContentNodeHistory history; + + // TODO: Add user info (or system) + + public ContentNodeHistoryEntry(ContentNodeHistory history) { + this.history = history; + } +} diff --git a/onyx-api/src/main/java/com/andrewlalis/onyx/content/history/entry/AccessRulesEntry.java b/onyx-api/src/main/java/com/andrewlalis/onyx/content/history/entry/AccessRulesEntry.java new file mode 100644 index 0000000..6b2768b --- /dev/null +++ b/onyx-api/src/main/java/com/andrewlalis/onyx/content/history/entry/AccessRulesEntry.java @@ -0,0 +1,53 @@ +package com.andrewlalis.onyx.content.history.entry; + +import com.andrewlalis.onyx.content.ContentAccessLevel; +import com.andrewlalis.onyx.content.history.ContentNodeHistory; +import com.andrewlalis.onyx.content.history.ContentNodeHistoryEntry; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * A history entry that records a modification to a content node's access rules + * by a user or the system. + */ +@Entity +@Table(name = "content_node_history_entry_access_rules") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class AccessRulesEntry extends ContentNodeHistoryEntry { + @Enumerated(EnumType.ORDINAL) @Column(nullable = false, columnDefinition = "TINYINT NOT NULL") + private ContentAccessLevel oldPublicAccessLevel; + @Enumerated(EnumType.ORDINAL) @Column(nullable = false, columnDefinition = "TINYINT NOT NULL") + private ContentAccessLevel oldNetworkAccessLevel; + @Enumerated(EnumType.ORDINAL) @Column(nullable = false, columnDefinition = "TINYINT NOT NULL") + private ContentAccessLevel oldNodeAccessLevel; + + @Enumerated(EnumType.ORDINAL) @Column(nullable = false, columnDefinition = "TINYINT NOT NULL") + private ContentAccessLevel newPublicAccessLevel; + @Enumerated(EnumType.ORDINAL) @Column(nullable = false, columnDefinition = "TINYINT NOT NULL") + private ContentAccessLevel newNetworkAccessLevel; + @Enumerated(EnumType.ORDINAL) @Column(nullable = false, columnDefinition = "TINYINT NOT NULL") + private ContentAccessLevel newNodeAccessLevel; + + // TODO: add usersAllowed, usersDisallowed + + public AccessRulesEntry( + ContentNodeHistory history, + ContentAccessLevel oldPublicAccessLevel, + ContentAccessLevel oldNetworkAccessLevel, + ContentAccessLevel oldNodeAccessLevel, + ContentAccessLevel newPublicAccessLevel, + ContentAccessLevel newNetworkAccessLevel, + ContentAccessLevel newNodeAccessLevel + ) { + super(history); + this.oldPublicAccessLevel = oldPublicAccessLevel; + this.oldNetworkAccessLevel = oldNetworkAccessLevel; + this.oldNodeAccessLevel = oldNodeAccessLevel; + this.newPublicAccessLevel = newPublicAccessLevel; + this.newNetworkAccessLevel = newNetworkAccessLevel; + this.newNodeAccessLevel = newNodeAccessLevel; + } +} diff --git a/onyx-api/src/main/java/com/andrewlalis/onyx/content/history/entry/ArchivedEntry.java b/onyx-api/src/main/java/com/andrewlalis/onyx/content/history/entry/ArchivedEntry.java new file mode 100644 index 0000000..ad8875d --- /dev/null +++ b/onyx-api/src/main/java/com/andrewlalis/onyx/content/history/entry/ArchivedEntry.java @@ -0,0 +1,27 @@ +package com.andrewlalis.onyx.content.history.entry; + +import com.andrewlalis.onyx.content.history.ContentNodeHistory; +import com.andrewlalis.onyx.content.history.ContentNodeHistoryEntry; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** + * History entry for tracking the state of a content node's `archived` status. + */ +@Entity +@Table(name = "content_node_history_entry_archived") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ArchivedEntry extends ContentNodeHistoryEntry { + @Column(nullable = false, updatable = false) + private boolean archived; + + public ArchivedEntry(ContentNodeHistory history, boolean archived) { + super(history); + this.archived = archived; + } +} diff --git a/onyx-api/src/main/java/com/andrewlalis/onyx/content/history/entry/ContainerEditEntry.java b/onyx-api/src/main/java/com/andrewlalis/onyx/content/history/entry/ContainerEditEntry.java new file mode 100644 index 0000000..000eb8b --- /dev/null +++ b/onyx-api/src/main/java/com/andrewlalis/onyx/content/history/entry/ContainerEditEntry.java @@ -0,0 +1,35 @@ +package com.andrewlalis.onyx.content.history.entry; + +import com.andrewlalis.onyx.content.ContentNode; +import com.andrewlalis.onyx.content.history.ContentNodeHistory; +import com.andrewlalis.onyx.content.history.ContentNodeHistoryEntry; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +/** + * History entry for tracking updates to the contents of a container node. + */ +@Entity +@Table(name = "content_node_history_entry_container_edit") +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ContainerEditEntry extends ContentNodeHistoryEntry { + public enum EditType { + CONTAINER_ADDED, + CONTAINER_REMOVED, + DOCUMENT_ADDED, + DOCUMENT_REMOVED + } + + @Enumerated(EnumType.ORDINAL) + private EditType type; + + @Column(nullable = false, updatable = false, length = ContentNode.MAX_NAME_LENGTH) + private String affectedNodeName; + + public ContainerEditEntry(ContentNodeHistory history, EditType type, String affectedNodeName) { + super(history); + this.type = type; + this.affectedNodeName = affectedNodeName; + } +} diff --git a/onyx-api/src/main/java/com/andrewlalis/onyx/content/history/entry/RenameEntry.java b/onyx-api/src/main/java/com/andrewlalis/onyx/content/history/entry/RenameEntry.java new file mode 100644 index 0000000..0854351 --- /dev/null +++ b/onyx-api/src/main/java/com/andrewlalis/onyx/content/history/entry/RenameEntry.java @@ -0,0 +1,29 @@ +package com.andrewlalis.onyx.content.history.entry; + +import com.andrewlalis.onyx.content.ContentNode; +import com.andrewlalis.onyx.content.history.ContentNodeHistory; +import com.andrewlalis.onyx.content.history.ContentNodeHistoryEntry; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "content_node_history_entry_rename") +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class RenameEntry extends ContentNodeHistoryEntry { + @Column(nullable = false, updatable = false, length = ContentNode.MAX_NAME_LENGTH) + private String oldName; + + @Column(nullable = false, updatable = false, length = ContentNode.MAX_NAME_LENGTH) + private String newName; + + public RenameEntry(ContentNodeHistory history, String oldName, String newName) { + super(history); + this.oldName = oldName; + this.newName = newName; + } +} diff --git a/onyx-api/src/main/java/com/andrewlalis/onyx/content/package-info.java b/onyx-api/src/main/java/com/andrewlalis/onyx/content/package-info.java new file mode 100644 index 0000000..31c9f05 --- /dev/null +++ b/onyx-api/src/main/java/com/andrewlalis/onyx/content/package-info.java @@ -0,0 +1,6 @@ +/** + * This package defines the content module for the onyx node, + * which is responsible for managing the actual hierarchical set of documents + * that are directly stored by the node. + */ +package com.andrewlalis.onyx.content; \ No newline at end of file diff --git a/onyx-api/src/main/resources/application-development.properties b/onyx-api/src/main/resources/application-development.properties new file mode 100644 index 0000000..a684ac0 --- /dev/null +++ b/onyx-api/src/main/resources/application-development.properties @@ -0,0 +1,5 @@ +# Development-specific settings. + +spring.datasource.url=jdbc:h2:./onyx-db-dev +spring.datasource.driver-class-name=org.h2.Driver +spring.jpa.hibernate.ddl-auto=create