diff --git a/dbrepo-authentication-service/dbrepo-realm.json b/dbrepo-authentication-service/dbrepo-realm.json index 99be5195d410b5b78f99d119b781df093d43425d..c3b1302374829703fd21ad3adc553d65ac8d9bf2 100644 --- a/dbrepo-authentication-service/dbrepo-realm.json +++ b/dbrepo-authentication-service/dbrepo-realm.json @@ -120,6 +120,17 @@ "clientRole" : false, "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", "attributes" : { } + }, { + "id" : "e63e61a2-d852-4ad3-bfb5-92d9ceafef6a", + "name" : "escalated-user-handling", + "description" : "${escalated-user-handling}", + "composite" : true, + "composites" : { + "realm" : [ "find-user" ] + }, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } }, { "id" : "be4e1aba-e276-4241-b6ea-01dce6c52f8b", "name" : "find-container", @@ -206,6 +217,14 @@ "clientRole" : false, "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", "attributes" : { } + }, { + "id" : "993b5c69-9eb2-42af-ac28-b4a46c6b61f2", + "name" : "find-user", + "description" : "${find-user}", + "composite" : false, + "clientRole" : false, + "containerId" : "82c39861-d877-4667-a0f3-4daa2ee230e0", + "attributes" : { } }, { "id" : "e4cfdc4d-2373-477b-a8df-161db99aba00", "name" : "create-foreign-identifier", @@ -1880,7 +1899,7 @@ "subType" : "authenticated", "subComponents" : { }, "config" : { - "allowed-protocol-mapper-types" : [ "oidc-usermodel-attribute-mapper", "oidc-usermodel-property-mapper", "oidc-full-name-mapper", "saml-role-list-mapper", "saml-user-property-mapper", "oidc-address-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-user-attribute-mapper" ] + "allowed-protocol-mapper-types" : [ "oidc-full-name-mapper", "oidc-usermodel-property-mapper", "oidc-usermodel-attribute-mapper", "saml-user-property-mapper", "saml-role-list-mapper", "oidc-address-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-user-attribute-mapper" ] } }, { "id" : "3ab11d74-5e76-408a-b85a-26bf8950f979", @@ -1889,7 +1908,7 @@ "subType" : "anonymous", "subComponents" : { }, "config" : { - "allowed-protocol-mapper-types" : [ "oidc-usermodel-property-mapper", "saml-role-list-mapper", "saml-user-attribute-mapper", "oidc-address-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-usermodel-attribute-mapper", "saml-user-property-mapper", "oidc-full-name-mapper" ] + "allowed-protocol-mapper-types" : [ "oidc-usermodel-attribute-mapper", "saml-role-list-mapper", "oidc-full-name-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-user-attribute-mapper", "saml-user-property-mapper", "oidc-address-mapper", "oidc-usermodel-property-mapper" ] } } ], "org.keycloak.keys.KeyProvider" : [ { @@ -1941,7 +1960,7 @@ "internationalizationEnabled" : false, "supportedLocales" : [ ], "authenticationFlows" : [ { - "id" : "e29d33f1-c44c-4f6b-b8ca-54eb4b468f1d", + "id" : "85d85037-1863-4869-b9ab-09582853f779", "alias" : "Account verification options", "description" : "Method with which to verity the existing account", "providerId" : "basic-flow", @@ -1963,7 +1982,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "bc4badef-d637-4448-b345-3e79f24290b0", + "id" : "e89d11ed-c578-409a-aaed-d00db2951f66", "alias" : "Authentication Options", "description" : "Authentication options.", "providerId" : "basic-flow", @@ -1992,7 +2011,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "06ddc635-bb0d-443a-ab6b-7f56be7fd59a", + "id" : "5bfb765d-30bf-4708-b85e-01beb0813a75", "alias" : "Browser - Conditional OTP", "description" : "Flow to determine if the OTP is required for the authentication", "providerId" : "basic-flow", @@ -2014,7 +2033,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "a8230646-a4a8-40c2-a37a-e7ac4daf9a67", + "id" : "dfe729c0-df4b-4b9b-8170-fd29d703f691", "alias" : "Direct Grant - Conditional OTP", "description" : "Flow to determine if the OTP is required for the authentication", "providerId" : "basic-flow", @@ -2036,7 +2055,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "e84a6624-232f-401e-9937-6c5865b2a341", + "id" : "e52f6f8c-edd8-42eb-a956-1e642d054a09", "alias" : "First broker login - Conditional OTP", "description" : "Flow to determine if the OTP is required for the authentication", "providerId" : "basic-flow", @@ -2058,7 +2077,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "00094f7f-cdcb-4670-87b0-d75ed3fa986b", + "id" : "0fcdfbb3-4e4f-4c55-9e06-9baf3afef314", "alias" : "Handle Existing Account", "description" : "Handle what to do if there is existing account with same email/username like authenticated identity provider", "providerId" : "basic-flow", @@ -2080,7 +2099,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "e8cea56d-d07c-4696-b389-0f70449ba26d", + "id" : "a3db4f3a-a772-4530-b948-c2ea688dc993", "alias" : "Reset - Conditional OTP", "description" : "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", "providerId" : "basic-flow", @@ -2102,7 +2121,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "505a65be-a4f8-4e6e-a1a0-21d20d3f07a5", + "id" : "d540d238-e69c-4eb1-8238-bf43c9f59118", "alias" : "User creation or linking", "description" : "Flow for the existing/non-existing user alternatives", "providerId" : "basic-flow", @@ -2125,7 +2144,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "9c31dad3-3367-4f7e-9fd0-3c855e1da0b8", + "id" : "b0b433e3-3a58-4915-8833-ad55fef4aab7", "alias" : "Verify Existing Account by Re-authentication", "description" : "Reauthentication of existing account", "providerId" : "basic-flow", @@ -2147,7 +2166,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "e6fdd307-ca87-43f0-9f74-2bc567cc02db", + "id" : "68ec036a-f399-4980-80b7-a27867f5e650", "alias" : "browser", "description" : "browser based authentication", "providerId" : "basic-flow", @@ -2183,7 +2202,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "b42d375b-1cb9-4cc7-be7d-fae32791d0a8", + "id" : "aa32f681-942b-4194-b8df-124f210bcaa9", "alias" : "clients", "description" : "Base authentication for clients", "providerId" : "client-flow", @@ -2219,7 +2238,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "3c534feb-1736-4ff4-8187-192e49f21fd9", + "id" : "1169765a-7850-4a9a-9a72-9a9dcf75ac8b", "alias" : "direct grant", "description" : "OpenID Connect Resource Owner Grant", "providerId" : "basic-flow", @@ -2248,7 +2267,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "5f3c06ca-8691-4c6c-8892-8d29563da9d0", + "id" : "60f23852-a761-4290-982b-c51876f4c382", "alias" : "docker auth", "description" : "Used by Docker clients to authenticate against the IDP", "providerId" : "basic-flow", @@ -2263,7 +2282,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "76abf3cd-3635-494e-b75d-a3132a7933a5", + "id" : "a09aa7bd-3f8f-444d-83d0-f095b5f7c6bb", "alias" : "first broker login", "description" : "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", "providerId" : "basic-flow", @@ -2286,7 +2305,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "d44f3e97-d11a-49ba-a3bf-7c8489de290d", + "id" : "85d2028a-ab84-4fcb-8088-94c927051018", "alias" : "forms", "description" : "Username, password, otp and other auth forms.", "providerId" : "basic-flow", @@ -2308,7 +2327,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "7e60e213-6272-484c-b620-d7cf4f98f950", + "id" : "43ffa27d-3940-4b9b-857d-85f2b3729710", "alias" : "http challenge", "description" : "An authentication flow based on challenge-response HTTP Authentication Schemes", "providerId" : "basic-flow", @@ -2330,7 +2349,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "cce9e0e9-82f4-499d-a6f7-fc556248fc25", + "id" : "2e900379-7ae7-431d-a586-2014f7688aa0", "alias" : "registration", "description" : "registration flow", "providerId" : "basic-flow", @@ -2346,7 +2365,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "61f08b26-e87b-4126-ac7a-c704d01e54dc", + "id" : "844942ca-41ae-4d50-b3ab-7815d9df6332", "alias" : "registration form", "description" : "registration form", "providerId" : "form-flow", @@ -2382,7 +2401,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "d9ce077f-16c2-4c96-aba5-dd60bb3aa536", + "id" : "7c108a2a-abbd-462e-a9fc-917f28b67f80", "alias" : "reset credentials", "description" : "Reset credentials for a user if they forgot their password or something", "providerId" : "basic-flow", @@ -2418,7 +2437,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "8d4383f0-ebd2-4e0b-8d3f-2c6e1793c05b", + "id" : "b47ae4b9-a177-44ef-b41e-c7e5da6220c7", "alias" : "saml ecp", "description" : "SAML ECP Profile Authentication Flow", "providerId" : "basic-flow", @@ -2434,13 +2453,13 @@ } ] } ], "authenticatorConfig" : [ { - "id" : "1206bd7a-0525-4e21-9bc7-d9ec62c1a4bc", + "id" : "81c9b7e4-2574-46b1-8a2a-e35edc716c1c", "alias" : "create unique user config", "config" : { "require.password.update.after.registration" : "false" } }, { - "id" : "f5ff277d-b059-4dff-aaf8-8c5feeca1d4c", + "id" : "14491568-0d51-4082-9bb2-216d3cb4ab34", "alias" : "review profile config", "config" : { "update.profile.on.first.login" : "missing" diff --git a/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/database/table/TableBriefDto.java b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/database/table/TableBriefDto.java index eff9f4ba22ed0430057084e57de173f00d954a85..aa4f25af3b379ac3731af006208dbe2299f2908b 100644 --- a/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/database/table/TableBriefDto.java +++ b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/database/table/TableBriefDto.java @@ -35,8 +35,8 @@ public class TableBriefDto { @Schema(example = "air_quality") private String internalName; - @NotNull(message = "creator is required") - private UserBriefDto creator; + @NotNull(message = "owner is required") + private UserBriefDto owner; @NotNull(message = "columns are required") @org.springframework.data.annotation.Transient diff --git a/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/database/table/TableDto.java b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/database/table/TableDto.java index 22475b247b929aa6b884a46802d675bf708626a5..25315bd658cff125b4a0fba15564ec3c719d683b 100644 --- a/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/database/table/TableDto.java +++ b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/database/table/TableDto.java @@ -45,6 +45,9 @@ public class TableDto { @NotNull(message = "creator is required") private UserBriefDto creator; + @NotNull(message = "owner is required") + private UserBriefDto owner; + @NotBlank(message = "queueName is required") @JsonProperty("queue_name") @Schema(example = "dbrepo/air_quality/air_quality") diff --git a/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/identifier/IdentifierCreateDto.java b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/identifier/IdentifierCreateDto.java index 67a9e858e95b8dc72b9074870de4e697346e72f8..e460cd8c59061e831aad1ba27c8378d5bd32e9f0 100644 --- a/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/identifier/IdentifierCreateDto.java +++ b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/identifier/IdentifierCreateDto.java @@ -38,6 +38,10 @@ public class IdentifierCreateDto { @Schema(example = "Air quality reports at Stephansplatz, Vienna") private String description; + @NotNull + @Schema(example = "everyone") + private VisibilityTypeDto visibility; + @JsonProperty("publication_day") @Schema(example = "15") private Integer publicationDay; diff --git a/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/identifier/IdentifierDto.java b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/identifier/IdentifierDto.java index 69e448003e42fcf4be8e0f40ead5b7a62ec7055d..1234beec9630665c55479a7c7929e3ffca2a86d9 100644 --- a/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/identifier/IdentifierDto.java +++ b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/identifier/IdentifierDto.java @@ -80,6 +80,10 @@ public class IdentifierDto { @Schema(example = "1") private Long resultNumber; + @NotNull + @Schema(example = "everyone") + private VisibilityTypeDto visibility; + @Schema(example = "10.1038/nphys1170") private String doi; diff --git a/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/identifier/VisibilityTypeDto.java b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/identifier/VisibilityTypeDto.java new file mode 100644 index 0000000000000000000000000000000000000000..19a913c61294907f4ffd65260cc616aebad8d260 --- /dev/null +++ b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/identifier/VisibilityTypeDto.java @@ -0,0 +1,26 @@ +package at.tuwien.api.identifier; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.ToString; + +@Getter +public enum VisibilityTypeDto { + + @JsonProperty("everyone") + EVERYONE("everyone"), + + @JsonProperty("self") + SELF("self"); + + private String name; + + VisibilityTypeDto(String name) { + this.name = name; + } + + @Override + public String toString() { + return this.name; + } +} \ No newline at end of file diff --git a/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/user/UserUpdateDto.java b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/user/UserUpdateDto.java index 108b54902e491615244797583670bc28b767d62f..a14c2e211fb3a1a6acd633f7894f1294f7f5ad49 100644 --- a/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/user/UserUpdateDto.java +++ b/dbrepo-metadata-db/api/src/main/java/at/tuwien/api/user/UserUpdateDto.java @@ -1,6 +1,5 @@ package at.tuwien.api.user; -import com.fasterxml.jackson.annotation.JsonProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; @@ -12,13 +11,6 @@ import lombok.*; @NoArgsConstructor public class UserUpdateDto { - @JsonProperty("titles_before") - @Schema(example = "Prof.") - private String titlesBefore; - - @JsonProperty("titles_after") - private String titlesAfter; - @Schema(example = "Josiah") private String firstname; diff --git a/dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/database/table/Table.java b/dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/database/table/Table.java index e05a1e03ae060f17bfec5da1027d3141104fa096..75b506a6c6aff6ef94051ff6caee2e760b3dcef9 100644 --- a/dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/database/table/Table.java +++ b/dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/database/table/Table.java @@ -45,6 +45,12 @@ public class Table { }) private User creator; + @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST) + @JoinColumns({ + @JoinColumn(name = "ownedBy", referencedColumnName = "ID", nullable = false, columnDefinition = "VARCHAR(36)", updatable = false) + }) + private User owner; + @Column(nullable = false, name = "tname") private String name; diff --git a/dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/identifier/Identifier.java b/dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/identifier/Identifier.java index 294a9c954db51f0144222de26f07e38e458be02e..6210ab7c35d4215f5d8090a7d9064dcd0b54f839 100644 --- a/dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/identifier/Identifier.java +++ b/dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/identifier/Identifier.java @@ -101,6 +101,10 @@ public class Identifier implements Serializable { @Column private Integer publicationDay; + @Column(nullable = false, columnDefinition = "enum('EVERYONE', 'SELF')") + @Enumerated(EnumType.STRING) + private VisibilityType visibility; + @OneToOne(fetch = FetchType.LAZY, cascade = {}) @JoinColumns({ @JoinColumn(name = "dbid", referencedColumnName = "id", insertable = false, updatable = false) diff --git a/dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/identifier/VisibilityType.java b/dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/identifier/VisibilityType.java new file mode 100644 index 0000000000000000000000000000000000000000..3565740c6b2042fd4fdb003e68a19f369710efcf --- /dev/null +++ b/dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/identifier/VisibilityType.java @@ -0,0 +1,11 @@ +package at.tuwien.entities.identifier; + +import lombok.Getter; +import lombok.ToString; + +@Getter +@ToString +public enum VisibilityType { + EVERYONE, + SELF; +} \ No newline at end of file diff --git a/dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/user/Role.java b/dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/user/Role.java index a06563f1d6c0d6398ba5fba9665cc846d8a7aeeb..17a0446bc87509b8ddb4754815b8e4f18fb06e57 100644 --- a/dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/user/Role.java +++ b/dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/user/Role.java @@ -1,12 +1,12 @@ package at.tuwien.entities.user; import lombok.*; -import org.hibernate.annotations.GenericGenerator; import org.hibernate.annotations.Immutable; import org.hibernate.annotations.Type; import org.springframework.data.jpa.domain.support.AuditingEntityListener; import javax.persistence.*; +import java.util.List; import java.util.UUID; @Data @@ -30,18 +30,12 @@ public class Role { @Column(name = "NAME", nullable = false) private String name; - @Column(name = "REALM_ID", nullable = false) - private String realmId; + @Column(name = "REALM_ID", nullable = false, columnDefinition = "VARCHAR(36)") + @Type(type = "uuid-char") + private UUID realmId; @ToString.Exclude - @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL) - @JoinTable(name = "user_role_mapping", - joinColumns = { - @JoinColumn(name = "ROLE_ID", referencedColumnName = "ID", insertable = false, updatable = false), - }, - inverseJoinColumns = { - @JoinColumn(name = "USER_ID", referencedColumnName = "ID", insertable = false, updatable = false), - }) - private User user; + @ManyToMany(fetch = FetchType.LAZY, mappedBy = "roles") + private List<User> users; } diff --git a/dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/user/RoleMapping.java b/dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/user/RoleMapping.java index 19b46e3307ab3b03768e2380bb85870f91cdf4b4..08d5df4b4d1ae70f6dbe15dd86cd45352141003c 100644 --- a/dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/user/RoleMapping.java +++ b/dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/user/RoleMapping.java @@ -16,7 +16,9 @@ import java.util.UUID; @IdClass(RoleMappingKey.class) @EntityListeners(AuditingEntityListener.class) @EqualsAndHashCode(onlyExplicitlyIncluded = true) -@Table(name = "user_role_mapping") +@Table(name = "user_role_mapping", uniqueConstraints = { + @UniqueConstraint(columnNames = {"USER_ID", "ROLE_ID"}) +}) public class RoleMapping { @Id diff --git a/dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/user/User.java b/dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/user/User.java index 98e6fbda337b38df9f931a586e9e5034d41fe58a..109e63b06dc8f96e0ddfce6328901f5c34fc87ca 100644 --- a/dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/user/User.java +++ b/dbrepo-metadata-db/entities/src/main/java/at/tuwien/entities/user/User.java @@ -69,11 +69,6 @@ public class User { @Column(nullable = false) private String databasePassword; - @Column(nullable = false) - @ToString.Exclude - @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "user") - private List<Role> roles; - @Column(nullable = false) @ToString.Exclude @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "user") @@ -99,6 +94,17 @@ public class User { @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "creator") private List<Identifier> identifiers; + @ToString.Exclude + @ManyToMany(fetch = FetchType.LAZY) + @JoinTable(name = "user_role_mapping", + joinColumns = { + @JoinColumn(name = "USER_ID", nullable = false, columnDefinition = "VARCHAR(36)", updatable = false), + }, + inverseJoinColumns = { + @JoinColumn(name = "ROLE_ID", nullable = false, columnDefinition = "VARCHAR(36)", updatable = false), + }) + private List<Role> roles; + @Transient @ToString.Exclude @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "creator") diff --git a/dbrepo-metadata-db/setup-schema.sql b/dbrepo-metadata-db/setup-schema.sql index 31eefbbbd3700c3be6724c81caac9b7c4cb643a1..9529463f2471d5163e4be25ad923caaf56da1ba1 100644 --- a/dbrepo-metadata-db/setup-schema.sql +++ b/dbrepo-metadata-db/setup-schema.sql @@ -126,6 +126,7 @@ CREATE TABLE IF NOT EXISTS `fda`.`mdb_tables` Version TEXT, created timestamp NOT NULL DEFAULT NOW(), created_by character varying(255) NOT NULL, + owned_by character varying(255) NOT NULL, last_modified timestamp, PRIMARY KEY (ID, tDBID), FOREIGN KEY (tDBID) REFERENCES mdb_databases (id) @@ -348,19 +349,20 @@ CREATE TABLE IF NOT EXISTS `fda`.`mdb_view_columns` CREATE TABLE IF NOT EXISTS `fda`.`mdb_identifiers` ( - id bigint NOT NULL AUTO_INCREMENT, - cid bigint NOT NULL, - dbid bigint NOT NULL, + id bigint NOT NULL AUTO_INCREMENT, + cid bigint NOT NULL, + dbid bigint NOT NULL, qid bigint, - title VARCHAR(255) NOT NULL, - publisher VARCHAR(255) NOT NULL, + title VARCHAR(255) NOT NULL, + publisher VARCHAR(255) NOT NULL, language VARCHAR(50), license VARCHAR(50), description TEXT, - publication_year INTEGER NOT NULL, + visibility ENUM ('SELF', 'EVERYONE') NOT NULL default 'EVERYONE', + publication_year INTEGER NOT NULL, publication_month INTEGER, publication_day INTEGER, - identifier_type varchar(50) NOT NULL, + identifier_type varchar(50) NOT NULL, query TEXT, query_normalized TEXT, query_hash VARCHAR(255), @@ -368,8 +370,8 @@ CREATE TABLE IF NOT EXISTS `fda`.`mdb_identifiers` result_hash VARCHAR(255), result_number bigint, doi VARCHAR(255), - created timestamp NOT NULL DEFAULT NOW(), - created_by character varying(255) NOT NULL, + created timestamp NOT NULL DEFAULT NOW(), + created_by character varying(255) NOT NULL, last_modified timestamp, PRIMARY KEY (id), /* must be a single id from persistent identifier concept */ FOREIGN KEY (cid) REFERENCES mdb_containers (id), diff --git a/dbrepo-metadata-db/test/src/main/java/at/tuwien/test/BaseTest.java b/dbrepo-metadata-db/test/src/main/java/at/tuwien/test/BaseTest.java index 43b2d75a3aebe49d85f0d5197539597bee790cc0..987f301117ffd757bf26f187e11e3d561e6818fb 100644 --- a/dbrepo-metadata-db/test/src/main/java/at/tuwien/test/BaseTest.java +++ b/dbrepo-metadata-db/test/src/main/java/at/tuwien/test/BaseTest.java @@ -31,6 +31,7 @@ import at.tuwien.entities.database.table.constraints.foreignKey.ForeignKeyRefere import at.tuwien.entities.database.table.constraints.unique.Unique; import at.tuwien.entities.identifier.*; import at.tuwien.entities.user.Realm; +import at.tuwien.entities.user.Role; import at.tuwien.entities.user.User; import at.tuwien.entities.user.UserAttribute; import at.tuwien.querystore.Query; @@ -134,14 +135,17 @@ public abstract class BaseTest { public final static String[] DEFAULT_USER_HANDLING = new String[]{"default-user-handling", "modify-user-theme", "modify-user-information"}; + public final static String[] ESCALATED_USER_HANDLING = new String[]{"escalated-user-handling", "find-user"}; + public final static String[] DEFAULT_RESEARCHER_ROLES = ArrayUtil.merge(List.of(new String[]{"default-researcher-roles"}, DEFAULT_CONTAINER_HANDLING, DEFAULT_DATABASE_HANDLING, DEFAULT_IDENTIFIER_HANDLING, DEFAULT_QUERY_HANDLING, DEFAULT_TABLE_HANDLING, DEFAULT_USER_HANDLING)); public final static String[] DEFAULT_DEVELOPER_ROLES = ArrayUtil.merge(List.of(new String[]{"default-developer-roles"}, DEFAULT_CONTAINER_HANDLING, DEFAULT_DATABASE_HANDLING, DEFAULT_IDENTIFIER_HANDLING, DEFAULT_QUERY_HANDLING, - DEFAULT_TABLE_HANDLING, DEFAULT_USER_HANDLING, ESCALATED_CONTAINER_HANDLING, ESCALATED_DATABASE_HANDLING, - ESCALATED_IDENTIFIER_HANDLING, ESCALATED_QUERY_HANDLING, ESCALATED_TABLE_HANDLING)); + DEFAULT_TABLE_HANDLING, DEFAULT_USER_HANDLING, ESCALATED_USER_HANDLING, ESCALATED_CONTAINER_HANDLING, + ESCALATED_DATABASE_HANDLING, ESCALATED_IDENTIFIER_HANDLING, ESCALATED_QUERY_HANDLING, + ESCALATED_TABLE_HANDLING)); public final static List<GrantedAuthorityDto> AUTHORITY_DEFAULT_RESEARCHER_ROLES = Arrays.stream(DEFAULT_RESEARCHER_ROLES) .map(GrantedAuthorityDto::new) @@ -177,6 +181,16 @@ public abstract class BaseTest { .enabled(REALM_DBREPO_ENABLED) .build(); + public final static UUID ROLE_DEFAULT_RESEARCHER_ROLES_ID = UUID.fromString("c74cbbe7-3ab1-4472-9211-cc9045672682"); + public final static String ROLE_DEFAULT_RESEARCHER_ROLES_NAME = "default-researcher-roles"; + public final static UUID ROLE_DEFAULT_RESEARCHER_ROLES_REALM_ID = REALM_DBREPO_ID; + + public final static Role ROLE_DEFAULT_RESEARCHER_ROLES = Role.builder() + .id(ROLE_DEFAULT_RESEARCHER_ROLES_ID) + .name(ROLE_DEFAULT_RESEARCHER_ROLES_NAME) + .realmId(ROLE_DEFAULT_RESEARCHER_ROLES_REALM_ID) + .build(); + public final static String USER_BROKER_USERNAME = "guest"; public final static String USER_BROKER_PASSWORD = "guest"; diff --git a/dbrepo-proxy/dbrepo.conf b/dbrepo-proxy/dbrepo.conf index 66470c4e92ab97c3d4ae4fac78d4000d26f9e308..ac955002347f24a0c292ecc1eb4efe7f363db680 100644 --- a/dbrepo-proxy/dbrepo.conf +++ b/dbrepo-proxy/dbrepo.conf @@ -1,4 +1,4 @@ -client_max_body_size 100G; +client_max_body_size 2G; server { listen 80 default_server; diff --git a/dbrepo-query-service/rest-service/src/main/java/at/tuwien/endpoint/QueryEndpoint.java b/dbrepo-query-service/rest-service/src/main/java/at/tuwien/endpoint/QueryEndpoint.java index d80b31df72e06474407e86455fe757574e99162e..fb3b7209d06c8d797293521b75cb630d095cac32 100644 --- a/dbrepo-query-service/rest-service/src/main/java/at/tuwien/endpoint/QueryEndpoint.java +++ b/dbrepo-query-service/rest-service/src/main/java/at/tuwien/endpoint/QueryEndpoint.java @@ -18,7 +18,6 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.security.core.Authentication; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.*; @@ -34,12 +33,14 @@ public class QueryEndpoint { private final QueryService queryService; private final StoreService storeService; + private final AccessService accessService; private final DatabaseService databaseService; private final EndpointValidator endpointValidator; @Autowired - public QueryEndpoint(QueryService queryService, StoreService storeService, + public QueryEndpoint(QueryService queryService, StoreService storeService, AccessService accessService, DatabaseService databaseService, EndpointValidator endpointValidator) { + this.accessService = accessService; this.databaseService = databaseService; this.endpointValidator = endpointValidator; this.queryService = queryService; @@ -71,18 +72,8 @@ public class QueryEndpoint { } endpointValidator.validateForbiddenStatements(data); endpointValidator.validateDataParams(page, size, sortDirection, sortColumn); - final Database database = databaseService.find(containerId, databaseId); - if (!database.getIsPublic()) { - if (principal == null) { - log.error("Failed to execute private query: principal is null"); - throw new NotAllowedException("Failed to re-execute private query: principal is null"); - } - final Authentication authentication = (Authentication) principal; - if (authentication.getAuthorities().stream().noneMatch(a -> a.getAuthority().equals("execute-query"))) { - log.error("Failed to execute private query: role missing"); - throw new NotAllowedException("Failed to re-execute private query: role missing"); - } - } + /* has access */ + accessService.find(databaseId, principal.getName()); /* execute */ final QueryResultDto result = queryService.execute(containerId, databaseId, data, principal, page, size, sortDirection, sortColumn); diff --git a/dbrepo-query-service/rest-service/src/main/java/at/tuwien/endpoint/StoreEndpoint.java b/dbrepo-query-service/rest-service/src/main/java/at/tuwien/endpoint/StoreEndpoint.java index 7c0da7215e72955570bf276ce5352070c845f2d5..29830f33a7c7908c8acb9313b7eee4c6b3dcbfb9 100644 --- a/dbrepo-query-service/rest-service/src/main/java/at/tuwien/endpoint/StoreEndpoint.java +++ b/dbrepo-query-service/rest-service/src/main/java/at/tuwien/endpoint/StoreEndpoint.java @@ -43,18 +43,20 @@ public class StoreEndpoint { private final QueryMapper queryMapper; private final UserService userService; private final StoreService storeService; + private final AccessService accessService; private final IdentifierMapper identifierMapper; private final EndpointValidator endpointValidator; private final IdentifierService identifierService; @Autowired public StoreEndpoint(UserMapper userMapper, QueryMapper queryMapper, UserService userService, - StoreService storeService, IdentifierMapper identifierMapper, + StoreService storeService, AccessService accessService, IdentifierMapper identifierMapper, EndpointValidator endpointValidator, IdentifierService identifierService) { this.userMapper = userMapper; this.queryMapper = queryMapper; this.userService = userService; this.storeService = storeService; + this.accessService = accessService; this.identifierMapper = identifierMapper; this.endpointValidator = endpointValidator; this.identifierService = identifierService; @@ -109,7 +111,7 @@ public class StoreEndpoint { DatabaseConnectionException, TableMalformedException, UserNotFoundException, NotAllowedException { log.debug("endpoint list queries, containerId={}, databaseId={}, persisted={}, principal={}", containerId, databaseId, persisted, principal); - endpointValidator.validateOnlyAccess(containerId, databaseId, principal); + endpointValidator.validateOnlyAccessOrPublic(containerId, databaseId, principal); final List<Query> queries = storeService.findAll(containerId, databaseId, persisted, principal); final List<Identifier> identifiers = identifierService.findAll(); final List<User> users = userService.findAll(); @@ -176,7 +178,9 @@ public class StoreEndpoint { DatabaseConnectionException { log.debug("endpoint find query, containerId={}, databaseId={}, queryId={}, principal={}", containerId, databaseId, queryId, principal); - endpointValidator.validateOnlyAccess(containerId, databaseId, principal); + /* check */ + endpointValidator.validateOnlyAccessOrPublic(containerId, databaseId, principal); + /* find */ final Query query = storeService.findOne(containerId, databaseId, queryId, principal); final QueryDto dto = queryMapper.queryToQueryDto(query); final User creator = userService.findByUsername(query.getCreatedBy()); @@ -237,7 +241,8 @@ public class StoreEndpoint { QueryAlreadyPersistedException, NotAllowedException { log.debug("endpoint persist query, container, containerId={}, databaseId={}, queryId={}, principal={}", containerId, databaseId, queryId, principal); - endpointValidator.validateOnlyAccess(containerId, databaseId, principal); + /* check */ + endpointValidator.validateOnlyAccessOrPublic(containerId, databaseId, principal); final Query check = storeService.findOne(containerId, databaseId, queryId, principal); if (!check.getCreatedBy().equals(principal.getName())) { log.error("Cannot persist foreign query: created by {}", check.getCreatedBy()); @@ -247,6 +252,9 @@ public class StoreEndpoint { log.error("Failed to persist, is already persisted"); throw new QueryAlreadyPersistedException("Failed to persist"); } + /* has access */ + accessService.find(databaseId, principal.getName()); + /* persist */ final Query query = storeService.persist(containerId, databaseId, queryId, principal); final QueryDto dto = queryMapper.queryToQueryDto(query); final User creator = userService.findByUsername(query.getCreatedBy()); diff --git a/dbrepo-query-service/rest-service/src/main/java/at/tuwien/endpoint/TableDataEndpoint.java b/dbrepo-query-service/rest-service/src/main/java/at/tuwien/endpoint/TableDataEndpoint.java index b738a891b56004663ce8127f1ec8ebe3c9d5375b..f53f5d489728afd6018e3143c1b728d78b59ffc5 100644 --- a/dbrepo-query-service/rest-service/src/main/java/at/tuwien/endpoint/TableDataEndpoint.java +++ b/dbrepo-query-service/rest-service/src/main/java/at/tuwien/endpoint/TableDataEndpoint.java @@ -6,6 +6,8 @@ import at.tuwien.api.database.query.QueryResultDto; import at.tuwien.api.database.table.TableCsvDeleteDto; import at.tuwien.api.database.table.TableCsvDto; import at.tuwien.api.database.table.TableCsvUpdateDto; +import at.tuwien.entities.database.Database; +import at.tuwien.entities.user.User; import at.tuwien.exception.*; import at.tuwien.service.*; import at.tuwien.validation.EndpointValidator; @@ -31,11 +33,14 @@ import java.time.Instant; public class TableDataEndpoint { private final QueryService queryService; + private final DatabaseService databaseService; private final EndpointValidator endpointValidator; @Autowired - public TableDataEndpoint(QueryService queryService, EndpointValidator endpointValidator) { + public TableDataEndpoint(QueryService queryService, DatabaseService databaseService, + EndpointValidator endpointValidator) { this.queryService = queryService; + this.databaseService = databaseService; this.endpointValidator = endpointValidator; } @@ -51,9 +56,12 @@ public class TableDataEndpoint { @NotNull Principal principal) throws TableNotFoundException, DatabaseNotFoundException, TableMalformedException, ImageNotSupportedException, ContainerNotFoundException, DatabaseConnectionException, - UserNotFoundException { + UserNotFoundException, NotAllowedException { log.debug("endpoint insert data, containerId={}, databaseId={}, tableId={}, data={}, principal={}", containerId, databaseId, tableId, data, principal); + /* check */ + endpointValidator.validateOnlyWriteOwnOrWriteAllAccess(containerId, databaseId, tableId, principal); + /* insert */ queryService.insert(containerId, databaseId, tableId, data, principal); return ResponseEntity.accepted() .build(); @@ -72,9 +80,12 @@ public class TableDataEndpoint { @NotNull Principal principal) throws TableNotFoundException, DatabaseNotFoundException, TableMalformedException, ImageNotSupportedException, DatabaseConnectionException, QueryMalformedException, - UserNotFoundException { + UserNotFoundException, NotAllowedException { log.debug("endpoint update data, containerId={}, databaseId={}, tableId={}, data={}, principal={}", containerId, databaseId, tableId, data, principal); + /* check */ + endpointValidator.validateOnlyWriteOwnOrWriteAllAccess(containerId, databaseId, tableId, principal); + /* update */ queryService.update(containerId, databaseId, tableId, data, principal); return ResponseEntity.accepted() .build(); @@ -92,9 +103,12 @@ public class TableDataEndpoint { @NotNull Principal principal) throws TableNotFoundException, DatabaseNotFoundException, TableMalformedException, ImageNotSupportedException, ContainerNotFoundException, - DatabaseConnectionException, QueryMalformedException, UserNotFoundException { + DatabaseConnectionException, QueryMalformedException, UserNotFoundException, NotAllowedException { log.debug("endpoint delete data, containerId={}, databaseId={}, tableId={}, data={}, principal={}", containerId, databaseId, tableId, data, principal); + /* check */ + endpointValidator.validateOnlyWriteOwnOrWriteAllAccess(containerId, databaseId, tableId, principal); + /* delete */ queryService.delete(containerId, databaseId, tableId, data, principal); return ResponseEntity.accepted() .build(); @@ -112,9 +126,12 @@ public class TableDataEndpoint { @NotNull Principal principal) throws TableNotFoundException, DatabaseNotFoundException, TableMalformedException, ImageNotSupportedException, ContainerNotFoundException, DatabaseConnectionException, - QueryMalformedException, UserNotFoundException { + QueryMalformedException, UserNotFoundException, NotAllowedException { log.debug("endpoint insert data from csv, containerId={}, databaseId={}, tableId={}, data={}, principal={}", containerId, databaseId, tableId, data, principal); + /* check */ + endpointValidator.validateOnlyWriteOwnOrWriteAllAccess(containerId, databaseId, tableId, principal); + /* insert */ queryService.insert(containerId, databaseId, tableId, data, principal); return ResponseEntity.accepted() .build(); @@ -135,11 +152,18 @@ public class TableDataEndpoint { @RequestParam(required = false) String sortColumn) throws TableNotFoundException, DatabaseNotFoundException, DatabaseConnectionException, ImageNotSupportedException, TableMalformedException, PaginationException, ContainerNotFoundException, - QueryMalformedException, UserNotFoundException, SortException { + QueryMalformedException, UserNotFoundException, SortException, NotAllowedException { log.debug("endpoint find table data, containerId={}, databaseId={}, tableId={}, principal={}, timestamp={}, page={}, size={}, sortDirection={}, sortColumn={}", containerId, databaseId, tableId, principal, timestamp, page, size, sortDirection, sortColumn); /* check */ endpointValidator.validateDataParams(page, size, sortDirection, sortColumn); + endpointValidator.validateOnlyAccessOrPublic(containerId, databaseId, principal); + final Database database = databaseService.find(containerId, databaseId); + if (!database.getIsPublic() && !User.hasRole(principal, "view-table-data")) { + log.error("Failed to view table data: database with id {} is private and user has no authority", databaseId); + throw new NotAllowedException("Failed to view table data: database with id " + databaseId + " is private and user has no authority"); + } + /* find */ final QueryResultDto response = queryService.tableFindAll(containerId, databaseId, tableId, timestamp, page, size, principal); log.trace("find table data resulted in result {}", response); return ResponseEntity.ok() @@ -147,18 +171,26 @@ public class TableDataEndpoint { } @GetMapping("/count") + @Transactional(readOnly = true) @Timed(value = "data.all.count", description = "Time needed to get count of all data from a table") @Operation(summary = "Find data", security = @SecurityRequirement(name = "bearerAuth")) public ResponseEntity<Long> getCount(@NotNull @PathVariable("id") Long containerId, - @NotNull @PathVariable("databaseId") Long databaseId, - @NotNull @PathVariable("tableId") Long tableId, - @NotNull Principal principal, - @RequestParam(required = false) Instant timestamp) + @NotNull @PathVariable("databaseId") Long databaseId, + @NotNull @PathVariable("tableId") Long tableId, + @NotNull Principal principal, + @RequestParam(required = false) Instant timestamp) throws TableNotFoundException, DatabaseNotFoundException, DatabaseConnectionException, ImageNotSupportedException, TableMalformedException, ContainerNotFoundException, - QueryStoreException, QueryMalformedException, UserNotFoundException { + QueryStoreException, QueryMalformedException, UserNotFoundException, NotAllowedException { log.debug("endpoint find table data, containerId={}, databaseId={}, tableId={}, principal={}, timestamp={}", containerId, databaseId, tableId, principal, timestamp); + /* check */ + endpointValidator.validateOnlyAccessOrPublic(containerId, databaseId, principal); + final Database database = databaseService.find(containerId, databaseId); + if (!database.getIsPublic() && !User.hasRole(principal, "view-table-data")) { + log.error("Failed to view table data: database with id {} is private and user has no authority", databaseId); + throw new NotAllowedException("Failed to view table data: database with id " + databaseId + " is private and user has no authority"); + } /* find */ final Long count = queryService.tableCount(containerId, databaseId, tableId, timestamp, principal); log.debug("table data count is {} tuples", count); diff --git a/dbrepo-query-service/rest-service/src/main/java/at/tuwien/validation/EndpointValidator.java b/dbrepo-query-service/rest-service/src/main/java/at/tuwien/validation/EndpointValidator.java index c264c9f50c24c96b31b98120155d95ed8e8a7605..435752f3088041e4d45f9d5a64abca0c0fe165c2 100644 --- a/dbrepo-query-service/rest-service/src/main/java/at/tuwien/validation/EndpointValidator.java +++ b/dbrepo-query-service/rest-service/src/main/java/at/tuwien/validation/EndpointValidator.java @@ -3,13 +3,14 @@ package at.tuwien.validation; import at.tuwien.SortType; import at.tuwien.api.database.query.ExecuteStatementDto; import at.tuwien.config.QueryConfig; +import at.tuwien.entities.database.AccessType; import at.tuwien.entities.database.Database; import at.tuwien.entities.database.DatabaseAccess; -import at.tuwien.entities.user.User; +import at.tuwien.entities.database.table.Table; import at.tuwien.exception.*; import at.tuwien.service.AccessService; import at.tuwien.service.DatabaseService; -import at.tuwien.service.UserService; +import at.tuwien.service.TableService; import lombok.extern.log4j.Log4j2; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -25,16 +26,16 @@ import java.util.regex.Pattern; @Component public class EndpointValidator { - private final UserService userService; private final QueryConfig queryConfig; + private final TableService tableService; private final AccessService accessService; private final DatabaseService databaseService; @Autowired - public EndpointValidator(UserService userService, QueryConfig queryConfig, AccessService accessService, - DatabaseService databaseService) { - this.userService = userService; + public EndpointValidator(QueryConfig queryConfig, TableService tableService, + AccessService accessService, DatabaseService databaseService) { this.queryConfig = queryConfig; + this.tableService = tableService; this.accessService = accessService; this.databaseService = databaseService; } @@ -88,7 +89,8 @@ public class EndpointValidator { throw new QueryMalformedException("Query contains forbidden keyword(s): " + Arrays.toString(words.toArray())); } - public void validateOnlyAccess(Long containerId, Long databaseId, Principal principal) throws DatabaseNotFoundException, NotAllowedException { + public void validateOnlyAccessOrPublic(Long containerId, Long databaseId, Principal principal) + throws DatabaseNotFoundException, NotAllowedException { final Database database = databaseService.find(containerId, databaseId); if (database.getIsPublic()) { log.trace("database with id {} is public: no access needed", databaseId); @@ -104,4 +106,27 @@ public class EndpointValidator { log.trace("found access {}", access); } + public void validateOnlyWriteOwnOrWriteAllAccess(Long containerId, Long databaseId, Long tableId, + Principal principal) + throws DatabaseNotFoundException, TableNotFoundException, NotAllowedException { + final Table table = tableService.find(containerId, databaseId, tableId); + if (principal == null) { + log.error("Access not allowed: no authorization provided"); + throw new NotAllowedException("Access not allowed: no authorization provided"); + } + log.trace("principal is {}", principal); + final DatabaseAccess access = accessService.find(databaseId, principal.getName()); + log.trace("found access {}", access); + if (access.getType().equals(AccessType.WRITE_ALL)) { + log.debug("user {} has write-all access, skip.", principal.getName()); + return; + } + if (table.getOwner().getUsername().equals(principal.getName()) && access.getType().equals(AccessType.WRITE_OWN)) { + log.debug("user {} has write-own access to their own table, skip.", principal.getName()); + return; + } + log.error("Access not allowed: no write access for table with id {}", tableId); + throw new NotAllowedException("Access not allowed: no write access for table with id " + tableId); + } + } diff --git a/dbrepo-query-service/rest-service/src/test/java/at/tuwien/endpoint/TableDataEndpointUnitTest.java b/dbrepo-query-service/rest-service/src/test/java/at/tuwien/endpoint/TableDataEndpointUnitTest.java index c15a10770ba3dbcc5956e675cd9fb14f82f08cbc..c6820d5653dac61acc871c0734b97d01632bc981 100644 --- a/dbrepo-query-service/rest-service/src/test/java/at/tuwien/endpoint/TableDataEndpointUnitTest.java +++ b/dbrepo-query-service/rest-service/src/test/java/at/tuwien/endpoint/TableDataEndpointUnitTest.java @@ -287,7 +287,7 @@ public class TableDataEndpointUnitTest extends BaseUnitTest { @Test @WithAnonymousUser - public void getAll_publicAnonymousSizeNull_fails() { + public void getAll_publicAnonymousSizeNull_fails() { /* test */ assertThrows(PaginationException.class, () -> { @@ -309,7 +309,7 @@ public class TableDataEndpointUnitTest extends BaseUnitTest { @Test @WithAnonymousUser - public void getAll_publicAnonymousSizeNegative_fails() { + public void getAll_publicAnonymousSizeNegative_fails() { /* test */ assertThrows(PaginationException.class, () -> { @@ -331,13 +331,32 @@ public class TableDataEndpointUnitTest extends BaseUnitTest { @Test @WithAnonymousUser - public void getAll_privateAnonymous_fails() throws UserNotFoundException, TableNotFoundException, - QueryStoreException, SortException, TableMalformedException, NotAllowedException, - DatabaseConnectionException, QueryMalformedException, DatabaseNotFoundException, ImageNotSupportedException, - PaginationException, ContainerNotFoundException { + public void getAll_privateAnonymous_fails() { /* test */ - generic_getAll(CONTAINER_1_ID, DATABASE_1_ID, TABLE_1_ID, DATABASE_1, TABLE_1, null, null, null, null, null, null, null, null); + assertThrows(NotAllowedException.class, () -> { + generic_getAll(CONTAINER_1_ID, DATABASE_1_ID, TABLE_1_ID, DATABASE_1, TABLE_1, null, null, null, null, null, null, null, null); + }); + } + + @Test + @WithMockUser(username = USER_3_USERNAME, authorities = {}) + public void getAll_privateNoRole_fails() { + + /* test */ + assertThrows(NotAllowedException.class, () -> { + generic_getAll(CONTAINER_1_ID, DATABASE_1_ID, TABLE_1_ID, DATABASE_1, TABLE_1, USER_3_USERNAME, DATABASE_1_RESEARCHER_READ_ACCESS, USER_3_PRINCIPAL, null, null, null, null, null); + }); + } + + @Test + @WithMockUser(username = USER_3_USERNAME, authorities = {}) + public void getCount_privateNoRole_fails() { + + /* test */ + assertThrows(NotAllowedException.class, () -> { + generic_getCount(CONTAINER_1_ID, DATABASE_1_ID, TABLE_1_ID, DATABASE_1, TABLE_1, USER_3_USERNAME, DATABASE_1_RESEARCHER_READ_ACCESS, USER_3_PRINCIPAL, null); + }); } public static Stream<Arguments> getAll_succeeds_parameters() { diff --git a/dbrepo-query-service/services/src/main/java/at/tuwien/service/impl/QueryServiceImpl.java b/dbrepo-query-service/services/src/main/java/at/tuwien/service/impl/QueryServiceImpl.java index 1421c00dba97adf6817b32d46337eda1b39a881d..536aff8f662e9e135f699b167475d7190ccdd932 100644 --- a/dbrepo-query-service/services/src/main/java/at/tuwien/service/impl/QueryServiceImpl.java +++ b/dbrepo-query-service/services/src/main/java/at/tuwien/service/impl/QueryServiceImpl.java @@ -437,7 +437,6 @@ public class QueryServiceImpl extends HibernateConnector implements QueryService .executeUpdate(); } catch (SQLException | IOException e) { log.error("Failed to insert temporary table: {}", e.getMessage()); - log.trace("failed to insert temporary table {}", table); dataSource.close(); throw new TableMalformedException("Failed to insert temporary table", e); } finally { diff --git a/dbrepo-table-service/rest-service/src/test/java/at/tuwien/endpoint/TableColumnEndpointUnitTest.java b/dbrepo-table-service/rest-service/src/test/java/at/tuwien/endpoint/TableColumnEndpointUnitTest.java index d1dd3b32603f7e4ebf80c9eb20b5bf30ad61071c..3b9d220e7eff3f0362c860421f4b9f31c5686cb0 100644 --- a/dbrepo-table-service/rest-service/src/test/java/at/tuwien/endpoint/TableColumnEndpointUnitTest.java +++ b/dbrepo-table-service/rest-service/src/test/java/at/tuwien/endpoint/TableColumnEndpointUnitTest.java @@ -1,16 +1,11 @@ package at.tuwien.endpoint; import at.tuwien.BaseUnitTest; -import at.tuwien.api.database.table.TableBriefDto; -import at.tuwien.api.database.table.TableCreateDto; -import at.tuwien.api.database.table.TableDto; import at.tuwien.api.database.table.columns.ColumnDto; import at.tuwien.api.database.table.columns.concepts.ColumnSemanticsUpdateDto; import at.tuwien.config.IndexConfig; import at.tuwien.config.ReadyConfig; import at.tuwien.endpoints.TableColumnEndpoint; -import at.tuwien.endpoints.TableEndpoint; -import at.tuwien.entities.container.Container; import at.tuwien.entities.database.Database; import at.tuwien.entities.database.DatabaseAccess; import at.tuwien.entities.database.table.Table; @@ -19,12 +14,10 @@ import at.tuwien.exception.*; import at.tuwien.repository.elastic.TableColumnIdxRepository; import at.tuwien.repository.elastic.TableIdxRepository; import at.tuwien.service.AccessService; -import at.tuwien.service.ContainerService; import at.tuwien.service.DatabaseService; import at.tuwien.service.TableService; import com.rabbitmq.client.Channel; import lombok.extern.log4j.Log4j2; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; @@ -32,7 +25,6 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.test.context.support.WithAnonymousUser; @@ -40,7 +32,6 @@ import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.junit.jupiter.SpringExtension; import java.security.Principal; -import java.util.List; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; diff --git a/dbrepo-table-service/rest-service/src/test/java/at/tuwien/endpoint/TableEndpointIntegrationTest.java b/dbrepo-table-service/rest-service/src/test/java/at/tuwien/endpoint/TableEndpointIntegrationTest.java index 40f8b173051ab18a42ce9b516adb4799cd4fe9aa..2d473bd693228c5bf1b17f308523bc11dc1a0964 100644 --- a/dbrepo-table-service/rest-service/src/test/java/at/tuwien/endpoint/TableEndpointIntegrationTest.java +++ b/dbrepo-table-service/rest-service/src/test/java/at/tuwien/endpoint/TableEndpointIntegrationTest.java @@ -5,15 +5,11 @@ import at.tuwien.config.DockerConfig; import at.tuwien.config.H2Utils; import at.tuwien.config.IndexConfig; import at.tuwien.config.ReadyConfig; -import at.tuwien.endpoints.TableColumnEndpoint; import at.tuwien.endpoints.TableEndpoint; import at.tuwien.exception.*; import at.tuwien.repository.elastic.TableColumnIdxRepository; import at.tuwien.repository.elastic.TableIdxRepository; import at.tuwien.repository.jpa.*; -import at.tuwien.service.AccessService; -import at.tuwien.service.DatabaseService; -import at.tuwien.service.TableService; import com.rabbitmq.client.Channel; import lombok.extern.log4j.Log4j2; import org.junit.jupiter.api.AfterEach; @@ -28,9 +24,6 @@ import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.context.junit.jupiter.SpringExtension; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.Mockito.doThrow; - @Log4j2 @EnableAutoConfiguration(exclude = RabbitAutoConfiguration.class) @SpringBootTest diff --git a/dbrepo-table-service/rest-service/src/test/java/at/tuwien/mapper/TableMapperUnitTest.java b/dbrepo-table-service/rest-service/src/test/java/at/tuwien/mapper/TableMapperUnitTest.java new file mode 100644 index 0000000000000000000000000000000000000000..234ce6491ee4cd2468bd73ee7197f5e467d26b76 --- /dev/null +++ b/dbrepo-table-service/rest-service/src/test/java/at/tuwien/mapper/TableMapperUnitTest.java @@ -0,0 +1,71 @@ +package at.tuwien.mapper; + +import at.tuwien.BaseUnitTest; +import at.tuwien.api.database.AccessTypeDto; +import at.tuwien.config.IndexConfig; +import at.tuwien.config.ReadyConfig; +import at.tuwien.exception.NotAllowedException; +import at.tuwien.repository.elastic.TableColumnIdxRepository; +import at.tuwien.repository.elastic.TableIdxRepository; +import com.rabbitmq.client.Channel; +import lombok.extern.log4j.Log4j2; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@Log4j2 +@EnableAutoConfiguration(exclude = RabbitAutoConfiguration.class) +@SpringBootTest +@ExtendWith(SpringExtension.class) +public class TableMapperUnitTest extends BaseUnitTest { + + @MockBean + private ReadyConfig readyConfig; + + @MockBean + private Channel channel; + + @MockBean + private IndexConfig indexInitializer; + + @MockBean + private TableIdxRepository tableidxRepository; + + @MockBean + private TableColumnIdxRepository tableColumnidxRepository; + + @Autowired + private TableMapper tableMapper; + + public static Stream<Arguments> nameToInternalName_parameters() { + return Stream.of( + Arguments.arguments("dash_minus", "OE/NO-027", "oe_no_027"), + Arguments.arguments("percent", "OE%NO-027", "oe_no_027"), + Arguments.arguments("umlaut", "OE/NĂ–-027", "oe_no__027"), + Arguments.arguments("dot", "OE.NO-027", "oe_no_027") + ); + } + + @ParameterizedTest + @MethodSource("nameToInternalName_parameters") + public void nameToInternalName_succeeds(String name, String request, String compare) { + + /* test */ + final String response = tableMapper.nameToInternalName(request); + assertEquals(compare, response); + } + +} diff --git a/dbrepo-table-service/services/src/main/java/at/tuwien/mapper/TableMapper.java b/dbrepo-table-service/services/src/main/java/at/tuwien/mapper/TableMapper.java index d12f4c9e432f0cc081abca833905d6fcda59e266..c6f7a253ec7ebbea23ccbfc917fab48d90d9e81a 100644 --- a/dbrepo-table-service/services/src/main/java/at/tuwien/mapper/TableMapper.java +++ b/dbrepo-table-service/services/src/main/java/at/tuwien/mapper/TableMapper.java @@ -226,7 +226,8 @@ public interface TableMapper { final Pattern WHITESPACE = Pattern.compile("[\\s]"); String nowhitespace = WHITESPACE.matcher(data).replaceAll("_"); String normalized = Normalizer.normalize(nowhitespace, Normalizer.Form.NFD); - String slug = NONLATIN.matcher(normalized).replaceAll(""); + String slug = NONLATIN.matcher(normalized).replaceAll("_") + .replaceAll("-", "_"); final String name = slug.toLowerCase(Locale.ENGLISH); log.trace("mapped name {} to internal name {}", data, name); return name; diff --git a/dbrepo-table-service/services/src/main/java/at/tuwien/service/impl/TableServiceImpl.java b/dbrepo-table-service/services/src/main/java/at/tuwien/service/impl/TableServiceImpl.java index 8523187a3ae8c0e28689f7d9b9cd13ea985c0732..d60ac66548bb76d27fffc6875d44c5291fdc6204 100644 --- a/dbrepo-table-service/services/src/main/java/at/tuwien/service/impl/TableServiceImpl.java +++ b/dbrepo-table-service/services/src/main/java/at/tuwien/service/impl/TableServiceImpl.java @@ -170,6 +170,7 @@ public class TableServiceImpl extends HibernateConnector implements TableService tmp.setConstraints(null); final User creator = userService.findByUsername(principal.getName()); tmp.setCreator(creator); + tmp.setOwner(creator); /* save in metadata database */ final Table entity = tableRepository.save(tmp); entity.setColumns(createDto.getColumns() diff --git a/dbrepo-ui/api/database.service.js b/dbrepo-ui/api/database.service.js index 85756eaca0db53e7b963558abcb68af4a8a51de4..c01fc2ceae0933c0df03b49cdeccfc89d786b013 100644 --- a/dbrepo-ui/api/database.service.js +++ b/dbrepo-ui/api/database.service.js @@ -148,7 +148,7 @@ class DatabaseService { giveAccess (id, databaseId, username, type) { return new Promise((resolve, reject) => { - api.post(`/api/container/${id}/database/${databaseId}/access/${username}`, { username, type }, { headers: { Accept: 'application/json' } }) + api.post(`/api/container/${id}/database/${databaseId}/access`, { username, type }, { headers: { Accept: 'application/json' } }) .then(() => resolve()) .catch((error) => { const { code, message } = error diff --git a/dbrepo-ui/api/database.utils.js b/dbrepo-ui/api/database.utils.js new file mode 100644 index 0000000000000000000000000000000000000000..38adfddd8f54fa59b524ed65e956d3d662068b48 --- /dev/null +++ b/dbrepo-ui/api/database.utils.js @@ -0,0 +1,10 @@ +class DatabaseUtils { + isOwner (database, user) { + if (!database || !user) { + return false + } + return database.owner.id === user.id + } +} + +export default new DatabaseUtils() diff --git a/dbrepo-ui/api/query.service.js b/dbrepo-ui/api/query.service.js index a7900546e0cd3cf7a7c99db12c7a677296673bc3..db1bc16f3a4c9fd21b125a56182a0e1cc048595a 100644 --- a/dbrepo-ui/api/query.service.js +++ b/dbrepo-ui/api/query.service.js @@ -151,7 +151,7 @@ class QueryService { execute (id, databaseId, data, page, size) { return new Promise((resolve, reject) => { - api.post(`/api/container/${id}/database/${databaseId}?page=${page}&size=${size}`, data, { headers: { Accept: 'application/json' } }) + api.post(`/api/container/${id}/database/${databaseId}/query?page=${page}&size=${size}`, data, { headers: { Accept: 'application/json' } }) .then((response) => { const result = response.data console.debug('response result', result) diff --git a/dbrepo-ui/api/table.service.js b/dbrepo-ui/api/table.service.js index 930aae5a8ee79ef8f008860fbae3cd1f7de94cc0..8cef6aff3f3317efbae7093aeda5a5fea6cc5f0b 100644 --- a/dbrepo-ui/api/table.service.js +++ b/dbrepo-ui/api/table.service.js @@ -58,6 +58,19 @@ class TableService { }) } + importCsv (id, databaseId, tableId, data) { + return new Promise((resolve, reject) => { + api.post(`/api/container/${id}/database/${databaseId}/table/${tableId}/data/import`, data, { headers: { Accept: 'application/json' } }) + .then(() => resolve()) + .catch((error) => { + const { code, message } = error + console.error('Failed to import table data', error) + Vue.$toast.error(`[${code}] Failed to import table data: ${message}`) + reject(error) + }) + }) + } + data (id, databaseId, tableId, page, size, timestamp) { return new Promise((resolve, reject) => { api.get(`/api/container/${id}/database/${databaseId}/table/${tableId}/data?page=${page}&size=${size}×tamp=${timestamp}`, { headers: { Accept: 'application/json' } }) diff --git a/dbrepo-ui/api/user.service.js b/dbrepo-ui/api/user.service.js index e324cf68f3b351bca19cade29fe1aedd388d5f49..0fe5a464bd50a40b9af0a945dc86ec52eae2ecb7 100644 --- a/dbrepo-ui/api/user.service.js +++ b/dbrepo-ui/api/user.service.js @@ -63,9 +63,9 @@ class UserService { const { code, message, response } = error const { status } = response if (status === 417) { - Vue.$toast.error(`[${code}] This e-mail address is taken: ${message}`) + Vue.$toast.error('This e-mail address is already taken') } else if (status === 409) { - Vue.$toast.error(`[${code}] This username is taken: ${message}`) + Vue.$toast.error('This username is already taken') } else if (status === 428) { Vue.$toast.warning(`[${code}] Account was created: ${message}`) } else { diff --git a/dbrepo-ui/api/user.utils.js b/dbrepo-ui/api/user.utils.js new file mode 100644 index 0000000000000000000000000000000000000000..094ca82926fdc616395edc33110c547eb35f57d7 --- /dev/null +++ b/dbrepo-ui/api/user.utils.js @@ -0,0 +1,17 @@ +class UserUtils { + hasReadAccess (access) { + if (!access) { + return false + } + return access.type === 'read' || this.hasWriteAccess(access) + } + + hasWriteAccess (access) { + if (!access) { + return false + } + return access.type === 'write_own' || access.type === 'write_all' + } +} + +export default new UserUtils() diff --git a/dbrepo-ui/components/TableToolbar.vue b/dbrepo-ui/components/TableToolbar.vue index 80a54b2813071910ed0c0b353acedcb73c99f93e..c11713dd33b36508a043d6112125d6f5a00838e0 100644 --- a/dbrepo-ui/components/TableToolbar.vue +++ b/dbrepo-ui/components/TableToolbar.vue @@ -53,8 +53,9 @@ <script> import EditTuple from '@/components/dialogs/EditTuple' -import { isResearcher } from '@/utils' import TableService from '@/api/table.service' +import UserUtils from '@/api/user.utils' +import DatabaseUtils from '@/api/database.utils' export default { components: { @@ -101,43 +102,34 @@ export default { return this.$store.state.token }, canAddTuple () { - if (!this.roles) { - return false - } - if (!this.isDataTab) { + if (!this.roles || !this.isDataTab) { return false } - return this.roles.includes('insert-table-data') + return UserUtils.hasWriteAccess(this.access) && this.roles.includes('insert-table-data') }, canEditTuple () { - if (!this.roles) { - return false - } - if (!this.isDataTab) { + if (!this.roles || !this.isDataTab) { return false } - return this.roles.includes('insert-table-data') + return UserUtils.hasWriteAccess(this.access) && this.roles.includes('insert-table-data') }, canDeleteTuple () { - if (!this.roles) { - return false - } - if (!this.isDataTab) { + if (!this.roles || !this.isDataTab) { return false } - return this.roles.includes('delete-table-data') + return UserUtils.hasWriteAccess(this.access) && this.roles.includes('delete-table-data') }, canExecuteQuery () { if (!this.roles) { return false } - return this.roles.includes('execute-query') + return UserUtils.hasReadAccess(this.access) && this.roles.includes('execute-query') }, canCreateView () { if (!this.user) { return false } - return this.roles.includes('create-database-view') + return DatabaseUtils.isOwner(this.database, this.user) && this.roles.includes('create-database-view') }, canViewTableData () { /* view when database is public or when private: 1) view-table-data role present 2) access is at least read */ @@ -169,12 +161,6 @@ export default { return false } return this.database.creator.username === this.user.username - }, - isResearcher () { - return isResearcher(this.user) - }, - databaseTooltip () { - return this.database.is_public ? 'Public' : 'Private' } }, methods: { diff --git a/dbrepo-ui/components/dialogs/EditAccess.vue b/dbrepo-ui/components/dialogs/EditAccess.vue index 1ba70270745992a29236b81d58b561d1cfc140b0..6048006744ea2c0b19a1ffa55e8669fedf7cfa87 100644 --- a/dbrepo-ui/components/dialogs/EditAccess.vue +++ b/dbrepo-ui/components/dialogs/EditAccess.vue @@ -26,6 +26,7 @@ v-if="!isModification" v-model="modify.username" :items="eligibleUsers" + :disabled="loadingUsers" :loading="loadingUsers" :rules="[v => !!v || $t('Required')]" required @@ -75,6 +76,7 @@ <script> import DatabaseService from '@/api/database.service' import UserService from '@/api/user.service' + export default { props: { username: { @@ -199,9 +201,9 @@ export default { }, revokeAccess () { this.loading = true - DatabaseService.revokeAccess(this.$route.params.container_id, this.$route.params.database_id, this.username) + DatabaseService.revokeAccess(this.$route.params.container_id, this.$route.params.database_id, this.modify.username) .then(() => { - this.$toast.success(`Successfully revoked access of ${this.username}`) + this.$toast.success(`Successfully revoked access of ${this.modify.username}`) this.$emit('close-dialog', { success: true }) }) .finally(() => { @@ -210,9 +212,9 @@ export default { }, modifyAccess () { this.loading = true - DatabaseService.modifyAccess(this.$route.params.container_id, this.$route.params.database_id, this.username, this.modify.type) + DatabaseService.modifyAccess(this.$route.params.container_id, this.$route.params.database_id, this.modify.username, this.modify.type) .then(() => { - this.$toast.success('Successfully modified access') + this.$toast.success(`Successfully modified access of ${this.modify.username}`) this.$emit('close-dialog', { success: true }) }) .finally(() => { @@ -220,11 +222,10 @@ export default { }) }, giveAccess () { - const username = this.modify.username this.loading = true - DatabaseService.giveAccess(this.$route.params.container_id, this.$route.params.database_id, this.username, this.modify.type) + DatabaseService.giveAccess(this.$route.params.container_id, this.$route.params.database_id, this.modify.username, this.modify.type) .then(() => { - this.$toast.success(`Successfully gave ${username} access`) + this.$toast.success(`Successfully gave ${this.modify.username} access`) this.$emit('close-dialog', { success: true }) }) .finally(() => { @@ -238,7 +239,7 @@ export default { this.users = users.filter(u => u.username !== this.database.creator.username) }) .finally(() => { - this.loading = false + this.loadingUsers = false }) }, init () { diff --git a/dbrepo-ui/components/dialogs/Persist.vue b/dbrepo-ui/components/dialogs/Persist.vue index 704a8b342cb8e5914beea4484d87a8d9f8841689..bcc3068d66cdf52477d57b2ff0360a06c6ee42e4 100644 --- a/dbrepo-ui/components/dialogs/Persist.vue +++ b/dbrepo-ui/components/dialogs/Persist.vue @@ -4,10 +4,10 @@ <v-card-title v-text="title" /> <v-card-text> <v-alert - v-if="isSubset" + v-if="identifier.visibility === 'self'" border="left" color="info"> - Choose an expressive subset title and describe what it produces. + Private datasets significantly impact the reusability of your research. </v-alert> <v-form v-model="formValid" autocomplete="off"> <v-row dense> @@ -20,6 +20,10 @@ :label="`${prefix} title *`" :rules="[v => !!v || $t('Required')]" required /> + </v-col> + </v-row> + <v-row dense> + <v-col> <v-textarea id="description" v-model="identifier.description" @@ -28,6 +32,20 @@ :label="`${prefix} description *`" /> </v-col> </v-row> + <v-row v-if="isSubset" dense> + <v-col> + <v-select + v-model="identifier.visibility" + :items="visibilities" + item-text="name" + item-value="value" + label="Visibility *" + :hint="visibilityHint" + persistent-hint + :rules="[ v => !!v || $t('Required') ]" + required /> + </v-col> + </v-row> <v-row dense> <v-col> <v-text-field @@ -215,12 +233,17 @@ export default { loading: false, error: false, // XXX: `error` is never changed licenses: [], + visibilities: [ + { name: 'Public', value: 'everyone' }, + { name: 'Private', value: 'self' } + ], identifier: { cid: parseInt(this.$route.params.container_id), dbid: parseInt(this.$route.params.database_id), qid: parseInt(this.$route.params.query_id), title: null, description: null, + visibility: 'everyone', publisher: this.$config.defaultPublisher, publication_year: formatYearUTC(Date.now()), publication_month: formatMonthUTC(Date.now()), @@ -326,6 +349,12 @@ export default { } return (title + ' identifier') }, + visibilityHint () { + if (this.identifier.visibility === 'public') { + return 'The result set will be open access (world-readable)' + } + return 'The result set will be visible only to you' + }, prefix () { if (this.isSubset) { return 'Subset' diff --git a/dbrepo-ui/components/query/Results.vue b/dbrepo-ui/components/query/Results.vue index 1f30ee22566bf88f9184576f86ffac03c42c8967..74a06f23e8b17e70409aac780278adcc3c100425 100644 --- a/dbrepo-ui/components/query/Results.vue +++ b/dbrepo-ui/components/query/Results.vue @@ -70,7 +70,7 @@ export default { } this.loading++ if (this.type === 'query') { - QueryService.reExecuteQuery(this.$route.params.container_id, this.$route.params.database_id, this.resultId, 0, this.options.itemsPerPage) + QueryService.reExecuteQuery(this.$route.params.container_id, this.$route.params.database_id, id, 0, this.options.itemsPerPage) .then((result) => { this.mapResults(result) this.id = id @@ -79,7 +79,7 @@ export default { this.loading-- }) } else { - QueryService.reExecuteView(this.$route.params.container_id, this.$route.params.database_id, this.resultId, 0, this.options.itemsPerPage) + QueryService.reExecuteView(this.$route.params.container_id, this.$route.params.database_id, id, 0, this.options.itemsPerPage) .then((result) => { this.mapResults(result) this.id = id @@ -95,7 +95,7 @@ export default { } this.loading++ if (this.type === 'query') { - QueryService.reExecuteQueryCount(this.$route.params.container_id, this.$route.params.database_id, this.resultId) + QueryService.reExecuteQueryCount(this.$route.params.container_id, this.$route.params.database_id, id) .then((count) => { this.total = count }) @@ -103,7 +103,7 @@ export default { this.loading-- }) } else { - QueryService.reExecuteViewCount(this.$route.params.container_id, this.$route.params.database_id, this.resultId) + QueryService.reExecuteViewCount(this.$route.params.container_id, this.$route.params.database_id, id) .then((count) => { this.total = count }) diff --git a/dbrepo-ui/pages/container/_container_id/database/_database_id/query/_query_id/index.vue b/dbrepo-ui/pages/container/_container_id/database/_database_id/query/_query_id/index.vue index e7cac6601311c7bf3711b7e26efef2c6333f615e..dcbe33966032f5b0c9bbfe9062d54ef259da85a9 100644 --- a/dbrepo-ui/pages/container/_container_id/database/_database_id/query/_query_id/index.vue +++ b/dbrepo-ui/pages/container/_container_id/database/_database_id/query/_query_id/index.vue @@ -11,7 +11,7 @@ </v-toolbar-title> <v-spacer /> <v-toolbar-title> - <v-btn v-if="!loadingQuery && !query.is_persisted && canWrite" :loading="loadingSave" class="mb-1" @click.stop="save"> + <v-btn v-if="canPersistQuery" :loading="loadingSave" class="mb-1" @click.stop="save"> <v-icon left>mdi-content-save-outline</v-icon> Save </v-btn> <v-btn v-if="query.is_persisted && !query.identifier && canWrite" class="mb-1 ml-2" color="primary" :disabled="!executionUTC" @click.stop="openDialog()"> @@ -104,7 +104,7 @@ </v-card-title> <v-card-text> <v-alert - v-if="!loadingQuery && !query.is_persisted && canWrite" + v-if="canPersistQuery" border="left" color="info"> Query is not yet saved in the query store, <a @click="save">save</a> it to view it later. @@ -225,6 +225,7 @@ import Banner from '@/components/identifier/Banner' import DownloadButton from '@/components/identifier/DownloadButton' import { formatTimestampUTCLabel, formatDateUTC } from '@/utils' import QueryService from '@/api/query.service' +import UserUtils from '@/api/user.utils' export default { name: 'QueryShow', @@ -304,23 +305,11 @@ export default { } return this.query.result_hash }, - config () { - if (this.token === null) { - return { - headers: {}, - progress: false - } - } - return { - headers: { Authorization: `Bearer ${this.token}` }, - progress: false - } - }, - silentConfig () { - return { - headers: this.config.headers, - progress: false + canPersistQuery () { + if (!this.query || this.query.is_persisted) { + return false } + return UserUtils.hasReadAccess(this.access) }, publisher () { if (this.database.publisher === null) { @@ -350,10 +339,7 @@ export default { if (!this.access || !this.access.type) { return false } - if (this.access.type === 'write_own' || this.access.type === 'write_all') { - return true - } - return false + return this.access.type === 'write_own' || this.access.type === 'write_all' }, publication () { if (this.query.identifier.publication_year && !this.query.identifier.publication_month && !this.query.identifier.publication_day) { diff --git a/dbrepo-ui/pages/container/_container_id/database/_database_id/query/create.vue b/dbrepo-ui/pages/container/_container_id/database/_database_id/query/create.vue index dd5c10a9e73cd3154a62dd00304457c9e333d5d9..f6864c8b031ed621d25b20b6b83cc6e6fc295680 100644 --- a/dbrepo-ui/pages/container/_container_id/database/_database_id/query/create.vue +++ b/dbrepo-ui/pages/container/_container_id/database/_database_id/query/create.vue @@ -6,6 +6,8 @@ </template> <script> +import UserUtils from '@/api/user.utils' + export default { data () { return { @@ -31,11 +33,14 @@ export default { roles () { return this.$store.state.roles }, + access () { + return this.$store.state.access + }, canExecuteQuery () { - if (!this.roles) { + if (!this.roles || !this.access) { return false } - return this.roles.includes('execute-query') + return UserUtils.hasReadAccess(this.access) && this.roles.includes('execute-query') } } } diff --git a/dbrepo-ui/pages/container/_container_id/database/_database_id/table/_table_id/import.vue b/dbrepo-ui/pages/container/_container_id/database/_database_id/table/_table_id/import.vue index b0cbfc7b4520127c537cdff9e2ca207760b368b9..d2caacbd0d4240577285a4f82f76e9db4e88e703 100644 --- a/dbrepo-ui/pages/container/_container_id/database/_database_id/table/_table_id/import.vue +++ b/dbrepo-ui/pages/container/_container_id/database/_database_id/table/_table_id/import.vue @@ -83,7 +83,7 @@ <v-file-input v-model="fileModel" accept=".csv,.tsv" - hint="max. 100 GB file size" + hint="max. 2GB file size" persistent-hint show-size label="CSV/TSV File" /> diff --git a/dbrepo-ui/pages/container/_container_id/database/_database_id/table/_table_id/info.vue b/dbrepo-ui/pages/container/_container_id/database/_database_id/table/_table_id/info.vue index 8f7e4e6b53c0a7dd5594bed0fd4db8645beb72ba..171c0e219e5d4d5e861ff06a8ed8e57c4faf2b4d 100644 --- a/dbrepo-ui/pages/container/_container_id/database/_database_id/table/_table_id/info.vue +++ b/dbrepo-ui/pages/container/_container_id/database/_database_id/table/_table_id/info.vue @@ -25,10 +25,10 @@ <v-skeleton-loader v-if="!table" type="text" class="skeleton-medium" /> </v-list-item-content> <v-list-item-title class="mt-2"> - Table Creator + Table Owner </v-list-item-title> <v-list-item-content> - <span v-if="table && table.creator">{{ formatCreator(table.creator) }} <span v-if="is_owner(table)" style="flex:none;"> (you)</span></span> + <span v-if="table && table.creator">{{ formatCreator(table.owner) }} <span v-if="is_owner(table)" style="flex:none;"> (you)</span></span> <v-skeleton-loader v-if="!table" type="text" class="skeleton-medium" /> </v-list-item-content> <v-list-item-title v-if="table && table.created" class="mt-2"> @@ -210,7 +210,7 @@ export default { if (!this.user) { return false } - return table.creator.username === this.user.username + return table.owner.username === this.user.username }, consumerDetails () { if (!this.table) { diff --git a/dbrepo-ui/pages/container/_container_id/database/_database_id/table/import.vue b/dbrepo-ui/pages/container/_container_id/database/_database_id/table/import.vue index a915657c3c5484e1cb150f35a200a0fdae6511cf..7f43332873c371001334fe57dc0eb4575367f803 100644 --- a/dbrepo-ui/pages/container/_container_id/database/_database_id/table/import.vue +++ b/dbrepo-ui/pages/container/_container_id/database/_database_id/table/import.vue @@ -139,7 +139,7 @@ <v-file-input v-model="fileModel" accept=".csv,.tsv" - hint="max. 100 GB file size" + hint="max. 2 GB file size" persistent-hint show-size label="File Upload (.csv/.tsv)" /> @@ -160,7 +160,7 @@ <v-btn class="mb-1" :disabled="!fileModel" - :loading="loading" + :loading="loadingUpload || loadingAnalyse" color="primary" type="submit" @click="uploadAndAnalyse"> @@ -174,7 +174,7 @@ Table Schema </v-stepper-step> <v-stepper-content step="4"> - <TableSchema :back="true" :error="error" :loading="loading" :columns="tableCreate.columns" @close="schemaClose" /> + <TableSchema :back="true" :error="error" :loading="loadingImage" :columns="tableCreate.columns" @close="schemaClose" /> </v-stepper-content> <v-stepper-step :complete="step > 5" @@ -256,6 +256,9 @@ export default { skip_lines: 1 }, loading: false, + loadingUpload: false, + loadingAnalyse: false, + loadingImage: false, url: null, columns: [], newTableId: 42 // FIXME ??? @@ -316,7 +319,7 @@ export default { this.$refs.form.validate() }, upload () { - this.loading = true + this.loadingUpload = true return new Promise((resolve, reject) => { MiddlewareService.upload(this.fileModel) .then((file) => { @@ -327,12 +330,12 @@ export default { reject(error) }) .finally(() => { - this.loading = false + this.loadingUpload = false }) }) }, analyse () { - this.loading = true + this.loadingAnalyse = true AnalyseService.determineDataTypes(`/tmp/${this.file.filename}`) .then((analysis) => { const { columns } = analysis @@ -348,10 +351,9 @@ export default { }) this.tableImport.location = `/tmp/${this.file.filename}` this.step = 4 - this.loading = false }) .finally(() => { - this.loading = false + this.loadingAnalyse = false }) }, listTables () { @@ -371,18 +373,24 @@ export default { return } this.validStep4 = true - this.step = 5 this.createTable() }, setOthers (column) { column.null_allowed = false column.unique = true }, - async loadDateFormats () { - this.loading = true - const res = await ContainerService.findOne(this.$route.params.container_id) - this.dateFormats = await ContainerService.findImage(res.image.id).date_formats - this.loading = true + loadDateFormats () { + this.loadingImage = true + ContainerService.findOne(this.$route.params.container_id) + .then((container) => { + ContainerService.findImage(container.image.id) + .then((image) => { + this.dateFormats = image.date_formats + }) + }) + .finally(() => { + this.loadingImage = true + }) }, createTable () { /* make enum values to array */ diff --git a/dbrepo-ui/pages/signup.vue b/dbrepo-ui/pages/signup.vue index f684bf48dc46e9c5449f1528bf0163b3147f1ccb..469907ca6a276796460295e3ec5dcf06ceefd0ac 100644 --- a/dbrepo-ui/pages/signup.vue +++ b/dbrepo-ui/pages/signup.vue @@ -36,7 +36,8 @@ required name="username" :rules="[v => !!v || $t('Required'), - v => /^[a-z0-9]{3,}$/.test(v) || $t('Only lowercase letters, min. 3 length')]" + v => /^[a-z0-9]{3,}$/.test(v) || $t('Only lowercase letters, min. 3 length'), + v => !usernames.includes(v) || $t('This username is already taken')]" hint="e.g. mmustermann" label="Username *" /> </v-col> @@ -109,6 +110,8 @@ export default { data () { return { loading: false, + loadingUsers: false, + usernames: [], error: false, // XXX: `error` is never changed valid: false, password2: null, @@ -132,6 +135,9 @@ export default { return this.$config.mailVerify } }, + mounted () { + this.loadUsers() + }, methods: { submit () { this.$refs.form.validate() @@ -147,6 +153,16 @@ export default { .catch(() => { this.loading = false }) + }, + loadUsers () { + this.loadingUsers = true + UserService.findAll() + .then((users) => { + this.usernames = users.map(u => u.username) + }) + .finally(() => { + this.loadingUsers = false + }) } } } diff --git a/dbrepo-user-service/pom.xml b/dbrepo-user-service/pom.xml index 8a5225c3fb3ded3fb1a5d5e42b60daca892ddce5..471c9323648323786875b697a08aa3402f29e778 100644 --- a/dbrepo-user-service/pom.xml +++ b/dbrepo-user-service/pom.xml @@ -226,6 +226,7 @@ <exclude>at/tuwien/handlers/**/*</exclude> <exclude>at/tuwien/exception/**/*</exclude> <exclude>at/tuwien/config/**/*</exclude> + <exclude>at/tuwien/auth/**/*</exclude> <exclude>**/FdaUserServiceApplication.class</exclude> </excludes> </configuration> diff --git a/dbrepo-user-service/rest-service/src/main/java/at/tuwien/endpoint/UserEndpoint.java b/dbrepo-user-service/rest-service/src/main/java/at/tuwien/endpoint/UserEndpoint.java index 08e2b0d7a36b8b02a9096b8f3c875049c4757cfc..3bdd01e898403c563ae0928004ea34c248c441ea 100644 --- a/dbrepo-user-service/rest-service/src/main/java/at/tuwien/endpoint/UserEndpoint.java +++ b/dbrepo-user-service/rest-service/src/main/java/at/tuwien/endpoint/UserEndpoint.java @@ -67,15 +67,21 @@ public class UserEndpoint { @PostMapping @Transactional + @PreAuthorize("!isAuthenticated()") @Timed(value = "user.create", description = "Time needed to create a user in the metadata database") @Operation(summary = "Create user") public ResponseEntity<UserBriefDto> create(@NotNull @Valid @RequestBody SignupRequestDto data) throws UserNotFoundException, RemoteUnavailableException, RealmNotFoundException, - UserAlreadyExistsException, RoleNotFoundException { + UserAlreadyExistsException, RoleNotFoundException, UserEmailAlreadyExistsException { log.debug("endpoint create a user, data={}", data); + /* check */ final Realm realm = realmService.find("dbrepo"); final Role role = roleService.find(authenticationConfig.getDefaultRole()); - final UserBriefDto dto = userMapper.userToUserBriefDto(userService.create(data, realm, role)); + userService.validateUsernameNotExists(data.getUsername()); + userService.validateEmailNotExists(data.getEmail()); + /* create */ + final User user = userService.create(data, realm, role); + final UserBriefDto dto = userMapper.userToUserBriefDto(user); log.trace("create user resulted in dto {}", dto); return ResponseEntity.status(HttpStatus.CREATED) .body(dto); @@ -83,17 +89,27 @@ public class UserEndpoint { @GetMapping("/{id}") @Transactional - @PreAuthorize("isAuthenticated()") + @PreAuthorize("isAuthenticated() or hasAuthority('find-user')") @Timed(value = "user.info", description = "Time needed to get information of a user in the metadata database") @Operation(summary = "Get a user info", security = @SecurityRequirement(name = "bearerAuth")) public ResponseEntity<UserDto> find(@NotNull @PathVariable("id") String id, @NotNull Principal principal) - throws UserNotFoundException { + throws UserNotFoundException, NotAllowedException { log.debug("endpoint find a user, id={}, principal={}", id, principal); - final UserDto dto = userMapper.userToUserDto(userService.find(UUID.fromString(id))); - log.trace("find user resulted in dto {}", dto); - return ResponseEntity.ok() - .body(dto); + /* check */ + final User user = userService.find(UUID.fromString(id)); + final UserDto dto = userMapper.userToUserDto(user); + if (user.getUsername().equals(principal.getName())) { + log.trace("find user resulted in dto {}", dto); + return ResponseEntity.ok() + .body(dto); + } else if (User.hasRole(principal, "find-user")) { + log.trace("find user resulted in dto {}", dto); + return ResponseEntity.ok() + .body(dto); + } + log.error("Failed to find user: no authority and not the current logged-in user"); + throw new NotAllowedException("Failed to find user: no authority"); } @PutMapping("/{id}") @@ -106,12 +122,14 @@ public class UserEndpoint { @NotNull Principal principal) throws UserNotFoundException, ForeignUserException, UserAttributeNotFoundException { log.debug("endpoint modify a user, id={}, data={}, principal={}", id, data, principal); + /* check */ final User user = userService.find(UUID.fromString(id)); if (!user.equalsPrincipal(principal)) { log.error("Failed to modify user: attempting to modify other user"); throw new ForeignUserException("Failed to modify user: attempting to modify other user"); } - final UserDto dto = userMapper.userToUserDto(userService.modify(user.getId(), data, principal)); + /* modify */ + final UserDto dto = userMapper.userToUserDto(userService.modify(user.getId(), data)); log.trace("modify user resulted in dto {}", dto); return ResponseEntity.status(HttpStatus.ACCEPTED) .body(dto); @@ -127,12 +145,14 @@ public class UserEndpoint { @NotNull Principal principal) throws UserNotFoundException, ForeignUserException, UserAttributeNotFoundException { log.debug("endpoint modify a user theme, id={}, data={}, principal={}", id, data, principal); + /* check */ final User user = userService.find(UUID.fromString(id)); if (!user.equalsPrincipal(principal)) { log.error("Failed to modify user: attempting to modify other user"); throw new ForeignUserException("Failed to modify user: attempting to modify other user"); } - final UserDto dto = userMapper.userToUserDto(userService.toggleTheme(user.getId(), data, principal)); + /* modify theme */ + final UserDto dto = userMapper.userToUserDto(userService.toggleTheme(user.getId(), data)); log.trace("modify user theme resulted in dto {}", dto); return ResponseEntity.accepted() .body(dto); @@ -148,12 +168,14 @@ public class UserEndpoint { @NotNull Principal principal) throws UserNotFoundException, ForeignUserException { log.debug("endpoint modify a user password, id={}, data={}, principal={}", id, data, principal); + /* check */ final User user = userService.find(UUID.fromString(id)); if (!user.equalsPrincipal(principal)) { log.error("Failed to modify user: attempting to modify other user"); throw new ForeignUserException("Failed to modify user: attempting to modify other user"); } - final UserDto dto = userMapper.userToUserDto(userService.updatePassword(user.getId(), data, principal)); + /* modify password */ + final UserDto dto = userMapper.userToUserDto(userService.updatePassword(user.getId(), data)); log.trace("updated user password resulted in dto {}", dto); return ResponseEntity.accepted() .body(dto); diff --git a/dbrepo-user-service/rest-service/src/main/java/at/tuwien/handlers/ApiExceptionHandler.java b/dbrepo-user-service/rest-service/src/main/java/at/tuwien/handlers/ApiExceptionHandler.java index b5d0c1658bf704de3856a2321cdf4d2057412c66..c4f7c69f64bfcc65bcedf508f16cccb09ab4edf4 100644 --- a/dbrepo-user-service/rest-service/src/main/java/at/tuwien/handlers/ApiExceptionHandler.java +++ b/dbrepo-user-service/rest-service/src/main/java/at/tuwien/handlers/ApiExceptionHandler.java @@ -15,90 +15,13 @@ import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExcep @ControllerAdvice public class ApiExceptionHandler extends ResponseEntityExceptionHandler { - @ResponseStatus(HttpStatus.NOT_ACCEPTABLE) - @ExceptionHandler(AmqpException.class) - public ResponseEntity<ApiErrorDto> handle(AmqpException e, WebRequest request) { - final ApiErrorDto response = ApiErrorDto.builder() - .status(HttpStatus.NOT_ACCEPTABLE) - .message(e.getLocalizedMessage()) - .code("error.query.amqp") - .build(); - return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus()); - } - - @ResponseStatus(HttpStatus.EXPECTATION_FAILED) - @ExceptionHandler(ColumnParseException.class) - public ResponseEntity<ApiErrorDto> handle(ColumnParseException e, WebRequest request) { - final ApiErrorDto response = ApiErrorDto.builder() - .status(HttpStatus.EXPECTATION_FAILED) - .message(e.getLocalizedMessage()) - .code("error.query.columnparse") - .build(); - return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus()); - } - - @ResponseStatus(HttpStatus.NOT_FOUND) - @ExceptionHandler(ContainerNotFoundException.class) - public ResponseEntity<ApiErrorDto> handle(ContainerNotFoundException e, WebRequest request) { - final ApiErrorDto response = ApiErrorDto.builder() - .status(HttpStatus.NOT_FOUND) - .message(e.getLocalizedMessage()) - .code("error.query.containernotfound") - .build(); - return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus()); - } - @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED) - @ExceptionHandler(DatabaseConnectionException.class) - public ResponseEntity<ApiErrorDto> handle(DatabaseConnectionException e, WebRequest request) { + @ExceptionHandler(ForeignUserException.class) + public ResponseEntity<ApiErrorDto> handle(ForeignUserException e, WebRequest request) { final ApiErrorDto response = ApiErrorDto.builder() .status(HttpStatus.METHOD_NOT_ALLOWED) .message(e.getLocalizedMessage()) - .code("error.query.databaseconnection") - .build(); - return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus()); - } - - @ResponseStatus(HttpStatus.NOT_FOUND) - @ExceptionHandler(DatabaseNotFoundException.class) - public ResponseEntity<ApiErrorDto> handle(DatabaseNotFoundException e, WebRequest request) { - final ApiErrorDto response = ApiErrorDto.builder() - .status(HttpStatus.NOT_FOUND) - .message(e.getLocalizedMessage()) - .code("error.query.databasenotfound") - .build(); - return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus()); - } - - @ResponseStatus(HttpStatus.BAD_REQUEST) - @ExceptionHandler(FileStorageException.class) - public ResponseEntity<ApiErrorDto> handle(FileStorageException e, WebRequest request) { - final ApiErrorDto response = ApiErrorDto.builder() - .status(HttpStatus.BAD_REQUEST) - .message(e.getLocalizedMessage()) - .code("error.query.filestore") - .build(); - return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus()); - } - - @ResponseStatus(HttpStatus.NOT_FOUND) - @ExceptionHandler(IdentifierNotFoundException.class) - public ResponseEntity<ApiErrorDto> handle(IdentifierNotFoundException e, WebRequest request) { - final ApiErrorDto response = ApiErrorDto.builder() - .status(HttpStatus.NOT_FOUND) - .message(e.getLocalizedMessage()) - .code("error.query.identifiernotfound") - .build(); - return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus()); - } - - @ResponseStatus(HttpStatus.CONFLICT) - @ExceptionHandler(ImageNotSupportedException.class) - public ResponseEntity<ApiErrorDto> handle(ImageNotSupportedException e, WebRequest request) { - final ApiErrorDto response = ApiErrorDto.builder() - .status(HttpStatus.CONFLICT) - .message(e.getLocalizedMessage()) - .code("error.query.imagenotsupported") + .code("error.user.foreignpermission") .build(); return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus()); } @@ -109,139 +32,84 @@ public class ApiExceptionHandler extends ResponseEntityExceptionHandler { final ApiErrorDto response = ApiErrorDto.builder() .status(HttpStatus.METHOD_NOT_ALLOWED) .message(e.getLocalizedMessage()) - .code("error.query.permission") - .build(); - return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus()); - } - - @ResponseStatus(HttpStatus.BAD_REQUEST) - @ExceptionHandler(PaginationException.class) - public ResponseEntity<ApiErrorDto> handle(PaginationException e, WebRequest request) { - final ApiErrorDto response = ApiErrorDto.builder() - .status(HttpStatus.BAD_REQUEST) - .message(e.getLocalizedMessage()) - .code("error.query.pagination") - .build(); - return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus()); - } - - @ResponseStatus(HttpStatus.CONFLICT) - @ExceptionHandler(QueryAlreadyPersistedException.class) - public ResponseEntity<ApiErrorDto> handle(QueryAlreadyPersistedException e, WebRequest request) { - final ApiErrorDto response = ApiErrorDto.builder() - .status(HttpStatus.CONFLICT) - .message(e.getLocalizedMessage()) - .code("error.query.alreadypersisted") - .build(); - return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus()); - } - - @ResponseStatus(HttpStatus.BAD_REQUEST) - @ExceptionHandler(QueryMalformedException.class) - public ResponseEntity<ApiErrorDto> handle(QueryMalformedException e, WebRequest request) { - final ApiErrorDto response = ApiErrorDto.builder() - .status(HttpStatus.BAD_REQUEST) - .message(e.getLocalizedMessage()) - .code("error.query.malformed") + .code("error.user.permission") .build(); return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus()); } @ResponseStatus(HttpStatus.NOT_FOUND) - @ExceptionHandler(QueryNotFoundException.class) - public ResponseEntity<ApiErrorDto> handle(QueryNotFoundException e, WebRequest request) { + @ExceptionHandler(RealmNotFoundException.class) + public ResponseEntity<ApiErrorDto> handle(RealmNotFoundException e, WebRequest request) { final ApiErrorDto response = ApiErrorDto.builder() .status(HttpStatus.NOT_FOUND) .message(e.getLocalizedMessage()) - .code("error.query.notfound") - .build(); - return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus()); - } - - @ResponseStatus(HttpStatus.GATEWAY_TIMEOUT) - @ExceptionHandler(QueryStoreException.class) - public ResponseEntity<ApiErrorDto> handle(QueryStoreException e, WebRequest request) { - final ApiErrorDto response = ApiErrorDto.builder() - .status(HttpStatus.GATEWAY_TIMEOUT) - .message(e.getLocalizedMessage()) - .code("error.query.store") - .build(); - return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus()); - } - - @ResponseStatus(HttpStatus.BAD_REQUEST) - @ExceptionHandler(SortException.class) - public ResponseEntity<ApiErrorDto> handle(SortException e, WebRequest request) { - final ApiErrorDto response = ApiErrorDto.builder() - .status(HttpStatus.BAD_REQUEST) - .message(e.getLocalizedMessage()) - .code("error.query.sort") + .code("error.user.realmnotfound") .build(); return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus()); } - @ResponseStatus(HttpStatus.LOCKED) - @ExceptionHandler(TableMalformedException.class) - public ResponseEntity<ApiErrorDto> handle(TableMalformedException e, WebRequest request) { + @ResponseStatus(HttpStatus.NO_CONTENT) + @ExceptionHandler(RemoteUnavailableException.class) + public ResponseEntity<ApiErrorDto> handle(RemoteUnavailableException e, WebRequest request) { final ApiErrorDto response = ApiErrorDto.builder() - .status(HttpStatus.LOCKED) + .status(HttpStatus.NO_CONTENT) .message(e.getLocalizedMessage()) - .code("error.query.tablemalformed") + .code("error.user.remoteunavailable") .build(); return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus()); } @ResponseStatus(HttpStatus.NOT_FOUND) - @ExceptionHandler(TableNotFoundException.class) - public ResponseEntity<ApiErrorDto> handle(TableNotFoundException e, WebRequest request) { + @ExceptionHandler(RoleNotFoundException.class) + public ResponseEntity<ApiErrorDto> handle(RoleNotFoundException e, WebRequest request) { final ApiErrorDto response = ApiErrorDto.builder() .status(HttpStatus.NOT_FOUND) .message(e.getLocalizedMessage()) - .code("error.query.tablenotfound") + .code("error.user.rolenotfound") .build(); return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus()); } @ResponseStatus(HttpStatus.CONFLICT) - @ExceptionHandler(TupleDeleteException.class) - public ResponseEntity<ApiErrorDto> handle(TupleDeleteException e, WebRequest request) { + @ExceptionHandler(UserAlreadyExistsException.class) + public ResponseEntity<ApiErrorDto> handle(UserAlreadyExistsException e, WebRequest request) { final ApiErrorDto response = ApiErrorDto.builder() .status(HttpStatus.CONFLICT) .message(e.getLocalizedMessage()) - .code("error.query.tupledelete") + .code("error.user.alreadyexists") .build(); return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus()); } @ResponseStatus(HttpStatus.NOT_FOUND) - @ExceptionHandler(UserNotFoundException.class) - public ResponseEntity<ApiErrorDto> handle(UserNotFoundException e, WebRequest request) { + @ExceptionHandler(UserAttributeNotFoundException.class) + public ResponseEntity<ApiErrorDto> handle(UserAttributeNotFoundException e, WebRequest request) { final ApiErrorDto response = ApiErrorDto.builder() .status(HttpStatus.NOT_FOUND) .message(e.getLocalizedMessage()) - .code("error.query.usernotfound") + .code("error.user.attributenotfound") .build(); return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus()); } - @ResponseStatus(HttpStatus.LOCKED) - @ExceptionHandler(ViewMalformedException.class) - public ResponseEntity<ApiErrorDto> handle(ViewMalformedException e, WebRequest request) { + @ResponseStatus(HttpStatus.EXPECTATION_FAILED) + @ExceptionHandler(UserEmailAlreadyExistsException.class) + public ResponseEntity<ApiErrorDto> handle(UserEmailAlreadyExistsException e, WebRequest request) { final ApiErrorDto response = ApiErrorDto.builder() - .status(HttpStatus.LOCKED) + .status(HttpStatus.EXPECTATION_FAILED) .message(e.getLocalizedMessage()) - .code("error.query.viewmalformed") + .code("error.user.emailexists") .build(); return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus()); } @ResponseStatus(HttpStatus.NOT_FOUND) - @ExceptionHandler(ViewNotFoundException.class) - public ResponseEntity<ApiErrorDto> handle(ViewNotFoundException e, WebRequest request) { + @ExceptionHandler(UserNotFoundException.class) + public ResponseEntity<ApiErrorDto> handle(UserNotFoundException e, WebRequest request) { final ApiErrorDto response = ApiErrorDto.builder() .status(HttpStatus.NOT_FOUND) .message(e.getLocalizedMessage()) - .code("error.query.viewnotfound") + .code("error.user.notfound") .build(); return new ResponseEntity<>(response, new HttpHeaders(), response.getStatus()); } diff --git a/dbrepo-user-service/rest-service/src/test/java/at/tuwien/endpoint/UserEndpointUnitTest.java b/dbrepo-user-service/rest-service/src/test/java/at/tuwien/endpoint/UserEndpointUnitTest.java new file mode 100644 index 0000000000000000000000000000000000000000..df7a391957fe6cc8d33c21d76aa808dbe21b3bcc --- /dev/null +++ b/dbrepo-user-service/rest-service/src/test/java/at/tuwien/endpoint/UserEndpointUnitTest.java @@ -0,0 +1,466 @@ +package at.tuwien.endpoint; + +import at.tuwien.BaseUnitTest; +import at.tuwien.api.auth.SignupRequestDto; +import at.tuwien.api.user.*; +import at.tuwien.config.AuthenticationConfig; +import at.tuwien.entities.user.Realm; +import at.tuwien.entities.user.Role; +import at.tuwien.entities.user.User; +import at.tuwien.exception.*; +import at.tuwien.service.RealmService; +import at.tuwien.service.RoleService; +import at.tuwien.service.UserService; +import lombok.extern.log4j.Log4j2; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.test.context.support.WithAnonymousUser; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.security.Principal; +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +@Log4j2 +@EnableAutoConfiguration(exclude = RabbitAutoConfiguration.class) +@SpringBootTest +@ExtendWith(SpringExtension.class) +public class UserEndpointUnitTest extends BaseUnitTest { + + @MockBean + private UserService userService; + + @MockBean + private RealmService realmService; + + @MockBean + private RoleService roleService; + + @MockBean + private AuthenticationConfig authenticationConfig; + + @Autowired + private UserEndpoint userEndpoint; + + @BeforeEach + public void beforeEach() { + when(authenticationConfig.getDefaultRole()) + .thenReturn("default-researcher-roles"); + } + + @Test + @WithAnonymousUser + public void findAll_anonymous_succeeds() { + + /* test */ + findAll_generic(); + } + + @Test + @WithMockUser(username = USER_1_USERNAME) + public void findAll_noRole_succeeds() { + + /* test */ + findAll_generic(); + } + + @Test + @WithAnonymousUser + public void create_anonymous_succeeds() throws UserNotFoundException, UserEmailAlreadyExistsException, + RealmNotFoundException, RoleNotFoundException, RemoteUnavailableException, UserAlreadyExistsException { + final SignupRequestDto request = SignupRequestDto.builder() + .email(USER_1_EMAIL) + .username(USER_1_USERNAME) + .password(USER_1_PASSWORD) + .build(); + + /* test */ + create_generic(USER_1, REALM_DBREPO, ROLE_DEFAULT_RESEARCHER_ROLES, request); + } + + @Test + @WithAnonymousUser + public void create_realmNotFound_fails() { + final SignupRequestDto request = SignupRequestDto.builder() + .email(USER_1_EMAIL) + .username(USER_1_USERNAME) + .password(USER_1_PASSWORD) + .build(); + + /* test */ + assertThrows(RealmNotFoundException.class, () -> { + create_generic(USER_1, null, ROLE_DEFAULT_RESEARCHER_ROLES, request); + }); + } + + @Test + @WithAnonymousUser + public void create_roleNotFound_fails() { + final SignupRequestDto request = SignupRequestDto.builder() + .email(USER_1_EMAIL) + .username(USER_1_USERNAME) + .password(USER_1_PASSWORD) + .build(); + + /* test */ + assertThrows(RoleNotFoundException.class, () -> { + create_generic(USER_1, REALM_DBREPO, null, request); + }); + } + + @Test + @WithMockUser(username = USER_1_USERNAME) + public void create_isAuthenticated_fails() { + final SignupRequestDto request = SignupRequestDto.builder() + .email(USER_2_EMAIL) + .username(USER_2_USERNAME) + .password(USER_2_PASSWORD) + .build(); + + /* test */ + assertThrows(AccessDeniedException.class, () -> { + create_generic(USER_2, REALM_DBREPO, ROLE_DEFAULT_RESEARCHER_ROLES, request); + }); + } + + @Test + @WithAnonymousUser + public void find_anonymous_fails() { + + /* test */ + assertThrows(AccessDeniedException.class, () -> { + find_generic(USER_1_ID.toString(), USER_1, null); + }); + } + + @Test + @WithMockUser(username = USER_1_USERNAME) + public void find_self_succeeds() throws UserNotFoundException, NotAllowedException { + + /* test */ + find_generic(USER_1_ID.toString(), USER_1, USER_1_PRINCIPAL); + } + + @Test + @WithMockUser(username = USER_1_USERNAME) + public void find_foreign_fails() { + + /* test */ + assertThrows(NotAllowedException.class, () -> { + find_generic(USER_2_ID.toString(), USER_2, USER_1_PRINCIPAL); + }); + } + + @Test + @WithMockUser(username = USER_4_USERNAME, authorities = {"find-user"}) + public void find_hasRoleForeign_succeeds() { + + /* test */ + assertThrows(NotAllowedException.class, () -> { + find_generic(USER_2_ID.toString(), USER_2, USER_4_PRINCIPAL); + }); + } + + @Test + @WithAnonymousUser + public void modify_anonymous_fails() { + final UserUpdateDto request = UserUpdateDto.builder() + .firstname(USER_1_FIRSTNAME) + .lastname(USER_1_LASTNAME) + .affiliation(USER_1_AFFILIATION) + .orcid(USER_1_ORCID) + .build(); + + /* test */ + assertThrows(AccessDeniedException.class, () -> { + modify_generic(USER_1_ID.toString(), USER_1, null, request); + }); + } + + @Test + @WithMockUser(username = USER_1_USERNAME) + public void modify_noRole_fails() { + final UserUpdateDto request = UserUpdateDto.builder() + .firstname(USER_1_FIRSTNAME) + .lastname(USER_1_LASTNAME) + .affiliation(USER_1_AFFILIATION) + .orcid(USER_1_ORCID) + .build(); + + /* test */ + assertThrows(AccessDeniedException.class, () -> { + modify_generic(USER_1_ID.toString(), USER_1, USER_1_PRINCIPAL, request); + }); + } + + @Test + @WithMockUser(username = USER_2_USERNAME, authorities = {"modify-user-information"}) + public void modify_hasRoleForeign_fails() { + final UserUpdateDto request = UserUpdateDto.builder() + .firstname(USER_1_FIRSTNAME) + .lastname(USER_1_LASTNAME) + .affiliation(USER_1_AFFILIATION) + .orcid(USER_1_ORCID) + .build(); + + /* test */ + assertThrows(ForeignUserException.class, () -> { + modify_generic(USER_1_ID.toString(), USER_1, USER_2_PRINCIPAL, request); + }); + } + + @Test + @WithMockUser(username = USER_1_USERNAME, authorities = {"modify-user-information"}) + public void modify_succeeds() throws UserNotFoundException, ForeignUserException, UserAttributeNotFoundException { + final UserUpdateDto request = UserUpdateDto.builder() + .firstname(USER_1_FIRSTNAME) + .lastname(USER_1_LASTNAME) + .affiliation(USER_1_AFFILIATION) + .orcid(USER_1_ORCID) + .build(); + + /* test */ + modify_generic(USER_1_ID.toString(), USER_1, USER_1_PRINCIPAL, request); + } + + @Test + @WithAnonymousUser + public void theme_anonymous_fails() { + final UserThemeSetDto request = UserThemeSetDto.builder() + .themeDark(USER_1_THEME_DARK) + .build(); + + /* test */ + assertThrows(AccessDeniedException.class, () -> { + theme_generic(USER_1_ID.toString(), USER_1, null, request); + }); + } + + @Test + @WithMockUser(username = USER_1_USERNAME) + public void theme_noRole_fails() { + final UserThemeSetDto request = UserThemeSetDto.builder() + .themeDark(USER_1_THEME_DARK) + .build(); + + /* test */ + assertThrows(AccessDeniedException.class, () -> { + theme_generic(USER_1_ID.toString(), USER_1, USER_1_PRINCIPAL, request); + }); + } + + @Test + @WithMockUser(username = USER_2_USERNAME, authorities = {"modify-user-theme"}) + public void theme_hasRoleForeign_fails() { + final UserThemeSetDto request = UserThemeSetDto.builder() + .themeDark(USER_1_THEME_DARK) + .build(); + + /* test */ + assertThrows(ForeignUserException.class, () -> { + theme_generic(USER_1_ID.toString(), USER_1, USER_2_PRINCIPAL, request); + }); + } + + @Test + @WithMockUser(username = USER_1_USERNAME, authorities = {"modify-user-theme"}) + public void theme_succeeds() throws UserNotFoundException, ForeignUserException, UserAttributeNotFoundException { + final UserThemeSetDto request = UserThemeSetDto.builder() + .themeDark(USER_1_THEME_DARK) + .build(); + + /* test */ + theme_generic(USER_1_ID.toString(), USER_1, USER_1_PRINCIPAL, request); + } + + @Test + @WithAnonymousUser + public void password_anonymous_fails() { + final UserPasswordDto request = UserPasswordDto.builder() + .password(USER_1_PASSWORD) + .build(); + + /* test */ + assertThrows(AccessDeniedException.class, () -> { + password_generic(USER_1_ID.toString(), USER_1, null, request); + }); + } + + @Test + @WithMockUser(username = USER_2_USERNAME) + public void password_noRoleForeign_fails() { + final UserPasswordDto request = UserPasswordDto.builder() + .password(USER_1_PASSWORD) + .build(); + + /* test */ + assertThrows(ForeignUserException.class, () -> { + password_generic(USER_1_ID.toString(), USER_1, USER_2_PRINCIPAL, request); + }); + } + + @Test + @WithMockUser(username = USER_1_USERNAME) + public void password_succeeds() throws UserNotFoundException, ForeignUserException { + final UserPasswordDto request = UserPasswordDto.builder() + .password(USER_1_PASSWORD) + .build(); + + /* test */ + password_generic(USER_1_ID.toString(), USER_1, USER_1_PRINCIPAL, request); + } + + /* ################################################################################################### */ + /* ## GENERIC TEST CASES ## */ + /* ################################################################################################### */ + + protected void findAll_generic() { + + /* mock */ + when(userService.findAll()) + .thenReturn(List.of(USER_1, USER_2)); + + /* test */ + final ResponseEntity<List<UserBriefDto>> response = userEndpoint.findAll(); + assertEquals(HttpStatus.OK, response.getStatusCode()); + final List<UserBriefDto> body = response.getBody(); + assertNotNull(body); + assertEquals(2, body.size()); + } + + protected void create_generic(User user, Realm realm, Role role, SignupRequestDto data) + throws UserNotFoundException, UserEmailAlreadyExistsException, RealmNotFoundException, + RoleNotFoundException, RemoteUnavailableException, UserAlreadyExistsException { + + /* mock */ + if (realm != null) { + when(realmService.find("dbrepo")) + .thenReturn(realm); + } else { + doThrow(RealmNotFoundException.class) + .when(realmService) + .find("dbrepo"); + } + if (role != null) { + when(roleService.find("default-researcher-roles")) + .thenReturn(role); + } else { + doThrow(RoleNotFoundException.class) + .when(roleService) + .find("default-researcher-roles"); + } + if (user != null) { + when(userService.create(data, realm, role)) + .thenReturn(user); + } else { + doThrow(UserNotFoundException.class) + .when(userService) + .create(data, realm, role); + } + + /* test */ + final ResponseEntity<UserBriefDto> response = userEndpoint.create(data); + assertEquals(HttpStatus.CREATED, response.getStatusCode()); + final UserBriefDto body = response.getBody(); + assertNotNull(body); + } + + protected void find_generic(String id, User user, Principal principal) throws UserNotFoundException, + NotAllowedException { + + /* mock */ + if (user != null) { + when(userService.find(UUID.fromString(id))) + .thenReturn(user); + } else { + doThrow(UserNotFoundException.class) + .when(userService) + .find(UUID.fromString(id)); + } + + /* test */ + final ResponseEntity<UserDto> response = userEndpoint.find(id, principal); + assertEquals(HttpStatus.OK, response.getStatusCode()); + final UserDto body = response.getBody(); + assertNotNull(body); + } + + protected void modify_generic(String id, User user, Principal principal, UserUpdateDto data) + throws UserNotFoundException, ForeignUserException, UserAttributeNotFoundException { + + /* mock */ + if (user != null) { + when(userService.find(UUID.fromString(id))) + .thenReturn(user); + } else { + doThrow(UserNotFoundException.class) + .when(userService) + .find(UUID.fromString(id)); + } + when(userService.modify(UUID.fromString(id), data)) + .thenReturn(user); + + /* test */ + final ResponseEntity<UserDto> response = userEndpoint.modify(id, data, principal); + assertEquals(HttpStatus.ACCEPTED, response.getStatusCode()); + final UserDto body = response.getBody(); + assertNotNull(body); + } + + protected void theme_generic(String id, User user, Principal principal, UserThemeSetDto data) + throws UserNotFoundException, ForeignUserException, UserAttributeNotFoundException { + + /* mock */ + if (user != null) { + when(userService.find(UUID.fromString(id))) + .thenReturn(user); + } else { + doThrow(UserNotFoundException.class) + .when(userService) + .find(UUID.fromString(id)); + } + when(userService.toggleTheme(UUID.fromString(id), data)) + .thenReturn(user); + + /* test */ + final ResponseEntity<UserDto> response = userEndpoint.theme(id, data, principal); + assertEquals(HttpStatus.ACCEPTED, response.getStatusCode()); + final UserDto body = response.getBody(); + assertNotNull(body); + } + + protected void password_generic(String id, User user, Principal principal, UserPasswordDto data) + throws UserNotFoundException, ForeignUserException { + + /* mock */ + if (user != null) { + when(userService.find(UUID.fromString(id))) + .thenReturn(user); + } else { + doThrow(UserNotFoundException.class) + .when(userService) + .find(UUID.fromString(id)); + } + when(userService.updatePassword(UUID.fromString(id), data)) + .thenReturn(user); + + /* test */ + final ResponseEntity<UserDto> response = userEndpoint.password(id, data, principal); + assertEquals(HttpStatus.ACCEPTED, response.getStatusCode()); + final UserDto body = response.getBody(); + assertNotNull(body); + } +} diff --git a/dbrepo-user-service/rest-service/src/test/java/at/tuwien/service/RealmServiceIntegrationTest.java b/dbrepo-user-service/rest-service/src/test/java/at/tuwien/service/RealmServiceIntegrationTest.java new file mode 100644 index 0000000000000000000000000000000000000000..53822947511046bfdc258e81fb11f0af22b3a697 --- /dev/null +++ b/dbrepo-user-service/rest-service/src/test/java/at/tuwien/service/RealmServiceIntegrationTest.java @@ -0,0 +1,57 @@ +package at.tuwien.service; + +import at.tuwien.BaseUnitTest; +import at.tuwien.entities.user.Realm; +import at.tuwien.exception.RealmNotFoundException; +import at.tuwien.repository.jpa.RealmRepository; +import lombok.extern.log4j.Log4j2; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.junit.jupiter.api.Assertions.*; + +@Log4j2 +@EnableAutoConfiguration(exclude = RabbitAutoConfiguration.class) +@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) +@SpringBootTest +@ExtendWith(SpringExtension.class) +public class RealmServiceIntegrationTest extends BaseUnitTest { + + @Autowired + private RealmRepository realmRepository; + + @Autowired + private RealmService realmService; + + @BeforeEach + public void beforeEach() { + realmRepository.save(REALM_DBREPO); + } + + @Test + public void find_succeeds() throws RealmNotFoundException { + + /* test */ + final Realm response = realmService.find(REALM_DBREPO_NAME); + assertNotNull(response); + assertEquals(REALM_DBREPO_ID, response.getId()); + assertEquals(REALM_DBREPO_NAME, response.getName()); + assertEquals(REALM_DBREPO_ENABLED, response.getEnabled()); + } + + @Test + public void find_fails() { + + /* test */ + assertThrows(RealmNotFoundException.class, () -> { + realmService.find("shadow"); + }); + } +} diff --git a/dbrepo-user-service/rest-service/src/test/java/at/tuwien/service/RoleServiceIntegrationTest.java b/dbrepo-user-service/rest-service/src/test/java/at/tuwien/service/RoleServiceIntegrationTest.java new file mode 100644 index 0000000000000000000000000000000000000000..2cc1cd90399afa2eb9757625c6afa879de77a0c3 --- /dev/null +++ b/dbrepo-user-service/rest-service/src/test/java/at/tuwien/service/RoleServiceIntegrationTest.java @@ -0,0 +1,64 @@ +package at.tuwien.service; + +import at.tuwien.BaseUnitTest; +import at.tuwien.entities.user.Realm; +import at.tuwien.entities.user.Role; +import at.tuwien.exception.RealmNotFoundException; +import at.tuwien.exception.RoleNotFoundException; +import at.tuwien.repository.jpa.RealmRepository; +import at.tuwien.repository.jpa.RoleRepository; +import at.tuwien.repository.jpa.UserRepository; +import lombok.extern.log4j.Log4j2; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.junit.jupiter.api.Assertions.*; + +@Log4j2 +@EnableAutoConfiguration(exclude = RabbitAutoConfiguration.class) +@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) +@SpringBootTest +@ExtendWith(SpringExtension.class) +public class RoleServiceIntegrationTest extends BaseUnitTest { + + @Autowired + private UserRepository userRepository; + + @Autowired + private RoleRepository roleRepository; + + @Autowired + private RoleService roleService; + + @BeforeEach + public void beforeEach() { + userRepository.save(USER_1_SIMPLE); + roleRepository.save(ROLE_DEFAULT_RESEARCHER_ROLES); + } + + @Test + public void find_succeeds() throws RoleNotFoundException { + + /* test */ + final Role response = roleService.find(ROLE_DEFAULT_RESEARCHER_ROLES_NAME); + assertNotNull(response); + assertEquals(ROLE_DEFAULT_RESEARCHER_ROLES_ID, response.getId()); + assertEquals(ROLE_DEFAULT_RESEARCHER_ROLES_NAME, response.getName()); + } + + @Test + public void find_fails() { + + /* test */ + assertThrows(RoleNotFoundException.class, () -> { + roleService.find("1role2rulethemall"); + }); + } +} diff --git a/dbrepo-user-service/rest-service/src/test/java/at/tuwien/service/UserAttributeServiceIntegrationTest.java b/dbrepo-user-service/rest-service/src/test/java/at/tuwien/service/UserAttributeServiceIntegrationTest.java new file mode 100644 index 0000000000000000000000000000000000000000..70b28170937e8de1a288ddcd7da2369e61f75d08 --- /dev/null +++ b/dbrepo-user-service/rest-service/src/test/java/at/tuwien/service/UserAttributeServiceIntegrationTest.java @@ -0,0 +1,100 @@ +package at.tuwien.service; + +import at.tuwien.BaseUnitTest; +import at.tuwien.entities.user.Role; +import at.tuwien.entities.user.UserAttribute; +import at.tuwien.exception.RoleNotFoundException; +import at.tuwien.exception.UserAttributeNotFoundException; +import at.tuwien.repository.jpa.RoleRepository; +import at.tuwien.repository.jpa.UserAttributeRepository; +import at.tuwien.repository.jpa.UserRepository; +import lombok.extern.log4j.Log4j2; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +@Log4j2 +@EnableAutoConfiguration(exclude = RabbitAutoConfiguration.class) +@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) +@SpringBootTest +@ExtendWith(SpringExtension.class) +public class UserAttributeServiceIntegrationTest extends BaseUnitTest { + + @Autowired + private UserRepository userRepository; + + @Autowired + private UserAttributeRepository userAttributeRepository; + + @Autowired + private UserAttributeService userAttributeService; + + @BeforeEach + public void beforeEach() { + userRepository.save(USER_1_SIMPLE); + userAttributeRepository.saveAll(USER_1_ATTRIBUTES); + userRepository.save(USER_2_SIMPLE); + } + + @Test + public void find_succeeds() throws UserAttributeNotFoundException { + + /* test */ + final UserAttribute response = userAttributeService.find(USER_1_ID, "theme_dark"); + assertNotNull(response); + } + + @Test + public void find_fails() { + + /* test */ + assertThrows(UserAttributeNotFoundException.class, () -> { + userAttributeService.find(USER_2_ID, "theme_dark"); + }); + } + + @Test + public void create_succeeds() { + final UserAttribute request = UserAttribute.builder() + .id(UUID.randomUUID()) + .userId(USER_2_ID) + .name("debug") + .value("yes") + .build(); + + /* test */ + final UserAttribute response = userAttributeService.create(request); + assertNotNull(response); + assertEquals("debug", response.getName()); + assertEquals("yes", response.getValue()); + } + + @Test + public void update_succeeds() throws UserAttributeNotFoundException { + + /* test */ + final UserAttribute response = userAttributeService.update(USER_1_ID, "affiliation", "NASA"); + assertNotNull(response); + assertEquals("affiliation", response.getName()); + assertEquals("NASA", response.getValue()); + } + + @Test + public void update_fails() { + + /* test */ + assertThrows(UserAttributeNotFoundException.class, () -> { + userAttributeService.update(USER_2_ID, "affiliation", "NASA"); + }); + } +} diff --git a/dbrepo-user-service/rest-service/src/test/java/at/tuwien/service/UserServiceIntegrationTest.java b/dbrepo-user-service/rest-service/src/test/java/at/tuwien/service/UserServiceIntegrationTest.java new file mode 100644 index 0000000000000000000000000000000000000000..52cb9f38f1b62fa33687549e68f6618ba451d2bb --- /dev/null +++ b/dbrepo-user-service/rest-service/src/test/java/at/tuwien/service/UserServiceIntegrationTest.java @@ -0,0 +1,246 @@ +package at.tuwien.service; + +import at.tuwien.BaseUnitTest; +import at.tuwien.api.auth.SignupRequestDto; +import at.tuwien.api.user.UserPasswordDto; +import at.tuwien.api.user.UserThemeSetDto; +import at.tuwien.api.user.UserUpdateDto; +import at.tuwien.entities.user.Role; +import at.tuwien.entities.user.User; +import at.tuwien.entities.user.UserAttribute; +import at.tuwien.exception.*; +import at.tuwien.repository.jpa.RealmRepository; +import at.tuwien.repository.jpa.RoleRepository; +import at.tuwien.repository.jpa.UserRepository; +import lombok.extern.log4j.Log4j2; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +@Log4j2 +@EnableAutoConfiguration(exclude = RabbitAutoConfiguration.class) +@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_EACH_TEST_METHOD) +@SpringBootTest +@ExtendWith(SpringExtension.class) +public class UserServiceIntegrationTest extends BaseUnitTest { + + @Autowired + private UserRepository userRepository; + + @Autowired + private RealmRepository realmRepository; + + @Autowired + private RoleRepository roleRepository; + + @Autowired + private UserService userService; + + @BeforeEach + public void beforeEach() { + realmRepository.save(REALM_DBREPO); + userRepository.save(USER_1); + roleRepository.save(ROLE_DEFAULT_RESEARCHER_ROLES); + } + + @Test + public void findAll_succeeds() { + + /* test */ + final List<User> response = userService.findAll(); + assertEquals(1, response.size()); + } + + @Test + public void create_succeeds() throws UserAlreadyExistsException { + final SignupRequestDto request = SignupRequestDto.builder() + .username(USER_2_USERNAME) + .password(USER_2_PASSWORD) + .email(USER_2_EMAIL) + .build(); + + /* test */ + final User response = userService.create(request, REALM_DBREPO, ROLE_DEFAULT_RESEARCHER_ROLES); + assertEquals(1, response.getRoles().size()); + final Role role = response.getRoles().get(0); + assertEquals(ROLE_DEFAULT_RESEARCHER_ROLES_ID, role.getId()); + assertEquals(ROLE_DEFAULT_RESEARCHER_ROLES_NAME, role.getName()); + } + + @Test + public void create_nonUniqueUsername_fails() { + final SignupRequestDto request = SignupRequestDto.builder() + .username(USER_1_USERNAME) + .password(USER_2_PASSWORD) + .email(USER_2_EMAIL) + .build(); + + /* test */ + assertThrows(DataIntegrityViolationException.class, () -> { + userService.create(request, REALM_DBREPO, ROLE_DEFAULT_RESEARCHER_ROLES); + }); + } + + @Test + public void create_nonUniqueEmail_fails() { + final SignupRequestDto request = SignupRequestDto.builder() + .username(USER_2_USERNAME) + .password(USER_2_PASSWORD) + .email(USER_1_EMAIL) + .build(); + + /* test */ + assertThrows(DataIntegrityViolationException.class, () -> { + userService.create(request, REALM_DBREPO, ROLE_DEFAULT_RESEARCHER_ROLES); + }); + } + + @Test + @Transactional + public void modify_succeeds() throws UserNotFoundException, UserAttributeNotFoundException { + final UserUpdateDto request = UserUpdateDto.builder() + .firstname(USER_1_FIRSTNAME) + .lastname(USER_1_LASTNAME) + .affiliation("NASA") + .orcid(null) + .build(); + + /* test */ + final User response = userService.modify(USER_1_ID, request); + assertEquals(USER_1_ID, response.getId()); + assertEquals(USER_1_FIRSTNAME, response.getFirstname()); + assertEquals(USER_1_LASTNAME, response.getLastname()); + assertEquals(3, response.getAttributes().size()); + final Optional<UserAttribute> affiliation = response.getAttributes().stream().filter(a -> a.getName().equals("affiliation")).findFirst(); + assertTrue(affiliation.isPresent()); + assertEquals("NASA", affiliation.get().getValue()); + final Optional<UserAttribute> orcid = response.getAttributes().stream().filter(a -> a.getName().equals("orcid")).findFirst(); + assertTrue(orcid.isPresent()); + assertNull(orcid.get().getValue()); + } + + @Test + public void modify_notFound_fails() { + final UserUpdateDto request = UserUpdateDto.builder() + .firstname(USER_2_FIRSTNAME) + .lastname(USER_2_LASTNAME) + .affiliation(USER_2_AFFILIATION) + .orcid(USER_2_ORCID) + .build(); + + /* test */ + assertThrows(UserNotFoundException.class, () -> { + userService.modify(USER_2_ID, request); + }); + } + + @Test + public void updatePassword_succeeds() throws UserNotFoundException { + final UserPasswordDto request = UserPasswordDto.builder() + .password(USER_1_PASSWORD) + .build(); + + /* test */ + final User response = userService.updatePassword(USER_1_ID, request); + assertEquals(1, response.getCredentials().size()); + } + + @Test + public void updatePassword_notFound_fails() { + final UserPasswordDto request = UserPasswordDto.builder() + .password(USER_1_PASSWORD) + .build(); + + /* test */ + assertThrows(UserNotFoundException.class, () -> { + userService.updatePassword(USER_2_ID, request); + }); + } + + @Test + @Transactional + public void toggleTheme_succeeds() throws UserNotFoundException, UserAttributeNotFoundException { + final UserThemeSetDto request = UserThemeSetDto.builder() + .themeDark(true) + .build(); + + /* test */ + final User response = userService.toggleTheme(USER_1_ID, request); + assertNotNull(response.getAttributes()); + assertEquals(3, response.getAttributes().size()); + } + + @Test + public void toggleTheme_fails() { + final UserThemeSetDto request = UserThemeSetDto.builder() + .themeDark(true) + .build(); + + /* test */ + assertThrows(UserNotFoundException.class, () -> { + userService.toggleTheme(USER_2_ID, request); + }); + } + + @Test + public void find_succeeds() throws UserNotFoundException { + + /* test */ + final User user = userService.find(USER_1_ID); + assertEquals(USER_1_ID, user.getId()); + } + + @Test + public void find_notFound_fails() { + + /* test */ + assertThrows(UserNotFoundException.class, () -> { + userService.find(USER_2_ID); + }); + } + + @Test + public void validateUsernameNotExists_succeeds() throws UserAlreadyExistsException { + + /* test */ + userService.validateUsernameNotExists(USER_2_USERNAME); + } + + @Test + public void validateUsernameNotExists_fails() { + + /* test */ + assertThrows(UserAlreadyExistsException.class, () -> { + userService.validateUsernameNotExists(USER_1_USERNAME); + }); + } + + @Test + public void validateEmailNotExists_succeeds() throws UserEmailAlreadyExistsException { + + /* test */ + userService.validateEmailNotExists(USER_2_EMAIL); + } + + @Test + public void validateEmailNotExists_fails() { + + /* test */ + assertThrows(UserEmailAlreadyExistsException.class, () -> { + userService.validateEmailNotExists(USER_1_EMAIL); + }); + } +} diff --git a/dbrepo-user-service/rest-service/src/test/resources/application.properties b/dbrepo-user-service/rest-service/src/test/resources/application.properties index ddba279a3bfcf1940585c1d6cb4c4608cd3f54c9..a2cfb02bead54bf217dd0152bb3170dda5e113f1 100644 --- a/dbrepo-user-service/rest-service/src/test/resources/application.properties +++ b/dbrepo-user-service/rest-service/src/test/resources/application.properties @@ -20,6 +20,8 @@ spring.jpa.show-sql=false # additional logging logging.level.root=error logging.level.at.tuwien.=trace +logging.level.org.hibernate.SQL=debug +logging.level.org.hibernate.type=trace # search service fda.consumers=2 diff --git a/dbrepo-user-service/services/src/main/java/at/tuwien/exception/AmqpException.java b/dbrepo-user-service/services/src/main/java/at/tuwien/exception/AmqpException.java deleted file mode 100644 index 6af0750d6f5089a8442a7159eac2076462df2825..0000000000000000000000000000000000000000 --- a/dbrepo-user-service/services/src/main/java/at/tuwien/exception/AmqpException.java +++ /dev/null @@ -1,21 +0,0 @@ -package at.tuwien.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(code = HttpStatus.NOT_ACCEPTABLE) -public class AmqpException extends Exception { - - public AmqpException(String msg) { - super(msg); - } - - public AmqpException(String msg, Throwable thr) { - super(msg, thr); - } - - public AmqpException(Throwable thr) { - super(thr); - } - -} diff --git a/dbrepo-user-service/services/src/main/java/at/tuwien/exception/ColumnParseException.java b/dbrepo-user-service/services/src/main/java/at/tuwien/exception/ColumnParseException.java deleted file mode 100644 index c0c1e109de740d33646d7b91bb1a68ebabdc7616..0000000000000000000000000000000000000000 --- a/dbrepo-user-service/services/src/main/java/at/tuwien/exception/ColumnParseException.java +++ /dev/null @@ -1,21 +0,0 @@ -package at.tuwien.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(code = HttpStatus.EXPECTATION_FAILED) -public class ColumnParseException extends Exception { - - public ColumnParseException(String msg) { - super(msg); - } - - public ColumnParseException(String msg, Throwable thr) { - super(msg, thr); - } - - public ColumnParseException(Throwable thr) { - super(thr); - } - -} diff --git a/dbrepo-user-service/services/src/main/java/at/tuwien/exception/ContainerNotFoundException.java b/dbrepo-user-service/services/src/main/java/at/tuwien/exception/ContainerNotFoundException.java deleted file mode 100644 index 85d49d4cb34b20b15fdc1441c69cbab0fe0a34f3..0000000000000000000000000000000000000000 --- a/dbrepo-user-service/services/src/main/java/at/tuwien/exception/ContainerNotFoundException.java +++ /dev/null @@ -1,21 +0,0 @@ -package at.tuwien.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(code = HttpStatus.NOT_FOUND) -public class ContainerNotFoundException extends Exception { - - public ContainerNotFoundException(String msg) { - super(msg); - } - - public ContainerNotFoundException(String msg, Throwable thr) { - super(msg, thr); - } - - public ContainerNotFoundException(Throwable thr) { - super(thr); - } - -} diff --git a/dbrepo-user-service/services/src/main/java/at/tuwien/exception/DatabaseConnectionException.java b/dbrepo-user-service/services/src/main/java/at/tuwien/exception/DatabaseConnectionException.java deleted file mode 100644 index 3c1f797647d67b4e1cb8148a4aba11b7d06aefc0..0000000000000000000000000000000000000000 --- a/dbrepo-user-service/services/src/main/java/at/tuwien/exception/DatabaseConnectionException.java +++ /dev/null @@ -1,23 +0,0 @@ -package at.tuwien.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -import java.io.IOException; - -@ResponseStatus(code = HttpStatus.METHOD_NOT_ALLOWED) -public class DatabaseConnectionException extends Exception { - - public DatabaseConnectionException(String msg) { - super(msg); - } - - public DatabaseConnectionException(String msg, Throwable thr) { - super(msg, thr); - } - - public DatabaseConnectionException(Throwable thr) { - super(thr); - } - -} diff --git a/dbrepo-user-service/services/src/main/java/at/tuwien/exception/DatabaseNotFoundException.java b/dbrepo-user-service/services/src/main/java/at/tuwien/exception/DatabaseNotFoundException.java deleted file mode 100644 index b9ca79c783048e8d0298db273abdb9462efeeec8..0000000000000000000000000000000000000000 --- a/dbrepo-user-service/services/src/main/java/at/tuwien/exception/DatabaseNotFoundException.java +++ /dev/null @@ -1,23 +0,0 @@ -package at.tuwien.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -import java.io.IOException; - -@ResponseStatus(code = HttpStatus.NOT_FOUND) -public class DatabaseNotFoundException extends Exception { - - public DatabaseNotFoundException(String msg) { - super(msg); - } - - public DatabaseNotFoundException(String msg, Throwable thr) { - super(msg, thr); - } - - public DatabaseNotFoundException(Throwable thr) { - super(thr); - } - -} diff --git a/dbrepo-user-service/services/src/main/java/at/tuwien/exception/FileStorageException.java b/dbrepo-user-service/services/src/main/java/at/tuwien/exception/FileStorageException.java deleted file mode 100644 index ab068e4245526e77b611d1c8571df867d5fc2cb6..0000000000000000000000000000000000000000 --- a/dbrepo-user-service/services/src/main/java/at/tuwien/exception/FileStorageException.java +++ /dev/null @@ -1,20 +0,0 @@ -package at.tuwien.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(code = HttpStatus.BAD_REQUEST) -public class FileStorageException extends Exception { - - public FileStorageException(String msg) { - super(msg); - } - - public FileStorageException(String msg, Throwable thr) { - super(msg, thr); - } - - public FileStorageException(Throwable thr) { - super(thr); - } -} diff --git a/dbrepo-user-service/services/src/main/java/at/tuwien/exception/IdentifierNotFoundException.java b/dbrepo-user-service/services/src/main/java/at/tuwien/exception/IdentifierNotFoundException.java deleted file mode 100644 index f0bb71f36492511efbe2c8c959dcdb97c679702f..0000000000000000000000000000000000000000 --- a/dbrepo-user-service/services/src/main/java/at/tuwien/exception/IdentifierNotFoundException.java +++ /dev/null @@ -1,21 +0,0 @@ -package at.tuwien.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(code = HttpStatus.NOT_FOUND) -public class IdentifierNotFoundException extends Exception { - - public IdentifierNotFoundException(String msg) { - super(msg); - } - - public IdentifierNotFoundException(String msg, Throwable thr) { - super(msg, thr); - } - - public IdentifierNotFoundException(Throwable thr) { - super(thr); - } - -} diff --git a/dbrepo-user-service/services/src/main/java/at/tuwien/exception/ImageNotSupportedException.java b/dbrepo-user-service/services/src/main/java/at/tuwien/exception/ImageNotSupportedException.java deleted file mode 100644 index 70963128f8410f856029916c606efe152957f12a..0000000000000000000000000000000000000000 --- a/dbrepo-user-service/services/src/main/java/at/tuwien/exception/ImageNotSupportedException.java +++ /dev/null @@ -1,23 +0,0 @@ -package at.tuwien.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -import java.io.IOException; - -@ResponseStatus(code = HttpStatus.CONFLICT) -public class ImageNotSupportedException extends Exception { - - public ImageNotSupportedException(String msg) { - super(msg); - } - - public ImageNotSupportedException(String msg, Throwable thr) { - super(msg, thr); - } - - public ImageNotSupportedException(Throwable thr) { - super(thr); - } - -} diff --git a/dbrepo-user-service/services/src/main/java/at/tuwien/exception/PaginationException.java b/dbrepo-user-service/services/src/main/java/at/tuwien/exception/PaginationException.java deleted file mode 100644 index 9d56aec9c2752c2c37e7b31f227950ad94c95ef3..0000000000000000000000000000000000000000 --- a/dbrepo-user-service/services/src/main/java/at/tuwien/exception/PaginationException.java +++ /dev/null @@ -1,21 +0,0 @@ -package at.tuwien.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(code = HttpStatus.BAD_REQUEST) -public class PaginationException extends Exception { - - public PaginationException(String msg) { - super(msg); - } - - public PaginationException(String msg, Throwable thr) { - super(msg, thr); - } - - public PaginationException(Throwable thr) { - super(thr); - } - -} diff --git a/dbrepo-user-service/services/src/main/java/at/tuwien/exception/QueryAlreadyPersistedException.java b/dbrepo-user-service/services/src/main/java/at/tuwien/exception/QueryAlreadyPersistedException.java deleted file mode 100644 index 48d3bb0ad9ec2fb1f7c6b9727f2a3e700291cbc6..0000000000000000000000000000000000000000 --- a/dbrepo-user-service/services/src/main/java/at/tuwien/exception/QueryAlreadyPersistedException.java +++ /dev/null @@ -1,19 +0,0 @@ -package at.tuwien.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(code = HttpStatus.CONFLICT) -public class QueryAlreadyPersistedException extends Exception { - - public QueryAlreadyPersistedException(String msg) { - super(msg); - } - - public QueryAlreadyPersistedException(String msg, Throwable thr) { - super(msg, thr); - } - - public QueryAlreadyPersistedException(Throwable thr) { super(thr); - } -} diff --git a/dbrepo-user-service/services/src/main/java/at/tuwien/exception/QueryMalformedException.java b/dbrepo-user-service/services/src/main/java/at/tuwien/exception/QueryMalformedException.java deleted file mode 100644 index 3d81b6ba4e659b49b59591fa8e9baec31767615a..0000000000000000000000000000000000000000 --- a/dbrepo-user-service/services/src/main/java/at/tuwien/exception/QueryMalformedException.java +++ /dev/null @@ -1,23 +0,0 @@ -package at.tuwien.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -import java.io.IOException; - -@ResponseStatus(code = HttpStatus.BAD_REQUEST) -public class QueryMalformedException extends Exception { - - public QueryMalformedException(String msg) { - super(msg); - } - - public QueryMalformedException(String msg, Throwable thr) { - super(msg, thr); - } - - public QueryMalformedException(Throwable thr) { - super(thr); - } - -} diff --git a/dbrepo-user-service/services/src/main/java/at/tuwien/exception/QueryNotFoundException.java b/dbrepo-user-service/services/src/main/java/at/tuwien/exception/QueryNotFoundException.java deleted file mode 100644 index a5e90754898f19f6cce8938d2385f3f9fecd43e4..0000000000000000000000000000000000000000 --- a/dbrepo-user-service/services/src/main/java/at/tuwien/exception/QueryNotFoundException.java +++ /dev/null @@ -1,21 +0,0 @@ -package at.tuwien.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(code = HttpStatus.NOT_FOUND) -public class QueryNotFoundException extends Exception { - - public QueryNotFoundException(String msg) { - super(msg); - } - - public QueryNotFoundException(String msg, Throwable thr) { - super(msg, thr); - } - - public QueryNotFoundException(Throwable thr) { - super(thr); - } - -} diff --git a/dbrepo-user-service/services/src/main/java/at/tuwien/exception/QueryStoreException.java b/dbrepo-user-service/services/src/main/java/at/tuwien/exception/QueryStoreException.java deleted file mode 100644 index b1f472f2a1179200475b7952dd459af0ee7f7b6a..0000000000000000000000000000000000000000 --- a/dbrepo-user-service/services/src/main/java/at/tuwien/exception/QueryStoreException.java +++ /dev/null @@ -1,19 +0,0 @@ -package at.tuwien.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(code = HttpStatus.GATEWAY_TIMEOUT) -public class QueryStoreException extends Exception { - - public QueryStoreException(String msg) { - super(msg); - } - - public QueryStoreException(String msg, Throwable thr) { - super(msg, thr); - } - - public QueryStoreException(Throwable thr) { super(thr); - } -} diff --git a/dbrepo-user-service/services/src/main/java/at/tuwien/exception/SortException.java b/dbrepo-user-service/services/src/main/java/at/tuwien/exception/SortException.java deleted file mode 100644 index 7415590ad637461baac4c9bf1d68b7d411054b19..0000000000000000000000000000000000000000 --- a/dbrepo-user-service/services/src/main/java/at/tuwien/exception/SortException.java +++ /dev/null @@ -1,21 +0,0 @@ -package at.tuwien.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(code = HttpStatus.BAD_REQUEST) -public class SortException extends Exception { - - public SortException(String msg) { - super(msg); - } - - public SortException(String msg, Throwable thr) { - super(msg, thr); - } - - public SortException(Throwable thr) { - super(thr); - } - -} diff --git a/dbrepo-user-service/services/src/main/java/at/tuwien/exception/TableMalformedException.java b/dbrepo-user-service/services/src/main/java/at/tuwien/exception/TableMalformedException.java deleted file mode 100644 index 542c789ad548084b1a98720630246514b431efcb..0000000000000000000000000000000000000000 --- a/dbrepo-user-service/services/src/main/java/at/tuwien/exception/TableMalformedException.java +++ /dev/null @@ -1,21 +0,0 @@ -package at.tuwien.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(code = HttpStatus.LOCKED) -public class TableMalformedException extends Exception { - - public TableMalformedException(String msg) { - super(msg); - } - - public TableMalformedException(String msg, Throwable thr) { - super(msg, thr); - } - - public TableMalformedException(Throwable thr) { - super(thr); - } - -} diff --git a/dbrepo-user-service/services/src/main/java/at/tuwien/exception/TableNotFoundException.java b/dbrepo-user-service/services/src/main/java/at/tuwien/exception/TableNotFoundException.java deleted file mode 100644 index 89fa3ed467e76998431c2b366bceb83804824f38..0000000000000000000000000000000000000000 --- a/dbrepo-user-service/services/src/main/java/at/tuwien/exception/TableNotFoundException.java +++ /dev/null @@ -1,21 +0,0 @@ -package at.tuwien.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(code = HttpStatus.NOT_FOUND) -public class TableNotFoundException extends Exception { - - public TableNotFoundException(String msg) { - super(msg); - } - - public TableNotFoundException(String msg, Throwable thr) { - super(msg, thr); - } - - public TableNotFoundException(Throwable thr) { - super(thr); - } - -} diff --git a/dbrepo-user-service/services/src/main/java/at/tuwien/exception/TupleDeleteException.java b/dbrepo-user-service/services/src/main/java/at/tuwien/exception/TupleDeleteException.java deleted file mode 100644 index 87e5e4a483f9dfe38f9563be9a9d275415b5a0d9..0000000000000000000000000000000000000000 --- a/dbrepo-user-service/services/src/main/java/at/tuwien/exception/TupleDeleteException.java +++ /dev/null @@ -1,19 +0,0 @@ -package at.tuwien.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(code = HttpStatus.CONFLICT) -public class TupleDeleteException extends Exception { - - public TupleDeleteException(String msg) { - super(msg); - } - - public TupleDeleteException(String msg, Throwable thr) { - super(msg, thr); - } - - public TupleDeleteException(Throwable thr) { super(thr); - } -} diff --git a/dbrepo-user-service/services/src/main/java/at/tuwien/exception/UserEmailAlreadyExistsException.java b/dbrepo-user-service/services/src/main/java/at/tuwien/exception/UserEmailAlreadyExistsException.java new file mode 100644 index 0000000000000000000000000000000000000000..803e94aa0a8c6ec2cf8f1d699d2fa490e76e0593 --- /dev/null +++ b/dbrepo-user-service/services/src/main/java/at/tuwien/exception/UserEmailAlreadyExistsException.java @@ -0,0 +1,21 @@ +package at.tuwien.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(code = HttpStatus.EXPECTATION_FAILED) +public class UserEmailAlreadyExistsException extends Exception { + + public UserEmailAlreadyExistsException(String message) { + super(message); + } + + public UserEmailAlreadyExistsException(String message, Throwable thr) { + super(message, thr); + } + + public UserEmailAlreadyExistsException(Throwable thr) { + super(thr); + } + +} diff --git a/dbrepo-user-service/services/src/main/java/at/tuwien/exception/ViewMalformedException.java b/dbrepo-user-service/services/src/main/java/at/tuwien/exception/ViewMalformedException.java deleted file mode 100644 index 5dfdaf170eaccc76da2290673448be19511e4fab..0000000000000000000000000000000000000000 --- a/dbrepo-user-service/services/src/main/java/at/tuwien/exception/ViewMalformedException.java +++ /dev/null @@ -1,21 +0,0 @@ -package at.tuwien.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(code = HttpStatus.LOCKED) -public class ViewMalformedException extends Exception { - - public ViewMalformedException(String msg) { - super(msg); - } - - public ViewMalformedException(String msg, Throwable thr) { - super(msg, thr); - } - - public ViewMalformedException(Throwable thr) { - super(thr); - } - -} diff --git a/dbrepo-user-service/services/src/main/java/at/tuwien/exception/ViewNotFoundException.java b/dbrepo-user-service/services/src/main/java/at/tuwien/exception/ViewNotFoundException.java deleted file mode 100644 index 2f260975ff4746858184a4c8e5d4ee8268425625..0000000000000000000000000000000000000000 --- a/dbrepo-user-service/services/src/main/java/at/tuwien/exception/ViewNotFoundException.java +++ /dev/null @@ -1,21 +0,0 @@ -package at.tuwien.exception; - -import org.springframework.http.HttpStatus; -import org.springframework.web.bind.annotation.ResponseStatus; - -@ResponseStatus(code = HttpStatus.NOT_FOUND, reason = "View not found") -public class ViewNotFoundException extends Exception { - - public ViewNotFoundException(String message) { - super(message); - } - - public ViewNotFoundException(String message, Throwable thr) { - super(message, thr); - } - - public ViewNotFoundException(Throwable thr) { - super(thr); - } - -} diff --git a/dbrepo-user-service/services/src/main/java/at/tuwien/service/UserService.java b/dbrepo-user-service/services/src/main/java/at/tuwien/service/UserService.java index c607aedaa7e24f26cd0b1bd136678f7db2ec5a36..0026eb377a762214937c80da8d12831476ab9130 100644 --- a/dbrepo-user-service/services/src/main/java/at/tuwien/service/UserService.java +++ b/dbrepo-user-service/services/src/main/java/at/tuwien/service/UserService.java @@ -16,22 +16,77 @@ import java.util.UUID; public interface UserService { /** - * Finds all users + * Finds all users in the metadata database * * @return The list of users. */ List<User> findAll(); - User create(SignupRequestDto data, Realm realm, Role role) throws RemoteUnavailableException, UserNotFoundException, - UserAlreadyExistsException; - - User modify(UUID id, UserUpdateDto data, Principal principal) throws UserNotFoundException, ForeignUserException, UserAttributeNotFoundException; + /** + * Creates a user in the metadata database managed by Keycloak in the given realm and with given role. + * + * @param data The user data. + * @param realm The realm this user should be created. + * @param role The role. + * @return The user, if successful. False otherwise. + * @throws UserAlreadyExistsException The user already exists in the metadata database. + */ + User create(SignupRequestDto data, Realm realm, Role role) throws UserAlreadyExistsException; - User updatePassword(UUID id, UserPasswordDto data, Principal principal) throws UserNotFoundException, - ForeignUserException; + /** + * Updates the user information for a user with given id in the metadata database. + * + * @param id The user id. + * @param data The user information. + * @return The user if successful. False otherwise. + * @throws UserNotFoundException The user was not found. + * @throws UserAttributeNotFoundException One or more user attributes for the user information were not found. + */ + User modify(UUID id, UserUpdateDto data) throws UserNotFoundException, UserAttributeNotFoundException; - User toggleTheme(UUID id, UserThemeSetDto data, Principal principal) throws UserNotFoundException, ForeignUserException, UserAttributeNotFoundException; + /** + * Updates the user password for a user with given id. + * + * @param id The user id. + * @param data The new password. + * @return The user if successful. False otherwise. + * @throws UserNotFoundException The user was not found. + */ + User updatePassword(UUID id, UserPasswordDto data) throws UserNotFoundException; + /** + * Updates the user theme for a user with given id. + * + * @param id The user id. + * @param data The user theme. + * @return The user if successful. False otherwise. + * @throws UserNotFoundException The user was not found. + * @throws UserAttributeNotFoundException One or more user attributes for the user information were not found. + */ + User toggleTheme(UUID id, UserThemeSetDto data) throws UserNotFoundException, UserAttributeNotFoundException; + /** + * Finds a specific user in the metadata database by given id. + * + * @param id The user id. + * @return The user if successful. False otherwise. + * @throws UserNotFoundException The user was not found. + */ User find(UUID id) throws UserNotFoundException; + + /** + * Validates if a user with the given username already exists in the metadata database. + * + * @param username The username. + * @throws UserAlreadyExistsException The user with this username already exists. + */ + void validateUsernameNotExists(String username) throws UserAlreadyExistsException; + + /** + * Validates if a user with the given email already exists in the metadata database. + * + * @param email The email. + * @throws UserEmailAlreadyExistsException The user with this email already exists. + */ + void validateEmailNotExists(String email) throws UserEmailAlreadyExistsException; } diff --git a/dbrepo-user-service/services/src/main/java/at/tuwien/service/impl/UserServiceImpl.java b/dbrepo-user-service/services/src/main/java/at/tuwien/service/impl/UserServiceImpl.java index 56f67ca22d47699a3c6a2b75e82a06972bc7ea20..0d0d80b5b83e23ed12a251a7775ca17dac4e469b 100644 --- a/dbrepo-user-service/services/src/main/java/at/tuwien/service/impl/UserServiceImpl.java +++ b/dbrepo-user-service/services/src/main/java/at/tuwien/service/impl/UserServiceImpl.java @@ -22,7 +22,6 @@ import org.springframework.transaction.annotation.Transactional; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.PBEKeySpec; import java.security.NoSuchAlgorithmException; -import java.security.Principal; import java.security.SecureRandom; import java.security.spec.InvalidKeySpecException; import java.security.spec.KeySpec; @@ -60,24 +59,14 @@ public class UserServiceImpl implements UserService { } @Override + @Transactional(readOnly = true) public List<User> findAll() { return userRepository.findAll(); } @Override - public User create(SignupRequestDto data, Realm realm, Role role) throws RemoteUnavailableException, UserNotFoundException, - UserAlreadyExistsException { - /* check */ - final Optional<User> optional = userRepository.findByUsername(data.getUsername()); - if (optional.isPresent()) { - log.error("User with username {} already exists", data.getUsername()); - throw new UserAlreadyExistsException("User with username " + data.getUsername() + " already exists"); - } - final Optional<User> optional2 = userRepository.findByEmail(data.getEmail()); - if (optional2.isPresent()) { - log.error("User with email {} already exists", data.getUsername()); - throw new UserAlreadyExistsException("User with email " + data.getUsername() + " already exists"); - } + @Transactional(rollbackFor = RuntimeException.class) + public User create(SignupRequestDto data, Realm realm, Role role) throws UserAlreadyExistsException { /* create secret */ final byte[] salt = getSalt(); final StringBuilder secretData = new StringBuilder("{\"value\":\"") @@ -111,30 +100,21 @@ public class UserServiceImpl implements UserService { credential = credentialRepository.save(credential); user.setCredentials(List.of(credential)); user.setAttributes(List.of(userAttribute1, userAttribute2, userAttribute3)); - final RoleMapping tmp2 = RoleMapping.builder() - .userId(user.getId()) - .roleId(role.getId()) - .build(); - roleMappingRepository.save(tmp2); user.setRoles(List.of(role)); log.info("Created user with id {}", user.getId()); - log.debug("created user {}", user); return user; } @Override - public User modify(UUID id, UserUpdateDto data, Principal principal) throws UserNotFoundException, - ForeignUserException, UserAttributeNotFoundException { + @Transactional + public User modify(UUID id, UserUpdateDto data) throws UserNotFoundException, + UserAttributeNotFoundException { /* check */ - User user = find(id); - if (!user.equalsPrincipal(principal)) { - log.error("Failed to modify user: attempting to modify other user"); - throw new ForeignUserException("Failed to modify user: attempting to modify other user"); - } - user.setFirstname(data.getFirstname()); - user.setLastname(data.getLastname()); + final User entity = find(id); + entity.setFirstname(data.getFirstname()); + entity.setLastname(data.getLastname()); /* save in metadata database */ - user = userRepository.save(user); + final User user = userRepository.save(entity); log.info("Modified user with id {}", user.getId()); /* modify attributes */ userAttributeService.update(user.getId(), "orcid", data.getOrcid()); @@ -143,14 +123,9 @@ public class UserServiceImpl implements UserService { } @Override - public User updatePassword(UUID id, UserPasswordDto data, Principal principal) throws UserNotFoundException, - ForeignUserException { - /* check */ + @Transactional(rollbackFor = RuntimeException.class) + public User updatePassword(UUID id, UserPasswordDto data) throws UserNotFoundException { final User user = find(id); - if (!user.equalsPrincipal(principal)) { - log.error("Failed to modify user: attempting to modify other user"); - throw new ForeignUserException("Failed to modify user: attempting to modify other user"); - } /* create secret */ final byte[] salt = getSalt(); final StringBuilder secretData = new StringBuilder("{\"value\":\"") @@ -158,7 +133,9 @@ public class UserServiceImpl implements UserService { .append("\",\"salt\":\"") .append(Base64.encodeBytes(salt)) .append("\",\"additionalParameters\":{}}"); - Credential credential = Credential.builder() + final Credential entity = Credential.builder() + .id(UUID.randomUUID()) + .userId(user.getId()) .createdDate(Instant.now().toEpochMilli()) .secretData(secretData.toString()) .type("password") @@ -166,14 +143,15 @@ public class UserServiceImpl implements UserService { .credentialData("{\"hashIterations\":" + DEFAULT_ITERATIONS + ",\"algorithm\":\"" + ID + "\",\"additionalParameters\":{}}") .build(); /* save */ - credential = credentialRepository.save(credential); + final Credential credential = credentialRepository.save(entity); user.setCredentials(List.of(credential)); log.info("Updated user password with id {}", user.getId()); return user; } @Override - public User toggleTheme(UUID id, UserThemeSetDto data, Principal principal) throws UserNotFoundException, + @Transactional + public User toggleTheme(UUID id, UserThemeSetDto data) throws UserNotFoundException, UserAttributeNotFoundException { /* check */ final User user = find(id); @@ -183,6 +161,7 @@ public class UserServiceImpl implements UserService { } @Override + @Transactional(readOnly = true) public User find(UUID id) throws UserNotFoundException { final Optional<User> optional = userRepository.findById(id); if (optional.isEmpty()) { @@ -192,6 +171,26 @@ public class UserServiceImpl implements UserService { return optional.get(); } + @Override + @Transactional(readOnly = true) + public void validateUsernameNotExists(String username) throws UserAlreadyExistsException { + final Optional<User> optional = userRepository.findByUsername(username); + if (optional.isPresent()) { + log.error("User with username {} already exists", username); + throw new UserAlreadyExistsException("User with username " + username + " already exists"); + } + } + + @Override + @Transactional(readOnly = true) + public void validateEmailNotExists(String email) throws UserEmailAlreadyExistsException { + final Optional<User> optional = userRepository.findByEmail(email); + if (optional.isPresent()) { + log.error("User with email {} already exists", email); + throw new UserEmailAlreadyExistsException("User with email already exists"); + } + } + private String encodedCredential(String rawPassword, int iterations, byte[] salt, int derivedKeySize) { final String rawPasswordWithPadding = PaddingUtils.padding(rawPassword, MAX_PADDING_LENGTH); final KeySpec spec = new PBEKeySpec(rawPasswordWithPadding.toCharArray(), salt, iterations, derivedKeySize);