From 277bd57d82776af2b3d323ba048340dc60b8da46 Mon Sep 17 00:00:00 2001
From: Martin Weise <martin.weise@tuwien.ac.at>
Date: Wed, 5 Apr 2023 16:08:05 +0200
Subject: [PATCH] Adding default roles works

---
 .../java/at/tuwien/entities/user/Role.java    | 44 +++++++++++++++++++
 .../at/tuwien/entities/user/RoleMapping.java  | 31 +++++++++++++
 .../tuwien/entities/user/RoleMappingKey.java  | 14 ++++++
 .../java/at/tuwien/entities/user/User.java    |  5 +++
 fda-ui/api/user/index.js                      |  2 +-
 .../java/at/tuwien/endpoint/UserEndpoint.java | 17 ++++---
 .../exception/RoleNotFoundException.java      | 21 +++++++++
 .../repository/jpa/RoleMappingRepository.java |  9 ++++
 .../tuwien/repository/jpa/RoleRepository.java | 13 ++++++
 .../java/at/tuwien/service/RoleService.java   |  8 ++++
 .../java/at/tuwien/service/UserService.java   |  3 +-
 .../tuwien/service/impl/RoleServiceImpl.java  | 36 +++++++++++++++
 .../tuwien/service/impl/UserServiceImpl.java  | 29 +++++++-----
 13 files changed, 213 insertions(+), 19 deletions(-)
 create mode 100644 fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/Role.java
 create mode 100644 fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/RoleMapping.java
 create mode 100644 fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/RoleMappingKey.java
 create mode 100644 fda-user-service/services/src/main/java/at/tuwien/exception/RoleNotFoundException.java
 create mode 100644 fda-user-service/services/src/main/java/at/tuwien/repository/jpa/RoleMappingRepository.java
 create mode 100644 fda-user-service/services/src/main/java/at/tuwien/repository/jpa/RoleRepository.java
 create mode 100644 fda-user-service/services/src/main/java/at/tuwien/service/RoleService.java
 create mode 100644 fda-user-service/services/src/main/java/at/tuwien/service/impl/RoleServiceImpl.java

diff --git a/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/Role.java b/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/Role.java
new file mode 100644
index 0000000000..f1a8b1031b
--- /dev/null
+++ b/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/Role.java
@@ -0,0 +1,44 @@
+package at.tuwien.entities.user;
+
+import lombok.*;
+import org.hibernate.annotations.GenericGenerator;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+
+import javax.persistence.*;
+
+@Data
+@Entity
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@ToString
+@EntityListeners(AuditingEntityListener.class)
+@EqualsAndHashCode(onlyExplicitlyIncluded = true)
+@Table(name = "keycloak_role")
+public class Role {
+
+    @Id
+    @EqualsAndHashCode.Include
+    @GeneratedValue(generator = "role-uuid")
+    @GenericGenerator(name = "role-uuid", strategy = "org.hibernate.id.UUIDGenerator")
+    @Column(name = "ID", nullable = false, columnDefinition = "VARCHAR(36)")
+    private String id;
+
+    @Column(name = "NAME", nullable = false)
+    private String name;
+
+    @Column(name = "REALM_ID", nullable = false)
+    private String 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;
+
+}
diff --git a/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/RoleMapping.java b/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/RoleMapping.java
new file mode 100644
index 0000000000..4ef61ea2ef
--- /dev/null
+++ b/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/RoleMapping.java
@@ -0,0 +1,31 @@
+package at.tuwien.entities.user;
+
+import lombok.*;
+import org.hibernate.annotations.GenericGenerator;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+
+import javax.persistence.*;
+
+@Data
+@Entity
+@Builder
+@AllArgsConstructor
+@NoArgsConstructor
+@ToString
+@IdClass(RoleMappingKey.class)
+@EntityListeners(AuditingEntityListener.class)
+@EqualsAndHashCode(onlyExplicitlyIncluded = true)
+@Table(name = "user_role_mapping")
+public class RoleMapping {
+
+    @Id
+    @EqualsAndHashCode.Include
+    @Column(name = "USER_ID", nullable = false)
+    private String userId;
+
+    @Id
+    @EqualsAndHashCode.Include
+    @Column(name = "ROLE_ID", nullable = false)
+    private String roleId;
+
+}
diff --git a/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/RoleMappingKey.java b/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/RoleMappingKey.java
new file mode 100644
index 0000000000..7e96f96b1f
--- /dev/null
+++ b/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/RoleMappingKey.java
@@ -0,0 +1,14 @@
+package at.tuwien.entities.user;
+
+import lombok.EqualsAndHashCode;
+
+import java.io.Serializable;
+
+@EqualsAndHashCode
+public class RoleMappingKey implements Serializable {
+
+    private String userId;
+
+    private String roleId;
+
+}
diff --git a/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/User.java b/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/User.java
index af7c013620..87d933a27d 100644
--- a/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/User.java
+++ b/fda-metadata-db/entities/src/main/java/at/tuwien/entities/user/User.java
@@ -58,6 +58,11 @@ public class User {
     @Column(nullable = false)
     private String databasePassword;
 
+    @Column(nullable = false)
+    @ToString.Exclude
+    @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "user")
+    private List<Role> roles;
+
     @Column(nullable = false)
     @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "user")
     private List<Credential> credentials;
diff --git a/fda-ui/api/user/index.js b/fda-ui/api/user/index.js
index 670535c449..feb0b974e7 100644
--- a/fda-ui/api/user/index.js
+++ b/fda-ui/api/user/index.js
@@ -10,7 +10,7 @@ export function authenticate (clientSecret, username, password) {
     password,
     grant_type: 'password',
     client_secret: clientSecret,
-    scope: 'openid roles'
+    scope: 'openid profile roles'
   }
   return axios.post('/api/auth/realms/dbrepo/protocol/openid-connect/token', qs.stringify(payload), {
     headers: { ContentType: 'application/form-data' }
diff --git a/fda-user-service/rest-service/src/main/java/at/tuwien/endpoint/UserEndpoint.java b/fda-user-service/rest-service/src/main/java/at/tuwien/endpoint/UserEndpoint.java
index 0d5148892f..0939c16efa 100644
--- a/fda-user-service/rest-service/src/main/java/at/tuwien/endpoint/UserEndpoint.java
+++ b/fda-user-service/rest-service/src/main/java/at/tuwien/endpoint/UserEndpoint.java
@@ -3,12 +3,11 @@ package at.tuwien.endpoint;
 import at.tuwien.api.auth.SignupRequestDto;
 import at.tuwien.api.user.UserBriefDto;
 import at.tuwien.entities.auth.Realm;
-import at.tuwien.exception.RealmNotFoundException;
-import at.tuwien.exception.RemoteUnavailableException;
-import at.tuwien.exception.UserAlreadyExistsException;
-import at.tuwien.exception.UserNotFoundException;
+import at.tuwien.entities.user.Role;
+import at.tuwien.exception.*;
 import at.tuwien.mapper.UserMapper;
 import at.tuwien.service.RealmService;
+import at.tuwien.service.RoleService;
 import at.tuwien.service.UserService;
 import io.micrometer.core.annotation.Timed;
 import io.swagger.v3.oas.annotations.Operation;
@@ -33,12 +32,15 @@ public class UserEndpoint {
     private final UserMapper userMapper;
     private final UserService userService;
     private final RealmService realmService;
+    private final RoleService roleService;
 
     @Autowired
-    public UserEndpoint(UserMapper userMapper, UserService userService, RealmService realmService) {
+    public UserEndpoint(UserMapper userMapper, UserService userService, RealmService realmService,
+                        RoleService roleService) {
         this.userMapper = userMapper;
         this.userService = userService;
         this.realmService = realmService;
+        this.roleService = roleService;
     }
 
     @GetMapping
@@ -61,10 +63,11 @@ public class UserEndpoint {
     @Operation(summary = "Create a user")
     public ResponseEntity<UserBriefDto> create(@NotNull @Valid @RequestBody SignupRequestDto data)
             throws UserNotFoundException, RemoteUnavailableException, RealmNotFoundException,
-            UserAlreadyExistsException {
+            UserAlreadyExistsException, RoleNotFoundException {
         log.debug("endpoint create a user, data={}", data);
         final Realm realm = realmService.find("dbrepo");
-        final UserBriefDto dto = userMapper.userToUserBriefDto(userService.create(data, realm));
+        final Role role = roleService.find("default-researcher-roles");
+        final UserBriefDto dto = userMapper.userToUserBriefDto(userService.create(data, realm, role));
         log.trace("create user resulted in dto {}", dto);
         return ResponseEntity.status(HttpStatus.CREATED)
                 .body(dto);
diff --git a/fda-user-service/services/src/main/java/at/tuwien/exception/RoleNotFoundException.java b/fda-user-service/services/src/main/java/at/tuwien/exception/RoleNotFoundException.java
new file mode 100644
index 0000000000..8430d64d76
--- /dev/null
+++ b/fda-user-service/services/src/main/java/at/tuwien/exception/RoleNotFoundException.java
@@ -0,0 +1,21 @@
+package at.tuwien.exception;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.web.bind.annotation.ResponseStatus;
+
+@ResponseStatus(code = HttpStatus.NOT_FOUND)
+public class RoleNotFoundException extends Exception {
+
+    public RoleNotFoundException(String msg) {
+        super(msg);
+    }
+
+    public RoleNotFoundException(String msg, Throwable thr) {
+        super(msg, thr);
+    }
+
+    public RoleNotFoundException(Throwable thr) {
+        super(thr);
+    }
+
+}
diff --git a/fda-user-service/services/src/main/java/at/tuwien/repository/jpa/RoleMappingRepository.java b/fda-user-service/services/src/main/java/at/tuwien/repository/jpa/RoleMappingRepository.java
new file mode 100644
index 0000000000..fd7261d3a8
--- /dev/null
+++ b/fda-user-service/services/src/main/java/at/tuwien/repository/jpa/RoleMappingRepository.java
@@ -0,0 +1,9 @@
+package at.tuwien.repository.jpa;
+
+import at.tuwien.entities.user.RoleMapping;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface RoleMappingRepository extends JpaRepository<RoleMapping, String> {
+}
diff --git a/fda-user-service/services/src/main/java/at/tuwien/repository/jpa/RoleRepository.java b/fda-user-service/services/src/main/java/at/tuwien/repository/jpa/RoleRepository.java
new file mode 100644
index 0000000000..6efb6c2071
--- /dev/null
+++ b/fda-user-service/services/src/main/java/at/tuwien/repository/jpa/RoleRepository.java
@@ -0,0 +1,13 @@
+package at.tuwien.repository.jpa;
+
+import at.tuwien.entities.user.Role;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+@Repository
+public interface RoleRepository extends JpaRepository<Role, String> {
+
+    Optional<Role> findByName(String name);
+}
diff --git a/fda-user-service/services/src/main/java/at/tuwien/service/RoleService.java b/fda-user-service/services/src/main/java/at/tuwien/service/RoleService.java
new file mode 100644
index 0000000000..08c3a0cc3f
--- /dev/null
+++ b/fda-user-service/services/src/main/java/at/tuwien/service/RoleService.java
@@ -0,0 +1,8 @@
+package at.tuwien.service;
+
+import at.tuwien.entities.user.Role;
+import at.tuwien.exception.RoleNotFoundException;
+
+public interface RoleService {
+    Role find(String name) throws RoleNotFoundException;
+}
diff --git a/fda-user-service/services/src/main/java/at/tuwien/service/UserService.java b/fda-user-service/services/src/main/java/at/tuwien/service/UserService.java
index 33addb5732..d60d9ab9b3 100644
--- a/fda-user-service/services/src/main/java/at/tuwien/service/UserService.java
+++ b/fda-user-service/services/src/main/java/at/tuwien/service/UserService.java
@@ -2,6 +2,7 @@ package at.tuwien.service;
 
 import at.tuwien.api.auth.SignupRequestDto;
 import at.tuwien.entities.auth.Realm;
+import at.tuwien.entities.user.Role;
 import at.tuwien.entities.user.User;
 import at.tuwien.exception.RemoteUnavailableException;
 import at.tuwien.exception.UserAlreadyExistsException;
@@ -27,7 +28,7 @@ public interface UserService {
      */
     User findByUsername(String username) throws UserNotFoundException;
 
-    User create(SignupRequestDto data, Realm realm) throws RemoteUnavailableException, UserNotFoundException, UserAlreadyExistsException;
+    User create(SignupRequestDto data, Realm realm, Role role) throws RemoteUnavailableException, UserNotFoundException, UserAlreadyExistsException;
 
     User find(String id) throws UserNotFoundException;
 }
diff --git a/fda-user-service/services/src/main/java/at/tuwien/service/impl/RoleServiceImpl.java b/fda-user-service/services/src/main/java/at/tuwien/service/impl/RoleServiceImpl.java
new file mode 100644
index 0000000000..7766f31afb
--- /dev/null
+++ b/fda-user-service/services/src/main/java/at/tuwien/service/impl/RoleServiceImpl.java
@@ -0,0 +1,36 @@
+package at.tuwien.service.impl;
+
+import at.tuwien.entities.user.Role;
+import at.tuwien.exception.RoleNotFoundException;
+import at.tuwien.repository.jpa.RoleRepository;
+import at.tuwien.service.RoleService;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.Optional;
+
+@Log4j2
+@Service
+public class RoleServiceImpl implements RoleService {
+
+    private final RoleRepository roleRepository;
+
+    @Autowired
+    public RoleServiceImpl(RoleRepository roleRepository) {
+        this.roleRepository = roleRepository;
+    }
+
+    @Override
+    public Role find(String name) throws RoleNotFoundException {
+        final Optional<Role> optional = roleRepository.findByName(name);
+        if (optional.isEmpty()) {
+            log.error("Failed to find role with name {}", name);
+            throw new RoleNotFoundException("Failed to find role with name " + name);
+        }
+        final Role role = optional.get();
+        log.trace("found role {}", role);
+        return role;
+    }
+
+}
diff --git a/fda-user-service/services/src/main/java/at/tuwien/service/impl/UserServiceImpl.java b/fda-user-service/services/src/main/java/at/tuwien/service/impl/UserServiceImpl.java
index 514460cf56..610bbc422d 100644
--- a/fda-user-service/services/src/main/java/at/tuwien/service/impl/UserServiceImpl.java
+++ b/fda-user-service/services/src/main/java/at/tuwien/service/impl/UserServiceImpl.java
@@ -3,12 +3,15 @@ package at.tuwien.service.impl;
 import at.tuwien.api.auth.SignupRequestDto;
 import at.tuwien.entities.auth.Realm;
 import at.tuwien.entities.user.Credential;
+import at.tuwien.entities.user.Role;
+import at.tuwien.entities.user.RoleMapping;
 import at.tuwien.entities.user.User;
 import at.tuwien.exception.RemoteUnavailableException;
 import at.tuwien.exception.UserAlreadyExistsException;
 import at.tuwien.exception.UserNotFoundException;
 import at.tuwien.mapper.UserMapper;
 import at.tuwien.repository.jpa.CredentialRepository;
+import at.tuwien.repository.jpa.RoleMappingRepository;
 import at.tuwien.repository.jpa.UserRepository;
 import at.tuwien.service.UserService;
 import lombok.extern.log4j.Log4j2;
@@ -41,13 +44,15 @@ public class UserServiceImpl implements UserService {
     private final UserMapper userMapper;
     private final UserRepository userRepository;
     private final CredentialRepository credentialRepository;
+    private final RoleMappingRepository roleMappingRepository;
 
     @Autowired
     public UserServiceImpl(UserMapper userMapper, UserRepository userRepository,
-                           CredentialRepository credentialRepository) {
+                           CredentialRepository credentialRepository, RoleMappingRepository roleMappingRepository) {
         this.userMapper = userMapper;
         this.userRepository = userRepository;
         this.credentialRepository = credentialRepository;
+        this.roleMappingRepository = roleMappingRepository;
     }
 
     @Override
@@ -66,7 +71,7 @@ public class UserServiceImpl implements UserService {
     }
 
     @Override
-    public User create(SignupRequestDto data, Realm realm) throws RemoteUnavailableException, UserNotFoundException,
+    public User create(SignupRequestDto data, Realm realm, Role role) throws RemoteUnavailableException, UserNotFoundException,
             UserAlreadyExistsException {
         /* check */
         final Optional<User> optional = userRepository.findByUsername(data.getUsername());
@@ -75,13 +80,6 @@ public class UserServiceImpl implements UserService {
             throw new UserAlreadyExistsException("User with username " + data.getUsername() + " already exists");
         }
         /* create secret */
-
-        /* save */
-        final User tmp = userMapper.signupRequestDtoToUser(data);
-        tmp.setEmailVerified(false);
-        tmp.setEnabled(true);
-        tmp.setRealmId(realm.getId());
-        tmp.setCreatedTimestamp(Instant.now().toEpochMilli());
         final byte[] salt = getSalt();
         final StringBuilder secretData = new StringBuilder("{\"value\":\"")
                 .append(encodedCredential(data.getPassword(), DEFAULT_ITERATIONS, salt, DERIVED_KEY_SIZE))
@@ -95,10 +93,22 @@ public class UserServiceImpl implements UserService {
                 .priority(10)
                 .credentialData("{\"hashIterations\":" + DEFAULT_ITERATIONS + ",\"algorithm\":\"" + ID + "\",\"additionalParameters\":{}}")
                 .build();
+        /* save */
+        final User tmp = userMapper.signupRequestDtoToUser(data);
+        tmp.setEmailVerified(false);
+        tmp.setEnabled(true);
+        tmp.setRealmId(realm.getId());
+        tmp.setCreatedTimestamp(Instant.now().toEpochMilli());
         final User user = userRepository.save(tmp);
         entity.setUserId(user.getId());
         final Credential credential = credentialRepository.save(entity);
         user.setCredentials(List.of(credential));
+        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;
@@ -116,7 +126,6 @@ public class UserServiceImpl implements UserService {
 
     private String encodedCredential(String rawPassword, int iterations, byte[] salt, int derivedKeySize) {
         final String rawPasswordWithPadding = PaddingUtils.padding(rawPassword, MAX_PADDING_LENGTH);
-        log.trace("padding: {}", rawPasswordWithPadding);
         final KeySpec spec = new PBEKeySpec(rawPasswordWithPadding.toCharArray(), salt, iterations, derivedKeySize);
         try {
             byte[] key = getSecretKeyFactory().generateSecret(spec).getEncoded();
-- 
GitLab