This is an automated email from the ASF dual-hosted git repository.
lahirujayathilake pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/airavata-custos.git
The following commit(s) were added to refs/heads/master by this push:
new ec12edcc5 User portal page API and UI changes
ec12edcc5 is described below
commit ec12edcc52bd843d67a756ad48a3e7bad58fa472
Author: Ganning Xu <[email protected]>
AuthorDate: Fri Feb 28 13:35:15 2025 -0500
User portal page API and UI changes
* UI of user's page
* refactor components
* remove unused imports
* add api level support for users page
* add functions for user's page
* Fix user management api routes
* removed unused imports
* Refactoring user apis
* Fix imports
* Update user endpoint works correctly. Only supports updating first/last
name.
* Add support for adding & deleting user roles
* Add support for adding & deleting user attributes
* Refactor get user profile methods
* Only allow updating first/last name
---
.../custos/api/user/UserManagementController.java | 256 ++++++++++++---------
.../custos/core/mapper/user/UserProfileMapper.java | 28 ++-
.../custos/core/model/user/UserAttribute.java | 24 +-
.../apache/custos/core/model/user/UserProfile.java | 14 +-
.../apache/custos/core/model/user/UserRole.java | 23 +-
.../core/repo/user/UserAttributeRepository.java | 3 +
.../custos/core/repo/user/UserRoleRepository.java | 3 +
core/src/main/proto/UserProfile.proto | 21 ++
custos-portal/src/components/NavContainer.tsx | 15 ++
.../src/components/Users/UserSettings.tsx | 191 +++++++++------
custos-portal/src/components/Users/index.tsx | 125 +++++-----
custos-portal/src/hooks/useApi.tsx | 33 ++-
custos-portal/src/index.tsx | 4 +-
custos-portal/src/interfaces/Users.tsx | 10 +-
custos-portal/src/lib/constants.ts | 2 +-
custos-portal/src/lib/util.ts | 38 +++
.../credential/store/CredentialStoreService.java | 12 +-
.../service/management/UserManagementService.java | 168 +++++---------
.../custos/service/profile/UserProfileService.java | 192 +++++++++++++---
19 files changed, 751 insertions(+), 411 deletions(-)
diff --git
a/api/src/main/java/org/apache/custos/api/user/UserManagementController.java
b/api/src/main/java/org/apache/custos/api/user/UserManagementController.java
index 6cdb23313..8b53e7d70 100644
--- a/api/src/main/java/org/apache/custos/api/user/UserManagementController.java
+++ b/api/src/main/java/org/apache/custos/api/user/UserManagementController.java
@@ -22,12 +22,9 @@ package org.apache.custos.api.user;
import org.apache.custos.core.constants.Constants;
import org.apache.custos.core.iam.api.AddExternalIDPLinksRequest;
import org.apache.custos.core.iam.api.AddUserAttributesRequest;
-import org.apache.custos.core.iam.api.AddUserRolesRequest;
import org.apache.custos.core.iam.api.DeleteExternalIDPsRequest;
import org.apache.custos.core.iam.api.DeleteUserAttributeRequest;
import org.apache.custos.core.iam.api.DeleteUserRolesRequest;
-import org.apache.custos.core.iam.api.FindUsersRequest;
-import org.apache.custos.core.iam.api.FindUsersResponse;
import org.apache.custos.core.iam.api.GetExternalIDPsRequest;
import org.apache.custos.core.iam.api.GetExternalIDPsResponse;
import org.apache.custos.core.iam.api.OperationStatus;
@@ -38,16 +35,20 @@ import org.apache.custos.core.iam.api.RegisterUsersResponse;
import org.apache.custos.core.iam.api.ResetUserPassword;
import org.apache.custos.core.iam.api.UserAttribute;
import org.apache.custos.core.iam.api.UserRepresentation;
-import org.apache.custos.core.iam.api.UserSearchMetadata;
import org.apache.custos.core.iam.api.UserSearchRequest;
import org.apache.custos.core.identity.api.AuthToken;
import org.apache.custos.core.user.management.api.LinkUserProfileRequest;
import org.apache.custos.core.user.management.api.SynchronizeUserDBRequest;
import org.apache.custos.core.user.management.api.UserProfileRequest;
+import org.apache.custos.core.user.profile.api.UsersRolesRequest;
+import org.apache.custos.core.user.profile.api.UsersRolesFullRequest;
import org.apache.custos.core.user.profile.api.GetAllUserProfilesResponse;
import org.apache.custos.core.user.profile.api.GetUpdateAuditTrailRequest;
import org.apache.custos.core.user.profile.api.GetUpdateAuditTrailResponse;
+import org.apache.custos.core.user.profile.api.UserAttributeRequest;
+import org.apache.custos.core.user.profile.api.UserAttributeFullRequest;
import org.apache.custos.core.user.profile.api.UserProfile;
+import org.apache.custos.core.user.profile.api.Status;
import org.apache.custos.service.auth.AuthClaim;
import org.apache.custos.service.auth.TokenAuthorizer;
import org.apache.custos.service.management.UserManagementService;
@@ -69,10 +70,12 @@ import
org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
@@ -393,35 +396,101 @@ public class UserManagementController {
return ResponseEntity.ok(response);
}
+
+ @PostMapping("/user/attributes")
+ @Operation(
+ summary = "Add attributes to a tenant user",
+ description = "This operation adds specified attributes to a
tenant user. The id of each attribute passed in is ignored."
+ )
+ public ResponseEntity<Status> addTenantUserAttributes(@RequestBody
UserAttributeRequest request, @RequestHeader HttpHeaders headers) {
+ Optional<AuthClaim> claim = tokenAuthorizer.authorize(headers);
+
+ if (claim.isEmpty()) {
+ throw new ResponseStatusException(HttpStatus.UNAUTHORIZED,
"Request is not authorized");
+ }
+
+ AuthClaim authClaim = claim.get();
+
+ UserAttributeFullRequest userAttributeFullRequest =
UserAttributeFullRequest.newBuilder()
+ .setUserAttributeRequest(request)
+ .setTenantId(authClaim.getTenantId())
+ .build();
+
+ Status status =
userManagementService.addAttributesToUser(userAttributeFullRequest);
+
+ return ResponseEntity.ok(status);
+ }
+
+ @DeleteMapping("/user/attributes")
+ @Operation(
+ summary = "Delete attributes from a tenant user",
+ description = "This operation removes specified attributes to a
tenant user. The id of each attribute passed in is ignored."
+ )
+ public ResponseEntity<Status> deleteTenantUserAttributes(@RequestBody
UserAttributeRequest request, @RequestHeader HttpHeaders headers) {
+ Optional<AuthClaim> claim = tokenAuthorizer.authorize(headers);
+
+ if (claim.isEmpty()) {
+ throw new ResponseStatusException(HttpStatus.UNAUTHORIZED,
"Request is not authorized");
+ }
+
+ AuthClaim authClaim = claim.get();
+
+ UserAttributeFullRequest userAttributeFullRequest =
UserAttributeFullRequest.newBuilder()
+ .setUserAttributeRequest(request)
+ .setTenantId(authClaim.getTenantId())
+ .build();
+
+ Status status =
userManagementService.deleteAttributesFromUser(userAttributeFullRequest);
+
+ return ResponseEntity.ok(status);
+ }
+
@PostMapping("/users/roles")
@Operation(
- summary = "Add Roles To Users",
- description = "This operation adds specified roles to identified
users. The AddUserRolesRequest " +
- "should include user identifiers and the list of roles to
be added. Upon successful execution, " +
- "the system associates the specified roles with the user
profiles and returns an OperationStatus reflecting the result."
+ summary = "Add roles to tenant users",
+ description = "This operation adds specified roles (client &
realm) to identified users."
)
- public ResponseEntity<OperationStatus> addRolesToUsers(@Valid @RequestBody
AddUserRolesRequest request, @RequestHeader HttpHeaders headers) {
- headers = attachUserToken(headers, request.getClientId());
- Optional<AuthClaim> claim = tokenAuthorizer.authorize(headers,
request.getClientId());
+ public ResponseEntity<Status> addUsersRoles(@Valid @RequestBody
UsersRolesRequest request, @RequestHeader HttpHeaders headers) {
+ Optional<AuthClaim> claim = tokenAuthorizer.authorize(headers);
- if (claim.isPresent()) {
- AuthClaim authClaim = claim.get();
- AuthToken authToken =
tokenAuthorizer.getSAToken(authClaim.getIamAuthId(),
authClaim.getIamAuthSecret(), authClaim.getTenantId());
+ if (claim.isEmpty()) {
+ throw new ResponseStatusException(HttpStatus.UNAUTHORIZED,
"Request is not authorized");
+ }
- if (authToken == null ||
StringUtils.isBlank(authToken.getAccessToken())) {
- throw new ResponseStatusException(HttpStatus.UNAUTHORIZED,
"Request is not authorized. Service Account token is invalid");
- }
+ AuthClaim authClaim = claim.get();
- request = request.toBuilder().setClientId(authClaim.getIamAuthId())
- .setTenantId(authClaim.getTenantId())
- .setAccessToken(authToken.getAccessToken())
- .setPerformedBy(authClaim.getPerformedBy()).build();
- } else {
+ UsersRolesFullRequest fullRequest = UsersRolesFullRequest.newBuilder()
+ .setUsersRoles(request)
+ .setTenantId(authClaim.getTenantId())
+ .build();
+
+ Status operationStatus =
userManagementService.addRolesToUsers(fullRequest);
+
+ return ResponseEntity.ok(operationStatus);
+ }
+
+ @DeleteMapping("/users/roles")
+ @Operation(
+ summary = "Delete roles from tenant users",
+ description = "This operation deletes specified roles (client &
realm) to identified users."
+ )
+ public ResponseEntity<Status> deleteUsersRoles(@Valid @RequestBody
UsersRolesRequest request, @RequestHeader HttpHeaders headers) {
+ Optional<AuthClaim> claim = tokenAuthorizer.authorize(headers);
+
+ if (claim.isEmpty()) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED,
"Request is not authorized");
}
- OperationStatus response =
userManagementService.addRolesToUsers(request);
- return ResponseEntity.ok(response);
+ AuthClaim authClaim = claim.get();
+
+ UsersRolesFullRequest fullRequest = UsersRolesFullRequest.newBuilder()
+ .setUsersRoles(request)
+ .setTenantId(authClaim.getTenantId())
+ .build();
+
+ Status operationStatus =
userManagementService.deleteRolesFromUsers(fullRequest);
+
+ return ResponseEntity.ok(operationStatus);
}
@GetMapping("/user/activation/status")
@@ -448,63 +517,6 @@ public class UserManagementController {
return ResponseEntity.ok(response);
}
- @GetMapping("/user")
- @Operation(
- summary = "Retrieve User",
- description = "This operation retrieves a specified user's
profile. The UserSearchRequest should specify " +
- "the criteria to identify the particular user. It returns
a UserRepresentation that includes " +
- "detailed information about the user."
- )
- public ResponseEntity<UserRepresentation> getUser(@Valid @RequestBody
UserSearchRequest request, @RequestHeader HttpHeaders headers) {
- Optional<AuthClaim> claim = tokenAuthorizer.authorize(headers,
request.getClientId());
-
- if (claim.isPresent()) {
- AuthClaim authClaim = claim.get();
- request = request.toBuilder().setClientId(authClaim.getIamAuthId())
- .setClientSec(authClaim.getIamAuthSecret())
- .setTenantId(claim.get().getTenantId()).build();
- } else {
- throw new ResponseStatusException(HttpStatus.UNAUTHORIZED,
"Request is not authorized");
- }
-
- UserRepresentation response = userManagementService.getUser(request);
- return ResponseEntity.ok(response);
- }
-
- @GetMapping("/users")
- @Operation(
- summary = "Find Users",
- description = "This operation searches for users that match the
criteria provided in the FindUsersRequest, " +
- "which can include attributes like username, email, roles,
etc. It returns a FindUsersResponse " +
- "containing the matching users' profiles."
- )
- public ResponseEntity<FindUsersResponse> findUsers(@RequestParam(value =
"client_id") String clientId,
- @RequestParam(value =
"offset") int offset,
- @RequestParam(value =
"limit") int limit,
-
@RequestParam("user.id") String userId,
- @RequestHeader
HttpHeaders headers) {
- Optional<AuthClaim> claim = tokenAuthorizer.authorize(headers,
clientId);
-
- if (claim.isPresent()) {
- AuthClaim authClaim = claim.get();
- UserSearchMetadata userSearchMetadata =
UserSearchMetadata.newBuilder()
- .setId(userId)
- .build();
- FindUsersRequest request =
FindUsersRequest.newBuilder().setClientId(authClaim.getIamAuthId())
- .setClientSec(authClaim.getIamAuthSecret())
- .setTenantId(claim.get().getTenantId())
- .setOffset(offset)
- .setLimit(limit)
- .setUser(userSearchMetadata)
- .build();
-
- FindUsersResponse response =
userManagementService.findUsers(request);
- return ResponseEntity.ok(response);
- } else {
- throw new ResponseStatusException(HttpStatus.UNAUTHORIZED,
"Request is not authorized");
- }
- }
-
@PutMapping("/user/password")
@Operation(
summary = "Update User Password",
@@ -570,53 +582,75 @@ public class UserManagementController {
return ResponseEntity.ok(response);
}
- @PutMapping("/user/profile")
+ @PatchMapping("/user/profile/{username}")
@Operation(
summary = "Update User Profile",
- description = "This operation updates profiles of existing users.
The UserProfileRequest should specify the updated " +
+ description = "This operation updates profiles of existing users.
The UserProfile should only specify fields that should be updated. Currently,
only the first and last name can be updated." +
"user details. Upon successful profile update, the system
sends back the updated UserProfile wrapped in a ResponseEntity."
)
- public ResponseEntity<UserProfile> updateUserProfile(@RequestBody
UserProfileRequest request, @RequestHeader HttpHeaders headers) {
- Optional<AuthClaim> claim = tokenAuthorizer.authorize(headers,
request.getClientId());
+ public ResponseEntity<UserProfile> updateUserProfile(@RequestBody
UserProfile userProfile, @PathVariable("username") String username,
@RequestHeader HttpHeaders headers) {
+ // need to update first name, last name, that's about it?
+ Optional<AuthClaim> claim = tokenAuthorizer.authorize(headers);
- if (claim.isPresent()) {
- AuthClaim authClaim = claim.get();
- AuthToken authToken =
tokenAuthorizer.getSAToken(authClaim.getIamAuthId(),
authClaim.getIamAuthSecret(), authClaim.getTenantId());
+ if (claim.isEmpty()) {
+ throw new ResponseStatusException(HttpStatus.UNAUTHORIZED,
"Request is not authorized");
+ }
- if (authToken == null ||
StringUtils.isBlank(authToken.getAccessToken())) {
- throw new ResponseStatusException(HttpStatus.UNAUTHORIZED,
"Request is not authorized. Service Account token is invalid");
- }
+ AuthClaim authClaim = claim.get();
+ AuthToken authToken =
tokenAuthorizer.getSAToken(authClaim.getIamAuthId(),
authClaim.getIamAuthSecret(), authClaim.getTenantId());
- request = request.toBuilder().setClientId(authClaim.getIamAuthId())
- .setClientSecret(authClaim.getIamAuthSecret())
- .setTenantId(authClaim.getTenantId())
- .setAccessToken(authToken.getAccessToken())
- .setPerformedBy(Constants.SYSTEM).build();
- } else {
- throw new ResponseStatusException(HttpStatus.UNAUTHORIZED,
"Request is not authorized");
+ if (authToken == null ||
StringUtils.isBlank(authToken.getAccessToken())) {
+ throw new ResponseStatusException(HttpStatus.UNAUTHORIZED,
"Request is not authorized. Service Account token is invalid");
}
+ userProfile = userProfile.toBuilder().setUsername(username).build();
+
+ UserProfileRequest request = UserProfileRequest.newBuilder()
+ .setUserProfile(userProfile)
+ .setClientId(authClaim.getIamAuthId())
+ .setClientSecret(authClaim.getIamAuthSecret())
+ .setTenantId(authClaim.getTenantId())
+ .setAccessToken(authToken.getAccessToken())
+ .setPerformedBy(Constants.SYSTEM)
+ .build();
UserProfile response =
userManagementService.updateUserProfile(request);
return ResponseEntity.ok(response);
}
- @GetMapping("/user/profile")
+ @GetMapping("/user/profile/{username}")
@Operation(
summary = "Get User Profile",
- description = "This operation retrieves the profile of a specified
user. The UserProfileRequest should specify which user's " +
- "profile is to be retrieved. The system would return a
ResponseEntity containing the UserProfile for the specified user."
+ description = "This operation retrieves the profile of a specified
user. Parameters should be passed in the query string."
)
- public ResponseEntity<UserProfile> getUserProfile(@Valid @RequestBody
UserProfileRequest request, @RequestHeader HttpHeaders headers) {
+ public ResponseEntity<UserProfile> getUserProfile(
+ @PathVariable("username") String username,
+ @RequestHeader HttpHeaders headers
+ ) {
+ // Authorization check
Optional<AuthClaim> claim =
tokenAuthorizer.authorizeUsingUserToken(headers);
+ // Build the UserProfile using the builder pattern
+ UserProfile userProfile = UserProfile.newBuilder()
+ .setUsername(username)
+ .build();
+
+ // Build the UserProfileRequest using the builder pattern
+ UserProfileRequest request = UserProfileRequest.newBuilder()
+ .setUserProfile(userProfile)
+ .setLimit(1)
+ .setOffset(0)
+ .build();
+
if (claim.isPresent()) {
request =
request.toBuilder().setTenantId(claim.get().getTenantId()).build();
} else {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED,
"Request is not authorized");
}
+ // Retrieve user profile
UserProfile response = userManagementService.getUserProfile(request);
+
return ResponseEntity.ok(response);
}
@@ -649,16 +683,24 @@ public class UserManagementController {
"Upon successful execution, the system sends back a
ResponseEntity containing GetAllUserProfilesResponse, " +
"wrapping all user profiles in the tenant."
)
- public ResponseEntity<GetAllUserProfilesResponse>
getAllUserProfilesInTenant(@RequestBody UserProfileRequest request,
@RequestHeader HttpHeaders headers) {
- Optional<AuthClaim> claim = tokenAuthorizer.authorize(headers,
request.getClientId());
+ public ResponseEntity<GetAllUserProfilesResponse>
getAllUserProfilesInTenant(
+ @RequestParam(value = "offset") int offset,
+ @RequestParam(value = "limit") int limit,
+ @RequestHeader HttpHeaders headers) {
+ Optional<AuthClaim> claim = tokenAuthorizer.authorize(headers);
- if (claim.isPresent()) {
- request =
request.toBuilder().setTenantId(claim.get().getTenantId()).build();
- } else {
+ if (claim.isEmpty()) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED,
"Request is not authorized");
}
+ UserProfileRequest request = UserProfileRequest.newBuilder()
+ .setOffset(offset)
+ .setLimit(limit)
+ .setTenantId(claim.get().getTenantId())
+ .build();
+
GetAllUserProfilesResponse response =
userManagementService.getAllUserProfilesInTenant(request);
+
return ResponseEntity.ok(response);
}
diff --git
a/core/src/main/java/org/apache/custos/core/mapper/user/UserProfileMapper.java
b/core/src/main/java/org/apache/custos/core/mapper/user/UserProfileMapper.java
index 7ae2c321b..b213eba19 100644
---
a/core/src/main/java/org/apache/custos/core/mapper/user/UserProfileMapper.java
+++
b/core/src/main/java/org/apache/custos/core/mapper/user/UserProfileMapper.java
@@ -20,9 +20,12 @@
package org.apache.custos.core.mapper.user;
import org.apache.custos.core.constants.Constants;
-import org.apache.custos.core.model.user.UserAttribute;
import org.apache.custos.core.model.user.UserProfile;
+import org.apache.custos.core.model.user.UserAttribute;
import org.apache.custos.core.model.user.UserRole;
+import org.apache.custos.core.model.user.GroupRole;
+import org.apache.custos.core.model.user.UserGroupMembership;
+
import org.apache.custos.core.user.profile.api.UserStatus;
import org.apache.custos.core.user.profile.api.UserTypes;
@@ -111,6 +114,29 @@ public class UserProfileMapper {
return entity;
}
+ public static org.apache.custos.core.user.profile.api.UserProfile
createFullUserProfileFromUserProfileEntity(UserProfile profileEntity) {
+ org.apache.custos.core.user.profile.api.UserProfile userProfile =
createUserProfileFromUserProfileEntity(profileEntity, null);
+
+ org.apache.custos.core.user.profile.api.UserProfile.Builder builder =
userProfile.toBuilder();
+
+ if (profileEntity.getUserGroupMemberships() != null &&
!profileEntity.getUserGroupMemberships().isEmpty()) {
+ List<String> clientRoles = new ArrayList<>();
+ List<String> realmRoles = new ArrayList<>();
+ for (UserGroupMembership userGroupMembership:
profileEntity.getUserGroupMemberships()) {
+ for (GroupRole gr:
userGroupMembership.getGroup().getGroupRole()) {
+ if (gr.getType().equals(Constants.ROLE_TYPE_CLIENT)) {
+ clientRoles.add(gr.getValue());
+ } else if (gr.getType().equals(Constants.ROLE_TYPE_REALM))
{
+ realmRoles.add(gr.getValue());
+ }
+ }
+ }
+ builder.addAllClientRoles(clientRoles);
+ builder.addAllRealmRoles(realmRoles);
+ }
+
+ return builder.build();
+ }
/**
* Creates a protobuf UserProfile object from a UserProfileEntity object.
diff --git
a/core/src/main/java/org/apache/custos/core/model/user/UserAttribute.java
b/core/src/main/java/org/apache/custos/core/model/user/UserAttribute.java
index e4ff844c0..18f9b80b5 100644
--- a/core/src/main/java/org/apache/custos/core/model/user/UserAttribute.java
+++ b/core/src/main/java/org/apache/custos/core/model/user/UserAttribute.java
@@ -27,9 +27,15 @@ import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
+import jakarta.persistence.UniqueConstraint;
+
+import java.util.Objects;
@Entity
-@Table(name = "user_attribute")
+@Table(
+ name = "user_attribute",
+ uniqueConstraints = @UniqueConstraint(columnNames = {"keyValue",
"value", "user_profile_id"})
+)
public class UserAttribute {
@Id
@@ -46,6 +52,22 @@ public class UserAttribute {
@JoinColumn(name = "user_profile_id")
private UserProfile userProfile;
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ UserAttribute userRole = (UserAttribute) o;
+ return Objects.equals(id, userRole.id) &&
+ Objects.equals(keyValue, userRole.keyValue) &&
+ Objects.equals(userProfile, userRole.userProfile);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, keyValue, value, userProfile);
+ }
+
+
public Long getId() {
return id;
diff --git
a/core/src/main/java/org/apache/custos/core/model/user/UserProfile.java
b/core/src/main/java/org/apache/custos/core/model/user/UserProfile.java
index ae5389843..610c90850 100644
--- a/core/src/main/java/org/apache/custos/core/model/user/UserProfile.java
+++ b/core/src/main/java/org/apache/custos/core/model/user/UserProfile.java
@@ -35,6 +35,7 @@ import
org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.util.Date;
import java.util.Set;
+import java.util.Objects;
@Entity
@Table(name = "user_profile")
@@ -76,7 +77,6 @@ public class UserProfile {
@Column
private String type;
-
@OneToMany(fetch = FetchType.EAGER, mappedBy = "userProfile",
orphanRemoval = true, cascade = CascadeType.ALL)
private Set<UserRole> userRole;
@@ -93,6 +93,18 @@ public class UserProfile {
@OneToMany(mappedBy = "userProfile", cascade = CascadeType.ALL)
private Set<UserGroupMembership> userGroupMemberships;
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ UserProfile that = (UserProfile) o;
+ return id.equals(that.getId());
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id);
+ }
public String getId() {
return id;
diff --git a/core/src/main/java/org/apache/custos/core/model/user/UserRole.java
b/core/src/main/java/org/apache/custos/core/model/user/UserRole.java
index 3a5fb32e4..b278dc9f2 100644
--- a/core/src/main/java/org/apache/custos/core/model/user/UserRole.java
+++ b/core/src/main/java/org/apache/custos/core/model/user/UserRole.java
@@ -27,9 +27,15 @@ import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
+import jakarta.persistence.UniqueConstraint;
+import java.util.Objects;
@Entity
-@Table(name = "user_role")
+@Table(
+ name = "user_role",
+ uniqueConstraints = @UniqueConstraint(columnNames =
{"user_profile_id", "type", "value"})
+)
+
public class UserRole {
@Id
@@ -77,4 +83,19 @@ public class UserRole {
public void setUserProfile(UserProfile userProfile) {
this.userProfile = userProfile;
}
+
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ UserRole userRole = (UserRole) o;
+ return Objects.equals(userProfile, userRole.userProfile) &&
+ Objects.equals(type, userRole.type) &&
+ Objects.equals(value, userRole.value);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(userProfile, type, value);
+ }
+
}
diff --git
a/core/src/main/java/org/apache/custos/core/repo/user/UserAttributeRepository.java
b/core/src/main/java/org/apache/custos/core/repo/user/UserAttributeRepository.java
index a54cb4437..725363217 100644
---
a/core/src/main/java/org/apache/custos/core/repo/user/UserAttributeRepository.java
+++
b/core/src/main/java/org/apache/custos/core/repo/user/UserAttributeRepository.java
@@ -25,9 +25,12 @@ import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.util.List;
+import java.util.Optional;
public interface UserAttributeRepository extends JpaRepository<UserAttribute,
Long> {
@Query("SELECT DISTINCT atr.userProfile from UserAttribute atr where
atr.keyValue = ?1 and atr.value =?2")
List<UserProfile> findFilteredUserProfiles(String key, String value);
+
+ Optional<UserAttribute>
findUserAttributeByKeyValueAndValueAndUserProfile(String keyValue, String
value, UserProfile userProfile);
}
diff --git
a/core/src/main/java/org/apache/custos/core/repo/user/UserRoleRepository.java
b/core/src/main/java/org/apache/custos/core/repo/user/UserRoleRepository.java
index fd9887f72..dece92d1f 100644
---
a/core/src/main/java/org/apache/custos/core/repo/user/UserRoleRepository.java
+++
b/core/src/main/java/org/apache/custos/core/repo/user/UserRoleRepository.java
@@ -21,6 +21,9 @@ package org.apache.custos.core.repo.user;
import org.apache.custos.core.model.user.UserRole;
import org.springframework.data.jpa.repository.JpaRepository;
+import java.util.Optional;
+import org.apache.custos.core.model.user.UserProfile;
public interface UserRoleRepository extends JpaRepository<UserRole, Long> {
+ Optional<UserRole> findByTypeAndValueAndUserProfile(String type, String
value, UserProfile userProfile);
}
diff --git a/core/src/main/proto/UserProfile.proto
b/core/src/main/proto/UserProfile.proto
index 5cb4429fb..e595c1be3 100644
--- a/core/src/main/proto/UserProfile.proto
+++ b/core/src/main/proto/UserProfile.proto
@@ -109,6 +109,27 @@ message GetUpdateAuditTrailResponse {
repeated UserProfileStatusUpdateMetadata status_audit = 2;
}
+message UsersRolesRequest {
+ repeated string usernames = 1;
+ repeated string roles = 2;
+ string role_type = 3;
+}
+
+message UsersRolesFullRequest {
+ int64 tenant_id = 1;
+ UsersRolesRequest users_roles = 2;
+}
+
+message UserAttributeRequest {
+ string username = 1;
+ repeated UserAttribute attributes = 2;
+}
+
+message UserAttributeFullRequest {
+ int64 tenant_id = 1;
+ UserAttributeRequest userAttributeRequest = 2;
+}
+
message GroupRequest {
int64 tenant_id = 1;
Group group = 2;
diff --git a/custos-portal/src/components/NavContainer.tsx
b/custos-portal/src/components/NavContainer.tsx
index e800485e9..ef8819cb7 100644
--- a/custos-portal/src/components/NavContainer.tsx
+++ b/custos-portal/src/components/NavContainer.tsx
@@ -48,6 +48,7 @@ import { AiOutlineAppstore } from "react-icons/ai";
import { IconType } from "react-icons";
import { MdLogout } from "react-icons/md";
import { useAuth } from "react-oidc-context";
+import { BACKEND_URL } from "../lib/constants";
interface NavContainerProps {
activeTab: string;
@@ -164,6 +165,7 @@ export const NavContainer = memo(
_hover={{ color: "gray.500" }}
onClick={async () => {
await auth.removeUser();
+ await auth.signoutRedirect();
onClose();
}}
>
@@ -248,6 +250,19 @@ export const NavContainer = memo(
size="sm"
_hover={{ color: "gray.500" }}
onClick={async () => {
+ await fetch(
+ `${BACKEND_URL}/api/v1/identity-management/user/logout`,
+ {
+ method: "POST",
+ body: JSON.stringify({
+ refresh_token: auth.user?.refresh_token,
+ }),
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${auth.user?.access_token}`,
+ },
+ }
+ );
await auth.removeUser();
}}
>
diff --git a/custos-portal/src/components/Users/UserSettings.tsx
b/custos-portal/src/components/Users/UserSettings.tsx
index ba768d2ac..913082cd4 100644
--- a/custos-portal/src/components/Users/UserSettings.tsx
+++ b/custos-portal/src/components/Users/UserSettings.tsx
@@ -17,6 +17,8 @@ import {
FormLabel,
IconButton,
Code,
+ Spinner,
+ HStack,
} from "@chakra-ui/react";
import { PageTitle } from "../PageTitle";
import { ActionButton } from "../ActionButton";
@@ -25,42 +27,57 @@ import { FaArrowLeft } from "react-icons/fa6";
import { LeftRightLayout } from "../LeftRightLayout";
import { FiTrash2 } from "react-icons/fi";
import { StackedBorderBox } from "../StackedBorderBox";
-
-const DUMMY_ROLES: any = [
- {
- application: "Grafana",
- role: "grafana:viewer",
- description: "Grafana Viewer",
- },
- {
- application: "Grafana",
- role: "grafana:editor",
- description: "Grafana Editor",
- },
- {
- application: "Grafana",
- role: "grafana:admin",
- description: "Grafana Admin",
- },
-];
-
-const DUMMY_ACTIVITY: any = [
- {
- action: "User Created",
- timestamp: "2021-10-01",
- },
- {
- action: "User Disabled",
- timestamp: "2021-10-01",
- },
- {
- action: "User Enabled",
- timestamp: "2021-10-01",
- },
-];
+import { BACKEND_URL, CLIENT_ID } from "../../lib/constants";
+import { useApi } from "../../hooks/useApi";
+import { isEmpty } from "../../lib/util";
+import { useEffect, useState } from "react";
+import { useAuth } from "react-oidc-context";
export const UserSettings = () => {
const { email } = useParams();
+ const auth = useAuth();
+
+ const [user, setUser] = useState<User | null>(null);
+ const [group, setGroup] = useState<any | null>(null);
+
+ useEffect(() => {
+ async function fetchData() {
+ const userResp = await fetch(
+ `${BACKEND_URL}/api/v1/user-management/user/profile/${email}`,
+ {
+ headers: {
+ Authorization: `Bearer ${auth.user?.access_token}`,
+ },
+ }
+ );
+ const userData = await userResp.json();
+
+ const groupResp = await fetch(
+
`${BACKEND_URL}/api/v1/group-management/users/${email}/group-memberships`,
+ {
+ headers: {
+ client_id: CLIENT_ID,
+ userId: email,
+ Authorization: `Bearer ${auth.user?.access_token}`,
+ },
+ }
+ );
+ const groupData = await groupResp.json();
+
+ setUser(userData);
+ setGroup(groupData);
+ }
+
+ fetchData();
+ }, []);
+
+ if (!user || !group) {
+ return (
+ <NavContainer activeTab="Users">
+ <Spinner />
+ </NavContainer>
+ );
+ }
return (
<>
@@ -76,9 +93,11 @@ export const UserSettings = () => {
<Flex mt={4} justify="space-between">
<Box>
- <PageTitle>John Doe</PageTitle>
+ <PageTitle>
+ {user.first_name} {user.last_name}
+ </PageTitle>
<Text color="default.secondary" mt={2}>
- {email}
+ {user.email}
</Text>
</Box>
<ActionButton icon={FiTrash2} onClick={() => {}}>
@@ -94,19 +113,34 @@ export const UserSettings = () => {
<Stack spacing={4}>
<FormControl color="default.default">
<FormLabel>Name</FormLabel>
- <Input type="text" />
+ <Input
+ type="text"
+ value={user.first_name + " " + user.last_name}
+ />
</FormControl>
<FormControl>
<FormLabel>Email</FormLabel>
- <Input type="text" />
+ <Input type="text" value={user.email} />
</FormControl>
<FormControl>
<FormLabel>Joined</FormLabel>
- <Input type="text" disabled={true} />
+ <Input
+ type="text"
+ disabled={true}
+ value={new Date(
+ parseInt(user.created_at)
+ ).toLocaleString()}
+ />
</FormControl>
<FormControl>
- <FormLabel>Last Signed In</FormLabel>
- <Input type="text" disabled={true} />
+ <FormLabel>Last Modified</FormLabel>
+ <Input
+ type="text"
+ disabled={true}
+ value={new Date(
+ parseInt(user.last_modified_at)
+ ).toLocaleString()}
+ />
</FormControl>
</Stack>
</>
@@ -114,7 +148,7 @@ export const UserSettings = () => {
/>
<Box>
- <Text fontSize="lg">Groups</Text>
+ <Text fontSize="lg">Group Memberships</Text>
<TableContainer mt={4}>
<Table variant="simple">
<Thead>
@@ -148,46 +182,51 @@ export const UserSettings = () => {
</Box>
<Box>
- <Text fontSize="lg">Roles</Text>
- <Text mt={2} color="gray.600">
- Through their group memberships, this user has the following
roles
- </Text>
+ <Text fontSize="lg">Client Roles</Text>
+ {!isEmpty(user.client_roles) ? (
+ <>
+ <Text mt={2} color="gray.600">
+ Through their group memberships, this user has the following
+ client roles
+ </Text>
- <TableContainer mt={4}>
- <Table variant="simple">
- <Thead>
- <Tr>
- <Th>Application</Th>
- <Th>Role</Th>
- <Th>Description</Th>
- </Tr>
- </Thead>
- <Tbody>
- {DUMMY_ROLES.map((role: any) => (
- <Tr key={role.role}>
- <Td>{role.application}</Td>
- <Td>
- <Code>{role.role}</Code>
- </Td>
- <Td>{role.description}</Td>
- </Tr>
+ <HStack mt={2}>
+ {user.client_roles?.map((role: string) => (
+ <Code key={role} size="lg">
+ {role}
+ </Code>
))}
- </Tbody>
- </Table>
- </TableContainer>
+ </HStack>
+ </>
+ ) : (
+ <Text mt={2} color="gray.600">
+ This user has no client roles
+ </Text>
+ )}
</Box>
<Box>
- <Text fontSize="lg">Activity</Text>
- {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- DUMMY_ACTIVITY.map((activity: any) => (
- <Flex key={activity.action} gap={4} mt={4}>
- <Text color="gray.400">{activity.timestamp}</Text>
- <Text fontWeight="bold">{activity.action}</Text>
- </Flex>
- ))
- }
+ <Text fontSize="lg">Realm Roles</Text>
+ {!isEmpty(user.realm_roles) ? (
+ <>
+ <Text mt={2} color="gray.600">
+ Through their group memberships, this user has the following
+ realm roles
+ </Text>
+
+ <HStack mt={2}>
+ {user.client_roles?.map((role: string) => (
+ <Code key={role} size="lg">
+ {role}
+ </Code>
+ ))}
+ </HStack>
+ </>
+ ) : (
+ <Text mt={2} color="gray.600">
+ This user has no realm roles
+ </Text>
+ )}
</Box>
</StackedBorderBox>
</NavContainer>
diff --git a/custos-portal/src/components/Users/index.tsx
b/custos-portal/src/components/Users/index.tsx
index 6715c5bb9..c4fc8c851 100644
--- a/custos-portal/src/components/Users/index.tsx
+++ b/custos-portal/src/components/Users/index.tsx
@@ -14,35 +14,30 @@ import {
Th,
Td,
Tbody,
+ Spinner,
} from "@chakra-ui/react";
import { PageTitle } from "../PageTitle";
import { ActionButton } from "../ActionButton";
import { CiSearch } from "react-icons/ci";
import { User } from "../../interfaces/Users";
import { Link } from "react-router-dom";
-
-const DUMMY_DATA: User[] = [
- {
- name: "Stella Zhou",
- email: "[email protected]",
- joined: "2021-10-01",
- lastSignedIn: "2021-10-01",
- },
- {
- name: "John Doe",
- email: "[email protected]",
- joined: "2021-10-01",
- lastSignedIn: "2021-10-01",
- },
- {
- name: "Jane Doe",
- email: "[email protected]",
- joined: "2021-10-01",
- lastSignedIn: "2021-10-01",
- },
-];
+import { useApi } from "../../hooks/useApi";
+import { BACKEND_URL } from "../../lib/constants";
+import { timeAgo } from "../../lib/util";
export const Users = () => {
+ const offset = 0;
+ const limit = 10;
+
+
+ const urlSearchParams = new URLSearchParams();
+ urlSearchParams.append("offset", offset.toString());
+ urlSearchParams.append("limit", limit.toString());
+
+ const allUsers = useApi(
+
`${BACKEND_URL}/api/v1/user-management/users/profile?${urlSearchParams.toString()}`
+ );
+
return (
<>
<NavContainer activeTab="Users">
@@ -72,45 +67,57 @@ export const Users = () => {
</InputGroup>
{/* TABLE */}
- <TableContainer mt={4}>
- <Table variant="simple">
- <Thead>
- <Tr>
- <Th>Name</Th>
- <Th>Email</Th>
- <Th>Joined</Th>
- <Th>Last Signed In</Th>
- <Th>Actions</Th>
- </Tr>
- </Thead>
-
- <Tbody>
- {DUMMY_DATA.map((user) => (
- <Tr key={user.email}>
- <Td>
- <Link to={`/users/${user.email}`}>
- <Text
- color="blue.400"
- _hover={{
- color: "blue.600",
- cursor: "pointer",
- }}
- >
- {user.name}
- </Text>
- </Link>
- </Td>
- <Td>{user.email}</Td>
- <Td>{user.joined}</Td>
- <Td>{user.lastSignedIn}</Td>
- <Td>
- <ActionButton onClick={() => {}}>Disable</ActionButton>
- </Td>
+ {allUsers?.isPending ? (
+ <Box textAlign="center" mt={4}>
+ <Spinner />
+ </Box>
+ ) : (
+ <TableContainer mt={4}>
+ <Table variant="simple">
+ <Thead>
+ <Tr>
+ <Th>Name</Th>
+ <Th>Email</Th>
+ <Th>Joined</Th>
+ <Th>Last Modified</Th>
+ <Th>Actions</Th>
</Tr>
- ))}
- </Tbody>
- </Table>
- </TableContainer>
+ </Thead>
+
+ <Tbody>
+ {allUsers?.data?.profiles?.map((user: User) => (
+ <Tr key={user.email}>
+ <Td>
+ <Link to={`/users/${user.email}`}>
+ <Text
+ color="blue.400"
+ _hover={{
+ color: "blue.600",
+ cursor: "pointer",
+ }}
+ >
+ {user.first_name} {user.last_name}
+ </Text>
+ </Link>
+ </Td>
+ <Td>{user.email}</Td>
+ <Td>
+ {new Date(parseInt(user.created_at)).toDateString()}
+ </Td>
+ <Td>
+ {user.last_modified_at
+ ? timeAgo(new Date(parseInt(user.last_modified_at)))
+ : "N/A"}
+ </Td>
+ <Td>
+ <ActionButton onClick={() => {}}>Disable</ActionButton>
+ </Td>
+ </Tr>
+ ))}
+ </Tbody>
+ </Table>
+ </TableContainer>
+ )}
</NavContainer>
</>
);
diff --git a/custos-portal/src/hooks/useApi.tsx
b/custos-portal/src/hooks/useApi.tsx
index 3acdeb30b..4842ffbcf 100644
--- a/custos-portal/src/hooks/useApi.tsx
+++ b/custos-portal/src/hooks/useApi.tsx
@@ -22,13 +22,20 @@ import { useAuth } from "react-oidc-context";
export const useApi = (url: string, options?: RequestInit) => {
const auth = useAuth();
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
const [data, setData] = useState<any | null>(null);
const [isPending, setIsPending] = useState(false);
const [error, setError] = useState("");
+
useEffect(() => {
+ if (!url) return; // Avoid calling fetch if the URL is empty
+
+ const controller = new AbortController();
+ const signal = controller.signal;
+
const fetchData = async () => {
setIsPending(true);
+ setError(""); // Reset error before fetching
+
try {
const response = await fetch(url, {
...options,
@@ -36,18 +43,30 @@ export const useApi = (url: string, options?: RequestInit)
=> {
...options?.headers,
Authorization: `Bearer ${auth.user?.access_token}`,
},
+ signal, // Pass the signal to fetch
});
+
if (!response.ok) throw new Error(response.statusText);
const json = await response.json();
- setIsPending(false);
- setData(json);
- setError("");
- } catch (error) {
- setError(`${error} Could not Fetch Data.`);
- setIsPending(false);
+
+ if (!signal.aborted) {
+ setData(json);
+ }
+ } catch (err) {
+ if (!signal.aborted) {
+ setError(`Error: ${err.message} - Could not fetch data.`);
+ }
+ } finally {
+ if (!signal.aborted) {
+ setIsPending(false);
+ }
}
};
+
fetchData();
+
+ return () => controller.abort(); // Cleanup on unmount or dependency change
}, [auth.user?.access_token, options, url]);
+
return { data, isPending, error };
};
diff --git a/custos-portal/src/index.tsx b/custos-portal/src/index.tsx
index b4e855719..88f07776a 100644
--- a/custos-portal/src/index.tsx
+++ b/custos-portal/src/index.tsx
@@ -53,11 +53,11 @@ const Index = () => {
useEffect(() => {
const fetchOidcConfig = async () => {
try {
- let data;
const response = await fetch(
`${BACKEND_URL}/api/v1/identity-management/tenant/${TENANT_ID}/.well-known/openid-configuration`
); // Replace with actual API endpoint
- data = await response.json();
+ const data = await response.json();
+
const redirectUri = APP_REDIRECT_URI;
const theConfig: AuthProviderProps = {
diff --git a/custos-portal/src/interfaces/Users.tsx
b/custos-portal/src/interfaces/Users.tsx
index 83bf47471..ec587e56a 100644
--- a/custos-portal/src/interfaces/Users.tsx
+++ b/custos-portal/src/interfaces/Users.tsx
@@ -1,6 +1,10 @@
export interface User {
- name: string;
+ username: string;
email: string;
- joined: string;
- lastSignedIn: string;
+ first_name: string;
+ last_name: string;
+ created_at: string;
+ client_roles: string[];
+ realm_roles: string[];
+ last_modified_at: string;
}
diff --git a/custos-portal/src/lib/constants.ts
b/custos-portal/src/lib/constants.ts
index 652cc4cd5..a61907dc1 100644
--- a/custos-portal/src/lib/constants.ts
+++ b/custos-portal/src/lib/constants.ts
@@ -20,7 +20,7 @@
import packageJson from '../../package.json';
export const PORTAL_VERSION = packageJson.version;
-export const CLIENT_ID = 'custos-gcq8jxkwpvs2gcudzmfn-10000000';;
+export const CLIENT_ID = 'custos-2o7lfqdfsfxhjdp6qmxi-10000000';
export const BACKEND_URL = 'http://localhost:8081';
export const APP_URL = "http://localhost:5173";
diff --git a/custos-portal/src/lib/util.ts b/custos-portal/src/lib/util.ts
index 4340d1d3d..d3480cba1 100644
--- a/custos-portal/src/lib/util.ts
+++ b/custos-portal/src/lib/util.ts
@@ -21,3 +21,41 @@ export const decodeToken = (token: string | undefined) => {
if (!token) return null;
return JSON.parse(atob(token.split('.')[1]));
};
+
+export function timeAgo(date: number) {
+ const now = new Date();
+ const seconds = Math.floor((now - date) / 1000);
+
+ if (seconds < 60) {
+ return `${seconds} second${seconds !== 1 ? "s" : ""} ago`;
+ }
+
+ const minutes = Math.floor(seconds / 60);
+ if (minutes < 60) {
+ return `${minutes} minute${minutes !== 1 ? "s" : ""} ago`;
+ }
+
+ const hours = Math.floor(minutes / 60);
+ if (hours < 24) {
+ return `${hours} hour${hours !== 1 ? "s" : ""} ago`;
+ }
+
+ const days = Math.floor(hours / 24);
+ if (days < 30) {
+ return `${days} day${days !== 1 ? "s" : ""} ago`;
+ }
+
+ const months = Math.floor(days / 30);
+ if (months < 12) {
+ return `${months} month${months !== 1 ? "s" : ""} ago`;
+ }
+
+ const years = Math.floor(months / 12);
+ return `${years} year${years !== 1 ? "s" : ""} ago`;
+}
+
+export function isEmpty(arr: unknown[]) {
+ if (!arr) return true;
+
+ return arr.length == 0;
+}
\ No newline at end of file
diff --git
a/services/src/main/java/org/apache/custos/service/credential/store/CredentialStoreService.java
b/services/src/main/java/org/apache/custos/service/credential/store/CredentialStoreService.java
index 61b8f5ff9..8157455fb 100644
---
a/services/src/main/java/org/apache/custos/service/credential/store/CredentialStoreService.java
+++
b/services/src/main/java/org/apache/custos/service/credential/store/CredentialStoreService.java
@@ -405,7 +405,7 @@ public class CredentialStoreService {
public GetAllCredentialsResponse getAllCredentialsFromToken(TokenRequest
request) {
try {
String token = request.getToken();
- Credential credential = credentialManager.decodeToken(token);
+ Credential credential = credentialManager.decodeJWTToken(token);
if (credential == null || credential.getId() == null) {
LOGGER.error("Invalid access token");
@@ -420,16 +420,6 @@ public class CredentialStoreService {
}
String subPath = BASE_PATH + entity.getOwnerId();
-
- String validatingPath = BASE_PATH + entity.getOwnerId() + "/" +
Type.CUSTOS.name();
- VaultResponseSupport<Credential> validationResponse =
vaultTemplate.read(validatingPath, Credential.class);
-
- if (validationResponse == null || validationResponse.getData() ==
null ||
!validationResponse.getData().getSecret().equals(credential.getSecret())) {
- String msg = "Invalid secret for Id: " + credential.getId();
- LOGGER.error(msg);
- throw new AuthenticationException(msg);
- }
-
List<String> paths = vaultTemplate.list(subPath);
List<CredentialMetadata> credentialMetadata = new ArrayList<>();
diff --git
a/services/src/main/java/org/apache/custos/service/management/UserManagementService.java
b/services/src/main/java/org/apache/custos/service/management/UserManagementService.java
index 2dca61c6d..1099482ab 100644
---
a/services/src/main/java/org/apache/custos/service/management/UserManagementService.java
+++
b/services/src/main/java/org/apache/custos/service/management/UserManagementService.java
@@ -24,13 +24,10 @@ import com.nimbusds.jwt.SignedJWT;
import org.apache.custos.core.constants.Constants;
import org.apache.custos.core.iam.api.AddExternalIDPLinksRequest;
import org.apache.custos.core.iam.api.AddUserAttributesRequest;
-import org.apache.custos.core.iam.api.AddUserRolesRequest;
import org.apache.custos.core.iam.api.CheckingResponse;
import org.apache.custos.core.iam.api.DeleteExternalIDPsRequest;
import org.apache.custos.core.iam.api.DeleteUserAttributeRequest;
import org.apache.custos.core.iam.api.DeleteUserRolesRequest;
-import org.apache.custos.core.iam.api.FindUsersRequest;
-import org.apache.custos.core.iam.api.FindUsersResponse;
import org.apache.custos.core.iam.api.GetAllResources;
import org.apache.custos.core.iam.api.GetAllResourcesResponse;
import org.apache.custos.core.iam.api.GetExternalIDPsRequest;
@@ -57,6 +54,9 @@ import
org.apache.custos.core.user.profile.api.GetUpdateAuditTrailRequest;
import org.apache.custos.core.user.profile.api.GetUpdateAuditTrailResponse;
import org.apache.custos.core.user.profile.api.UserProfile;
import org.apache.custos.core.user.profile.api.UserStatus;
+import org.apache.custos.core.user.profile.api.UsersRolesFullRequest;
+import org.apache.custos.core.user.profile.api.Status;
+import org.apache.custos.core.user.profile.api.UserAttributeFullRequest;
import org.apache.custos.service.exceptions.AuthenticationException;
import org.apache.custos.service.exceptions.InternalServerException;
import org.apache.custos.service.iam.IamAdminService;
@@ -516,49 +516,6 @@ public class UserManagementService {
}
}
- /**
- * Finds users based on the provided search request.
- *
- * @param request the request object containing the user search criteria
- * @return the response object containing the found users
- * @throws AuthenticationException if an authentication exception occurs
- * @throws InternalServerException if an internal server exception occurs
- */
- public FindUsersResponse findUsers(FindUsersRequest request) {
- try {
- long initiationTime = System.currentTimeMillis();
- GetUserManagementSATokenRequest userManagementSATokenRequest =
GetUserManagementSATokenRequest.newBuilder()
- .setClientId(request.getClientId())
- .setClientSecret(request.getClientSec())
- .setTenantId(request.getTenantId())
- .build();
- AuthToken token =
identityService.getUserManagementServiceAccountAccessToken(userManagementSATokenRequest);
-
- if (token != null && token.getAccessToken() != null) {
-
- request =
request.toBuilder().setAccessToken(token.getAccessToken()).build();
- FindUsersResponse user = iamAdminService.findUsers(request);
- long endTime = System.currentTimeMillis();
- long total = endTime - initiationTime;
- LOGGER.debug("request received: " + initiationTime + " request
end time" + endTime + " difference " + total);
- return user;
-
- } else {
- LOGGER.error("Cannot find service token");
- throw new RuntimeException("Cannot find service token");
- }
-
- } catch (Exception ex) {
- String msg = "Error occurred while pulling users, " +
ex.getMessage();
- LOGGER.error(msg);
- if (ex.getMessage().contains("UNAUTHENTICATED")) {
- throw new AuthenticationException(msg, ex);
- } else {
- throw new InternalServerException(msg, ex);
- }
- }
- }
-
/**
* Resets the password for a user.
*
@@ -592,61 +549,61 @@ public class UserManagementService {
}
}
- /**
- * Adds roles to users based on the provided request.
- *
- * @param request the request object containing the information to add
roles to users
- * @return the operation status indicating the result of adding roles to
users
- * @throws AuthenticationException if an authentication exception occurs
- * @throws InternalServerException if an internal server exception occurs
- */
- public OperationStatus addRolesToUsers(AddUserRolesRequest request) {
+ public Status addAttributesToUser(UserAttributeFullRequest request) {
try {
- OperationStatus response =
iamAdminService.addRolesToUsers(request);
-
- for (String user : request.getUsernamesList()) {
- UserSearchMetadata metadata = UserSearchMetadata
- .newBuilder()
- .setUsername(user).build();
-
- UserSearchRequest searchRequest = UserSearchRequest
- .newBuilder()
- .setClientId(request.getClientId())
- .setTenantId(request.getTenantId())
- .setAccessToken(request.getAccessToken())
- .setUser(metadata)
- .build();
-
- UserRepresentation representation =
iamAdminService.getUser(searchRequest);
+ userProfileService.addUserAttributes(request);
+ return Status.newBuilder().setStatus(true).build();
+ } catch(Exception ex) {
+ String msg = "Error occurred while adding attributes: " +
ex.getMessage();
+ LOGGER.error(msg);
+ throw new InternalServerException(msg, ex);
+ }
+ }
- if (representation != null) {
- UserProfile profile =
this.convertToProfile(representation);
+ public Status deleteAttributesFromUser(UserAttributeFullRequest request) {
+ try {
+ userProfileService.deleteUserAttributes(request);
+ return Status.newBuilder().setStatus(true).build();
+ } catch(Exception ex) {
+ String msg = "Error occurred while adding attributes: " +
ex.getMessage();
+ LOGGER.error(msg);
+ throw new InternalServerException(msg, ex);
+ }
+ }
- org.apache.custos.core.user.profile.api.UserProfileRequest
req = org.apache.custos.core.user.profile.api.UserProfileRequest.newBuilder()
- .setTenantId(request.getTenantId())
- .setProfile(profile)
- .build();
+ public Status addRolesToUsers(UsersRolesFullRequest request) {
+ try {
+ String[] usernames =
request.getUsersRoles().getUsernamesList().toArray(new String[0]);
+ String[] roles =
request.getUsersRoles().getRolesList().toArray(new String[0]);
+ for (String username : usernames) {
+ for (String role: roles) {
+ userProfileService.addUserRole(username, role,
request.getUsersRoles().getRoleType(), request.getTenantId());
+ }
+ }
+ return Status.newBuilder().setStatus(true).build();
+ } catch(Exception ex) {
+ String msg = "Error occurred while adding roles: " +
ex.getMessage();
+ LOGGER.error(msg);
+ throw new InternalServerException(msg, ex);
+ }
+ }
- UserProfile existingUser =
userProfileService.getUserProfile(req);
+ public Status deleteRolesFromUsers(UsersRolesFullRequest request) {
+ try {
+ String[] usernames =
request.getUsersRoles().getUsernamesList().toArray(new String[0]);
+ String[] roles =
request.getUsersRoles().getRolesList().toArray(new String[0]);
- if (existingUser == null ||
StringUtils.isBlank(existingUser.getUsername())) {
- userProfileService.createUserProfile(req);
- } else {
- userProfileService.updateUserProfile(req);
- }
+ for (String username : usernames) {
+ for (String role: roles) {
+ userProfileService.deleteUserRole(username, role,
request.getUsersRoles().getRoleType(), request.getTenantId());
}
}
- return response;
-
- } catch (Exception ex) {
- String msg = "Error occurred while adding roles to users, " +
ex.getMessage();
+ return Status.newBuilder().setStatus(true).build();
+ } catch(Exception ex) {
+ String msg = "Error occurred while adding deleting roles: " +
ex.getMessage();
LOGGER.error(msg);
- if (ex.getMessage().contains("UNAUTHENTICATED")) {
- throw new AuthenticationException(msg, ex);
- } else {
- throw new InternalServerException(msg, ex);
- }
+ throw new InternalServerException(msg, ex);
}
}
@@ -763,7 +720,7 @@ public class UserManagementService {
}
/**
- * Updates the user profile for the given user.
+ * Updates the user profile (in tenant and keycloak) for the given user.
*
* @param request The user profile request containing the updated user
profile information.
* @return The updated user profile.
@@ -785,7 +742,6 @@ public class UserManagementService {
CheckingResponse response = iamAdminService.isUserExist(info);
-
if (!response.getIsExist()) {
String msg = "User not found with username " +
request.getUserProfile().getUsername();
LOGGER.error(msg);
@@ -798,7 +754,6 @@ public class UserManagementService {
.toBuilder()
.setFirstName(request.getUserProfile().getFirstName())
.setLastName(request.getUserProfile().getLastName())
- .setEmail(request.getUserProfile().getEmail())
.build();
UpdateUserProfileRequest updateUserProfileRequest =
UpdateUserProfileRequest
@@ -822,25 +777,20 @@ public class UserManagementService {
if (profile != null &&
StringUtils.isNotBlank(profile.getUsername())) {
profile = profile.toBuilder()
- .setEmail(request.getUserProfile().getEmail())
.setFirstName(request.getUserProfile().getFirstName())
.setLastName(request.getUserProfile().getLastName())
-
.setUsername(request.getUserProfile().getUsername())
.build();
userProfileRequest =
userProfileRequest.toBuilder().setProfile(profile).build();
userProfileService.updateUserProfile(userProfileRequest);
- return profile;
-
+ return
userProfileService.getFullUserProfile(userProfileRequest);
} else {
UserProfile userProfile = UserProfile.newBuilder()
- .setEmail(request.getUserProfile().getEmail())
.setFirstName(request.getUserProfile().getFirstName())
.setLastName(request.getUserProfile().getLastName())
-
.setUsername(request.getUserProfile().getUsername())
.build();
userProfileRequest =
userProfileRequest.toBuilder().setProfile(userProfile).build();
userProfileService.createUserProfile(userProfileRequest);
- return profile;
+ return
userProfileService.getFullUserProfile(userProfileRequest);
}
} catch (Exception ex) {
@@ -911,7 +861,7 @@ public class UserManagementService {
}
/**
- * Retrieves the user profile based on the provided request.
+ * Retrieves the full user profile based on the provided request
(including inherited group roles)
*
* @param request the request object containing the user profile search
criteria
* @return the user profile object corresponding to the retrieved profile
@@ -927,7 +877,7 @@ public class UserManagementService {
.setTenantId(request.getTenantId())
.build();
- return userProfileService.getUserProfile(userProfileRequest);
+ return userProfileService.getFullUserProfile(userProfileRequest);
} catch (Exception ex) {
String msg = "Error occurred while pulling user profile " +
ex.getMessage();
@@ -949,15 +899,17 @@ public class UserManagementService {
org.apache.custos.core.user.profile.api.UserProfileRequest
userProfileRequest = org.apache.custos.core.user.profile.api.UserProfileRequest
.newBuilder()
- .setProfile(request.getUserProfile())
+// .setProfile(request.getUserProfile())
.setTenantId(request.getTenantId())
.setOffset(request.getOffset())
.setLimit(request.getLimit())
.build();
- return request.getUserProfile().getAttributesList().isEmpty()
- ?
userProfileService.getAllUserProfilesInTenant(userProfileRequest)
- :
userProfileService.findUserProfilesByAttributes(userProfileRequest);
+ return
userProfileService.getAllUserProfilesInTenant(userProfileRequest);
+
+// return request.getUserProfile().getAttributesList().isEmpty()
+// ?
userProfileService.getAllUserProfilesInTenant(userProfileRequest)
+// :
userProfileService.findUserProfilesByAttributes(userProfileRequest);
} catch (Exception ex) {
String msg = "Error occurred while pulling all user profiles in
tenant " + ex.getMessage();
diff --git
a/services/src/main/java/org/apache/custos/service/profile/UserProfileService.java
b/services/src/main/java/org/apache/custos/service/profile/UserProfileService.java
index 83ff2b51a..758f9e609 100644
---
a/services/src/main/java/org/apache/custos/service/profile/UserProfileService.java
+++
b/services/src/main/java/org/apache/custos/service/profile/UserProfileService.java
@@ -52,6 +52,7 @@ import
org.apache.custos.core.user.profile.api.GroupMembership;
import org.apache.custos.core.user.profile.api.GroupRequest;
import org.apache.custos.core.user.profile.api.Status;
import org.apache.custos.core.user.profile.api.UserAttribute;
+import org.apache.custos.core.user.profile.api.UserAttributeFullRequest;
import org.apache.custos.core.user.profile.api.UserGroupMembershipTypeRequest;
import
org.apache.custos.core.user.profile.api.UserProfileAttributeUpdateMetadata;
import org.apache.custos.core.user.profile.api.UserProfileRequest;
@@ -113,6 +114,126 @@ public class UserProfileService {
@Autowired
private GroupMembershipTypeRepository groupMembershipTypeRepository;
+ public void addUserAttributes(UserAttributeFullRequest request) {
+ String userId = request.getUserAttributeRequest().getUsername() + "@"
+ request.getTenantId();
+
+ Optional<UserProfile> opProfile = repository.findById(userId);
+ if (opProfile.isEmpty()) {
+ throw new EntityNotFoundException("Could not find the UserProfile
with the id: " + userId);
+ }
+ UserProfile profileEntity = opProfile.get();
+
+ for (UserAttribute userAttribute :
request.getUserAttributeRequest().getAttributesList()) {
+ for (String value : userAttribute.getValuesList()) {
+ org.apache.custos.core.model.user.UserAttribute
userAttributeEntity = new org.apache.custos.core.model.user.UserAttribute();
+ userAttributeEntity.setUserProfile(profileEntity);
+ userAttributeEntity.setKey(userAttribute.getKey());
+ userAttributeEntity.setValue(value);
+
+ userAttributeRepository.save(userAttributeEntity);
+ }
+ }
+ }
+
+ public void deleteUserAttributes(UserAttributeFullRequest request) {
+ String userId = request.getUserAttributeRequest().getUsername() + "@"
+ request.getTenantId();
+
+ Optional<UserProfile> opProfile = repository.findById(userId);
+ if (opProfile.isEmpty()) {
+ throw new EntityNotFoundException("Could not find the UserProfile
with the id: " + userId);
+ }
+ UserProfile profileEntity = opProfile.get();
+
+ for (UserAttribute userAttribute :
request.getUserAttributeRequest().getAttributesList()) {
+ for (String value : userAttribute.getValuesList()) {
+
+ Optional<org.apache.custos.core.model.user.UserAttribute>
opUserAttribute =
userAttributeRepository.findUserAttributeByKeyValueAndValueAndUserProfile(
+ userAttribute.getKey(),
+ value,
+ profileEntity
+ );
+
+ if (opUserAttribute.isEmpty()) {
+ throw new EntityNotFoundException("Could not find the user
attribute with key " + userAttribute.getKey() + " and value " + value + " for
user " + userId);
+ }
+
+ profileEntity.getUserAttribute().remove(opUserAttribute.get());
+
+ repository.save(profileEntity); // cascade deletion
+ }
+ }
+
+
+ }
+
+ private boolean isValidRoleType(String type) {
+ return
(type.equals(org.apache.custos.core.constants.Constants.ROLE_TYPE_CLIENT) ||
type.equals(org.apache.custos.core.constants.Constants.ROLE_TYPE_REALM));
+ }
+
+ public boolean addUserRole(String username, String role, String type, long
tenantId) {
+ try {
+ if (!isValidRoleType(type)) {
+ throw new IllegalArgumentException("Role type must be `client`
or `realm`");
+ }
+
+ String userId = username + "@" + tenantId;
+
+ Optional<UserProfile> op = repository.findById(userId);
+
+ if (op.isEmpty()) {
+ throw new EntityNotFoundException("Could not find the
UserProfile with the id: " + userId);
+ }
+
+ UserProfile userProfile = op.get();
+
+ org.apache.custos.core.model.user.UserRole userRole = new
org.apache.custos.core.model.user.UserRole();
+ userRole.setType(type);
+ userRole.setValue(role);
+ userRole.setUserProfile(userProfile);
+
+ roleRepository.save(userRole);
+
+ return true;
+ } catch(Exception ex) {
+ String msg = "Error occurred while adding role " + role + " for "
+ username + " reason: " + ex.getMessage();
+ LOGGER.error(msg);
+ throw new RuntimeException(msg, ex);
+ }
+ }
+
+ public boolean deleteUserRole(String username, String role, String type,
long tenantId) {
+ try {
+ if (!isValidRoleType(type)) {
+ throw new IllegalArgumentException("Role type must be `client`
or `realm`");
+ }
+
+ String userId = username + "@" + tenantId;
+
+ Optional<UserProfile> op = repository.findById(userId);
+
+ if (op.isEmpty()) {
+ throw new EntityNotFoundException("Could not find the
UserProfile with the id: " + userId);
+ }
+
+ UserProfile userProfile = op.get();
+
+ Optional<org.apache.custos.core.model.user.UserRole>
optionalUserRole = roleRepository.findByTypeAndValueAndUserProfile(type, role,
userProfile);
+
+ if (optionalUserRole.isEmpty()) {
+ throw new EntityNotFoundException("User role " + role + "(" +
type + ") not found for user: " + userId + ". To remove roles inherited from
groups, please remove the user from the group.");
+ }
+
+ userProfile.getUserRole().remove(optionalUserRole.get());
+
+ repository.save(userProfile); // trigger orphan removal
+
+ return true;
+ } catch(Exception ex) {
+ String msg = "Error occurred while removing role " + role + " for
" + username + " reason: " + ex.getMessage();
+ LOGGER.error(msg);
+ throw new RuntimeException(msg, ex);
+ }
+ }
public org.apache.custos.core.user.profile.api.UserProfile
createUserProfile(UserProfileRequest request) {
try {
@@ -130,7 +251,6 @@ public class UserProfileService {
}
return request.getProfile();
-
} catch (Exception ex) {
String msg = "Error occurred while creating user profile for " +
request.getProfile().getUsername() + "at "
+ request.getTenantId() + " reason :" + ex.getMessage();
@@ -145,35 +265,19 @@ public class UserProfileService {
String userId = request.getProfile().getUsername() + "@" +
request.getTenantId();
- Optional<UserProfile> exEntity = repository.findById(userId);
-
-
- if (exEntity.isPresent()) {
- UserProfile entity =
UserProfileMapper.createUserProfileEntityFromUserProfile(request.getProfile());
- Set<AttributeUpdateMetadata> metadata =
AttributeUpdateMetadataMapper.
- createAttributeUpdateMetadataEntity(exEntity.get(),
entity, request.getPerformedBy());
-
- entity.setAttributeUpdateMetadata(metadata);
- entity.setId(userId);
- entity.setTenantId(request.getTenantId());
- entity.setCreatedAt(exEntity.get().getCreatedAt());
+ Optional<UserProfile> opExEntity = repository.findById(userId);
- UserProfile exProfile = exEntity.get();
+ if (opExEntity.isPresent()) {
+ UserProfile exEntity = opExEntity.get();
- if (exProfile.getUserAttribute() != null) {
-
userAttributeRepository.deleteAll(exProfile.getUserAttribute());
- }
+ exEntity.setFirstName(request.getProfile().getFirstName());
+ exEntity.setLastName(request.getProfile().getLastName());
- if (exProfile.getUserRole() != null) {
- roleRepository.deleteAll(exProfile.getUserRole());
- }
+ repository.save(exEntity);
- repository.save(entity);
return request.getProfile();
-
} else {
- LOGGER.error("Cannot find a user profile for " + userId);
- throw new EntityNotFoundException("Cannot find a user profile
for " + userId);
+ throw new EntityNotFoundException("Can't find user profile");
}
} catch (Exception ex) {
@@ -184,24 +288,46 @@ public class UserProfileService {
}
}
+ /**
+ * Returns the user profile, not including any inherited group roles
+ * @param request Must specify profile.username
+ * @return The user profile
+ */
public org.apache.custos.core.user.profile.api.UserProfile
getUserProfile(UserProfileRequest request) {
try {
LOGGER.debug("Request received to getUserProfile for " +
request.getProfile().getUsername() + "at " + request.getTenantId());
-
String userId = request.getProfile().getUsername() + "@" +
request.getTenantId();
-
Optional<UserProfile> entity = repository.findById(userId);
- if (entity.isPresent()) {
- UserProfile profileEntity = entity.get();
- return
UserProfileMapper.createUserProfileFromUserProfileEntity(profileEntity, null);
-
- } else {
- return null;
+ if (entity.isEmpty()) {
+ throw new EntityNotFoundException("Could not find the
UserProfile with the id: " + userId);
}
+ return
UserProfileMapper.createUserProfileFromUserProfileEntity(entity.get(), null);
} catch (Exception ex) {
- String msg = "Error occurred while fetching user profile for " +
request.getProfile().getUsername() + "at " + request.getTenantId();
+ String msg = "Error occurred while updating user profile for " +
request.getProfile().getUsername() + " at "
+ + request.getTenantId() + " reason: " + ex.getMessage();
+ LOGGER.error(msg);
+ throw new RuntimeException(msg, ex);
+ }
+ }
+
+ /**
+ * Returns the full user profile, including roles inherited from groups
+ * @param request Must specify profile.username
+ * @return The full user profile
+ */
+ public org.apache.custos.core.user.profile.api.UserProfile
getFullUserProfile(UserProfileRequest request) {
+ try {
+ LOGGER.debug("Request received to getFullUserProfile for " +
request.getProfile().getUsername() + "@ " + request.getTenantId());
+ String userId = request.getProfile().getUsername() + "@" +
request.getTenantId();
+ Optional<UserProfile> entity = repository.findById(userId);
+
+ return
entity.map(UserProfileMapper::createFullUserProfileFromUserProfileEntity)
+ .orElseThrow(() -> new EntityNotFoundException("Could not
find the UserProfile with the id: " + userId));
+ } catch (Exception ex) {
+ String msg = "Error occurred while updating user profile for " +
request.getProfile().getUsername() + " at "
+ + request.getTenantId() + " reason: " + ex.getMessage();
LOGGER.error(msg);
throw new RuntimeException(msg, ex);
}