Skip to content

Commit

Permalink
35071 Handle dev-user mode for the user api
Browse files Browse the repository at this point in the history
Added new interface to allow dev-settings implementation.
Added conditionnal bean on Keycloak based
  • Loading branch information
cgendreau committed Oct 28, 2024
1 parent 5c4fc04 commit 0b04b05
Show file tree
Hide file tree
Showing 9 changed files with 783 additions and 723 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public ResourceList<DinaUserDto> findAll(QuerySpec querySpec) {

@Override
public DinaUserDto findOne(Serializable id, QuerySpec querySpec) {
DinaUserDto fetched = service.findOne(id, DinaUserDto.class);
DinaUserDto fetched = service.findOne(id);
authService.authorizeFindOne(fetched);
return fetched;
}
Expand All @@ -81,7 +81,7 @@ public ResourceList<DinaUserDto> findAll(Collection<Serializable> ids, QuerySpec

// 3. otherwise only the authenticated user.
return filteredQuery.apply(
List.of(service.findOne(user.getInternalIdentifier(), DinaUserDto.class)));
List.of(service.findOne(user.getInternalIdentifier())));
}

@Override
Expand Down
247 changes: 6 additions & 241 deletions src/main/java/ca/gc/aafc/dinauser/api/service/DinaGroupService.java
Original file line number Diff line number Diff line change
@@ -1,255 +1,20 @@
package ca.gc.aafc.dinauser.api.service;

import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import org.apache.commons.lang3.StringUtils;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.resource.GroupResource;
import org.keycloak.admin.client.resource.GroupsResource;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import ca.gc.aafc.dina.security.DinaRole;
import ca.gc.aafc.dinauser.api.dto.DinaGroupDto;
import ca.gc.aafc.dinauser.api.dto.DinaGroupDto.DinaGroupDtoBuilder;
import ca.gc.aafc.dinauser.api.dto.DinaGroupMembershipDto;
import ca.gc.aafc.dinauser.api.dto.DinaUserSummaryDto;

import javax.ws.rs.NotFoundException;
import javax.ws.rs.core.Response;
import lombok.extern.log4j.Log4j2;

import static org.keycloak.admin.client.CreatedResponseUtil.getCreatedId;

@Service
@Log4j2
public class DinaGroupService {

private static final Pattern GROUP_NAME_REGEX = Pattern.compile("[a-z0-9][a-z0-9-]{1,61}[a-z0-9]");
private static final Set<DinaRole> NON_GROUP_BASED_ROLES = EnumSet.of(DinaRole.DINA_ADMIN, DinaRole.READ_ONLY_ADMIN);

private static final String LABEL_ATTR_KEY_PREFIX = "groupLabel-";
private static final String GROUPS_CACHE_NAME = "groups";
private static final int MAX_GROUP_MEMBER_PAGE_SIZE = 100;

@Autowired
private KeycloakClientService keycloakClientService;

@Autowired
private Keycloak keycloakClient;

private RealmResource getRealmResource() {
return keycloakClient.realm(keycloakClientService.getRealm());
}

private GroupsResource getGroupsResource() {
return getRealmResource().groups();
}

private String getStringAttr(final Map<String, List<String>> attrs, final String key) {
final List<String> list = attrs.get(key);
if (list == null || list.isEmpty()) {
log.warn("group has no {}", key);
return null;
}

return list.get(0);
}

private DinaGroupDto convertFromRepresentation(final GroupRepresentation groupRep) {
if (groupRep == null) {
log.error("cannot convert null group");
return null;
}

DinaGroupDtoBuilder builder = DinaGroupDto.builder()
.internalId(groupRep.getId())
.name(groupRep.getName())
.path(groupRep.getPath());

final Map<String, List<String>> attributes = groupRep.getAttributes();

if (attributes == null) {
log.warn("group '{}' has no attribute map", groupRep.getName());
} else {
log.info("Getting labels for group '{}'", groupRep.getName());
for (final String key : attributes.keySet()) {
if (key.startsWith(LABEL_ATTR_KEY_PREFIX)) {
final String languageCode = key.substring(LABEL_ATTR_KEY_PREFIX.length());
final String label = getStringAttr(attributes, key);
log.debug("Label for language {}: {}", languageCode, label);

builder = builder.label(languageCode, label);
}
}
}

return builder.build();
}

@Cacheable(cacheNames = GROUPS_CACHE_NAME)
public List<DinaGroupDto> getGroups() {
return getGroups(null, null);
}

@Cacheable(cacheNames = GROUPS_CACHE_NAME)
public List<DinaGroupDto> getGroups(final Integer firstResult, final Integer maxResults) {
log.debug("getting raw group list from {} ({} max)", firstResult, maxResults);
final List<GroupRepresentation> rawGroups = getGroupsResource().groups(null, firstResult, maxResults, false);

log.debug("converting groups");
final List<DinaGroupDto> cookedGroups = rawGroups
.stream()
.map(this::convertFromRepresentation)
.collect(Collectors.toList());

log.debug("done converting groups; returning");
return cookedGroups;
}

@Cacheable(cacheNames = GROUPS_CACHE_NAME)
public DinaGroupDto getGroup(final String id) {
log.debug("getting group {}", id);

final GroupResource groupRes = getGroupsResource().group(id);

if (groupRes == null) {
log.error("No group with id {}", id);
return null;
}

final GroupRepresentation groupRep = groupRes.toRepresentation();

if (groupRep == null) {
log.error("Group with id {} has no representation", id);
return null;
}

return convertFromRepresentation(groupRep);
}

/**
* Get the group membership based on the groupName.
* Only base groups are supported, no subgroups.
* @param identifier the uuid or the name of the group
* @return DinaGroupMembershipDto or null if not found
*/
public DinaGroupMembershipDto getGroupMembership(String identifier) {

String groupName;
// Check if it's a UUID first
if (DinaUserService.UUID_REGEX.matcher(identifier).matches()) {
GroupResource groupResFromUUID = getGroupsResource().group(identifier);
groupName = groupResFromUUID.toRepresentation().getName();
} else {
groupName = identifier;
}

// We only handle base group so, we need to control the path
String subgroupNameForRole =
StringUtils.remove(groupName, "/") + "/" + DinaRole.SUPER_USER.getKeycloakRoleName();

GroupRepresentation subgroupForRole;
try {
subgroupForRole = getRealmResource().getGroupByPath(subgroupNameForRole);
} catch (NotFoundException nfEx) {
log.error("No subgroupForRole with id path {}", subgroupNameForRole);
return null;
}

GroupResource groupRes = getGroupsResource().group(subgroupForRole.getId());

List<UserRepresentation> members = groupRes.members(0, MAX_GROUP_MEMBER_PAGE_SIZE);
if (members.size() == MAX_GROUP_MEMBER_PAGE_SIZE) {
log.error("Max page size number for group {}", subgroupNameForRole);
return null;
}

List<DinaUserSummaryDto> managedBy = members.stream().map(
ur ->
DinaUserSummaryDto.builder()
.username(ur.getUsername())
.agentId(DinaUserService.getAgentId(ur))
.build()).toList();

return DinaGroupMembershipDto.builder()
.name(groupName)
.managedBy(managedBy)
.build();
}

/**
* Create a new group within the realm.
* @param groupDto
* @return
*/
@CacheEvict(cacheNames = GROUPS_CACHE_NAME, allEntries = true)
public DinaGroupDto createGroup(DinaGroupDto groupDto) {

if (!GROUP_NAME_REGEX.matcher(groupDto.getName()).matches()) {
throw new IllegalArgumentException("Invalid name");
}

GroupRepresentation newGroup = new GroupRepresentation();
newGroup.setName(groupDto.getName());

// handle group labels
for (var entry : groupDto.getLabels().entrySet()) {
if (entry.getKey().length() == 2) {
newGroup.singleAttribute(LABEL_ATTR_KEY_PREFIX + entry.getKey(), entry.getValue());
}
}

try (Response response = getGroupsResource().add(newGroup)) {
if (!isSuccessful(response)) {
log.error("Failed to create group {}. Returned code {}", groupDto.getName(), response.getStatusInfo().getStatusCode());
throw new IllegalStateException(response.getStatusInfo().getReasonPhrase());
}
String groupId = getCreatedId(response);
newGroup.setId(groupId);
}
public interface DinaGroupService {

GroupResource grpResource = getGroupsResource().group(newGroup.getId());
List<DinaGroupDto> getGroups();

// create all the subgroups per role
for (DinaRole dr : DinaRole.values()) {
// Skip non group-based roles
if (!NON_GROUP_BASED_ROLES.contains(dr)) {
createDinaSubGroup(grpResource, dr);
}
}
return convertFromRepresentation(grpResource.toRepresentation());
}
List<DinaGroupDto> getGroups(Integer firstResult, Integer maxResults);

/**
* Create a Keycloak subgroup based on DinaRole.
*
* @param groupResource
* @param role
*/
private void createDinaSubGroup(GroupResource groupResource, DinaRole role) {
GroupRepresentation subGroup = new GroupRepresentation();
subGroup.setName(role.getKeycloakRoleName());
DinaGroupDto getGroup(String id);

try (Response response = groupResource.subGroup(subGroup)) {
String groupId = getCreatedId(response);
subGroup.setId(groupId);
log.debug("Created Subgroup : " + role.getKeycloakRoleName());
}
}
DinaGroupDto createGroup(DinaGroupDto groupDto);

private static boolean isSuccessful(Response response) {
return response.getStatusInfo().getFamily() == Response.Status.Family.SUCCESSFUL;
}
DinaGroupMembershipDto getGroupMembership(String identifier);

}
Loading

0 comments on commit 0b04b05

Please sign in to comment.