diff --git a/pom.xml b/pom.xml index c46d4a5..c42d3f8 100644 --- a/pom.xml +++ b/pom.xml @@ -23,6 +23,10 @@ onecx-tenant + + org.tkit.quarkus.lib + tkit-quarkus-data-import + org.tkit.quarkus.lib tkit-quarkus-rest-context @@ -105,6 +109,10 @@ io.quarkus quarkus-opentelemetry + + io.quarkus + quarkus-oidc + @@ -137,6 +145,11 @@ tkit-quarkus-test-db-import test + + io.quarkus + quarkus-test-keycloak-server + test + @@ -170,6 +183,19 @@ + + di-v1 + + generate + + + src/main/openapi/onecx-permission-di-v1.yaml + gen.io.github.onecx.permission.domain.di.v1 + gen.io.github.onecx.permission.domain.di.v1.model + DTOV1 + false + + internal @@ -194,6 +220,19 @@ DTOV1 + + v1 + + generate + + + src/main/openapi/onecx-permission-v1.yaml + gen.io.github.onecx.permission.rs.external.v1 + gen.io.github.onecx.permission.rs.external.v1.model + DTOV1 + ApiV1 + + diff --git a/src/main/java/io/github/onecx/permission/common/models/TokenConfig.java b/src/main/java/io/github/onecx/permission/common/models/TokenConfig.java new file mode 100644 index 0000000..6930ea2 --- /dev/null +++ b/src/main/java/io/github/onecx/permission/common/models/TokenConfig.java @@ -0,0 +1,29 @@ +package io.github.onecx.permission.common.models; + +import java.util.Optional; + +import io.quarkus.runtime.annotations.StaticInitSafe; +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithDefault; +import io.smallrye.config.WithName; + +@StaticInitSafe +@ConfigMapping(prefix = "onecx.permission") +public interface TokenConfig { + + @WithName("token.verified") + boolean tokenVerified(); + + @WithName("token.issuer.public-key-location.suffix") + String tokenPublicKeyLocationSuffix(); + + @WithName("token.issuer.public-key-location.enabled") + boolean tokenPublicKeyEnabled(); + + @WithName("token.claim.separator") + Optional tokenClaimSeparator(); + + @WithName("token.claim.path") + @WithDefault("realm_access/roles") + String tokenClaimPath(); +} diff --git a/src/main/java/io/github/onecx/permission/common/services/ClaimService.java b/src/main/java/io/github/onecx/permission/common/services/ClaimService.java new file mode 100644 index 0000000..35567ef --- /dev/null +++ b/src/main/java/io/github/onecx/permission/common/services/ClaimService.java @@ -0,0 +1,36 @@ +package io.github.onecx.permission.common.services; + +import java.util.regex.Pattern; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import io.github.onecx.permission.common.models.TokenConfig; + +@ApplicationScoped +public class ClaimService { + + @SuppressWarnings("java:S5998") + private static final Pattern CLAIM_PATH_PATTERN = Pattern.compile("\\/(?=(?:(?:[^\"]*\"){2})*[^\"]*$)"); + + private static String[] claimPath; + + @Inject + TokenConfig config; + + @PostConstruct + @SuppressWarnings("java:S2696") + public void init() { + claimPath = splitClaimPath(config.tokenClaimPath()); + } + + public String[] getClaimPath() { + return claimPath; + } + + @SuppressWarnings("java:S2692") + static String[] splitClaimPath(String claimPath) { + return claimPath.indexOf('/') > 0 ? CLAIM_PATH_PATTERN.split(claimPath) : new String[] { claimPath }; + } +} diff --git a/src/main/java/io/github/onecx/permission/common/services/TokenService.java b/src/main/java/io/github/onecx/permission/common/services/TokenService.java new file mode 100644 index 0000000..ba42a16 --- /dev/null +++ b/src/main/java/io/github/onecx/permission/common/services/TokenService.java @@ -0,0 +1,85 @@ +package io.github.onecx.permission.common.services; + +import static io.github.onecx.permission.common.utils.TokenUtil.findClaimWithRoles; + +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import org.jose4j.jws.JsonWebSignature; +import org.jose4j.jwt.JwtClaims; +import org.jose4j.jwt.MalformedClaimException; +import org.jose4j.jwt.consumer.InvalidJwtException; +import org.jose4j.jwx.JsonWebStructure; +import org.jose4j.lang.JoseException; + +import io.github.onecx.permission.common.models.TokenConfig; +import io.smallrye.jwt.auth.principal.JWTAuthContextInfo; +import io.smallrye.jwt.auth.principal.JWTParser; +import io.smallrye.jwt.auth.principal.ParseException; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@ApplicationScoped +public class TokenService { + + @Inject + JWTAuthContextInfo authContextInfo; + + @Inject + TokenConfig config; + + @Inject + JWTParser parser; + + @Inject + ClaimService claimService; + + public List getTokenRoles(String tokenData) { + try { + return getRoles(tokenData); + } catch (Exception ex) { + throw new TokenException("Error parsing principal token", ex); + } + } + + private List getRoles(String tokenData) + throws JoseException, InvalidJwtException, MalformedClaimException, ParseException { + + var claimPath = claimService.getClaimPath(); + + if (config.tokenVerified()) { + var info = authContextInfo; + + // get public key location from issuer URL + if (config.tokenPublicKeyEnabled()) { + var jws = (JsonWebSignature) JsonWebStructure.fromCompactSerialization(tokenData); + var jwtClaims = JwtClaims.parse(jws.getUnverifiedPayload()); + var publicKeyLocation = jwtClaims.getIssuer() + config.tokenPublicKeyLocationSuffix(); + info = new JWTAuthContextInfo(authContextInfo); + info.setPublicKeyLocation(publicKeyLocation); + } + + var token = parser.parse(tokenData, info); + var first = token.getClaim(claimPath[0]); + + return findClaimWithRoles(config, first, claimPath); + + } else { + + var jws = (JsonWebSignature) JsonWebStructure.fromCompactSerialization(tokenData); + + var jwtClaims = JwtClaims.parse(jws.getUnverifiedPayload()); + var first = jwtClaims.getClaimValue(claimPath[0]); + return findClaimWithRoles(config, first, claimPath); + } + } + + public static class TokenException extends RuntimeException { + + public TokenException(String message, Throwable t) { + super(message, t); + } + } +} diff --git a/src/main/java/io/github/onecx/permission/common/utils/TokenUtil.java b/src/main/java/io/github/onecx/permission/common/utils/TokenUtil.java new file mode 100644 index 0000000..a298caa --- /dev/null +++ b/src/main/java/io/github/onecx/permission/common/utils/TokenUtil.java @@ -0,0 +1,59 @@ +package io.github.onecx.permission.common.utils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import jakarta.json.JsonArray; +import jakarta.json.JsonObject; +import jakarta.json.JsonValue; + +import io.github.onecx.permission.common.models.TokenConfig; +import io.smallrye.jwt.JsonUtils; + +public final class TokenUtil { + + private TokenUtil() { + } + + public static List findClaimWithRoles(TokenConfig config, Object value, String[] path) { + JsonValue first = JsonUtils.wrapValue(value); + JsonValue claimValue = findClaimValue(first, path, 1); + + if (claimValue instanceof JsonArray jsonArray) { + return convertJsonArrayToList(jsonArray); + } else if (claimValue != null) { + if (claimValue.toString().isBlank()) { + return Collections.emptyList(); + } + return Arrays.asList(claimValue.toString().split(config.tokenClaimSeparator().orElse(" "))); + } else { + return Collections.emptyList(); + } + } + + static List convertJsonArrayToList(JsonArray claimValue) { + List list = new ArrayList<>(claimValue.size()); + for (int i = 0; i < claimValue.size(); i++) { + String claimValueStr = claimValue.getString(i); + if (claimValueStr.isBlank()) { + continue; + } + list.add(claimValue.getString(i)); + } + return list; + } + + private static JsonValue findClaimValue(JsonValue json, String[] pathArray, int step) { + if (json == null) { + return null; + } + if (step < pathArray.length && (json instanceof JsonObject)) { + JsonValue claimValue = json.asJsonObject().get(pathArray[step].replace("\"", "")); + return findClaimValue(claimValue, pathArray, step + 1); + } + return json; + } + +} diff --git a/src/main/java/io/github/onecx/permission/domain/criteria/AssignmentSearchCriteria.java b/src/main/java/io/github/onecx/permission/domain/criteria/AssignmentSearchCriteria.java new file mode 100644 index 0000000..2a9bfab --- /dev/null +++ b/src/main/java/io/github/onecx/permission/domain/criteria/AssignmentSearchCriteria.java @@ -0,0 +1,13 @@ +package io.github.onecx.permission.domain.criteria; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class AssignmentSearchCriteria { + + private String appId; + private Integer pageNumber; + private Integer pageSize; +} diff --git a/src/main/java/io/github/onecx/permission/domain/criteria/PermissionSearchCriteria.java b/src/main/java/io/github/onecx/permission/domain/criteria/PermissionSearchCriteria.java index 55e17d3..f7bca5a 100644 --- a/src/main/java/io/github/onecx/permission/domain/criteria/PermissionSearchCriteria.java +++ b/src/main/java/io/github/onecx/permission/domain/criteria/PermissionSearchCriteria.java @@ -8,9 +8,6 @@ public class PermissionSearchCriteria { private String appId; - private String name; - private String resource; - private String action; private Integer pageNumber; private Integer pageSize; } diff --git a/src/main/java/io/github/onecx/permission/domain/criteria/RoleSearchCriteria.java b/src/main/java/io/github/onecx/permission/domain/criteria/RoleSearchCriteria.java new file mode 100644 index 0000000..90dd3fa --- /dev/null +++ b/src/main/java/io/github/onecx/permission/domain/criteria/RoleSearchCriteria.java @@ -0,0 +1,14 @@ +package io.github.onecx.permission.domain.criteria; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class RoleSearchCriteria { + + private String name; + private String description; + private Integer pageNumber; + private Integer pageSize; +} diff --git a/src/main/java/io/github/onecx/permission/domain/criteria/WorkspaceAssignmentSearchCriteria.java b/src/main/java/io/github/onecx/permission/domain/criteria/WorkspaceAssignmentSearchCriteria.java new file mode 100644 index 0000000..bf4ca66 --- /dev/null +++ b/src/main/java/io/github/onecx/permission/domain/criteria/WorkspaceAssignmentSearchCriteria.java @@ -0,0 +1,13 @@ +package io.github.onecx.permission.domain.criteria; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class WorkspaceAssignmentSearchCriteria { + + private String workspaceId; + private Integer pageNumber; + private Integer pageSize; +} diff --git a/src/main/java/io/github/onecx/permission/domain/criteria/WorkspacePermissionSearchCriteria.java b/src/main/java/io/github/onecx/permission/domain/criteria/WorkspacePermissionSearchCriteria.java new file mode 100644 index 0000000..852861a --- /dev/null +++ b/src/main/java/io/github/onecx/permission/domain/criteria/WorkspacePermissionSearchCriteria.java @@ -0,0 +1,13 @@ +package io.github.onecx.permission.domain.criteria; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class WorkspacePermissionSearchCriteria { + + private String workspaceId; + private Integer pageNumber; + private Integer pageSize; +} diff --git a/src/main/java/io/github/onecx/permission/domain/daos/AssignmentDAO.java b/src/main/java/io/github/onecx/permission/domain/daos/AssignmentDAO.java new file mode 100644 index 0000000..7df5327 --- /dev/null +++ b/src/main/java/io/github/onecx/permission/domain/daos/AssignmentDAO.java @@ -0,0 +1,72 @@ +package io.github.onecx.permission.domain.daos; + +import java.util.ArrayList; +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.persistence.NoResultException; +import jakarta.persistence.criteria.Predicate; +import jakarta.transaction.Transactional; + +import org.tkit.quarkus.jpa.daos.AbstractDAO; +import org.tkit.quarkus.jpa.daos.Page; +import org.tkit.quarkus.jpa.daos.PageResult; +import org.tkit.quarkus.jpa.exceptions.DAOException; +import org.tkit.quarkus.jpa.models.AbstractTraceableEntity_; +import org.tkit.quarkus.jpa.models.TraceableEntity_; + +import io.github.onecx.permission.domain.criteria.AssignmentSearchCriteria; +import io.github.onecx.permission.domain.models.Assignment; +import io.github.onecx.permission.domain.models.Assignment_; +import io.github.onecx.permission.domain.models.Permission_; + +@ApplicationScoped +public class AssignmentDAO extends AbstractDAO { + + // https://hibernate.atlassian.net/browse/HHH-16830#icft=HHH-16830 + @Override + public Assignment findById(Object id) throws DAOException { + try { + var cb = this.getEntityManager().getCriteriaBuilder(); + var cq = cb.createQuery(Assignment.class); + var root = cq.from(Assignment.class); + cq.where(cb.equal(root.get(TraceableEntity_.ID), id)); + return this.getEntityManager().createQuery(cq).getSingleResult(); + } catch (NoResultException nre) { + return null; + } catch (Exception e) { + throw new DAOException(ErrorKeys.FIND_ENTITY_BY_ID_FAILED, e, entityName, id); + } + } + + @Transactional(Transactional.TxType.NOT_SUPPORTED) + public PageResult findByCriteria(AssignmentSearchCriteria criteria) { + try { + var cb = this.getEntityManager().getCriteriaBuilder(); + var cq = cb.createQuery(Assignment.class); + var root = cq.from(Assignment.class); + + List predicates = new ArrayList<>(); + + if (criteria.getAppId() != null && !criteria.getAppId().isBlank()) { + predicates.add(cb.equal(root.get(Assignment_.permission).get(Permission_.APP_ID), criteria.getAppId())); + } + + if (!predicates.isEmpty()) { + cq.where(predicates.toArray(new Predicate[] {})); + } + + cq.orderBy(cb.asc(root.get(AbstractTraceableEntity_.creationDate))); + + return createPageQuery(cq, Page.of(criteria.getPageNumber(), criteria.getPageSize())).getPageResult(); + } catch (Exception ex) { + throw new DAOException(ErrorKeys.ERROR_FIND_ASSIGNMENT_BY_CRITERIA, ex); + } + } + + public enum ErrorKeys { + + FIND_ENTITY_BY_ID_FAILED, + ERROR_FIND_ASSIGNMENT_BY_CRITERIA; + } +} diff --git a/src/main/java/io/github/onecx/permission/domain/daos/PermissionDAO.java b/src/main/java/io/github/onecx/permission/domain/daos/PermissionDAO.java index 24421cf..e5e5f47 100644 --- a/src/main/java/io/github/onecx/permission/domain/daos/PermissionDAO.java +++ b/src/main/java/io/github/onecx/permission/domain/daos/PermissionDAO.java @@ -5,17 +5,18 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Subquery; import jakarta.transaction.Transactional; import org.tkit.quarkus.jpa.daos.AbstractDAO; import org.tkit.quarkus.jpa.daos.Page; import org.tkit.quarkus.jpa.daos.PageResult; import org.tkit.quarkus.jpa.exceptions.DAOException; +import org.tkit.quarkus.jpa.models.TraceableEntity_; import org.tkit.quarkus.jpa.utils.QueryCriteriaUtil; import io.github.onecx.permission.domain.criteria.PermissionSearchCriteria; -import io.github.onecx.permission.domain.models.Permission; -import io.github.onecx.permission.domain.models.Permission_; +import io.github.onecx.permission.domain.models.*; @ApplicationScoped public class PermissionDAO extends AbstractDAO { @@ -29,18 +30,9 @@ public PageResult findByCriteria(PermissionSearchCriteria criteria) List predicates = new ArrayList<>(); - if (criteria.getName() != null && !criteria.getName().isBlank()) { - predicates.add(cb.like(root.get(Permission_.name), QueryCriteriaUtil.wildcard(criteria.getName()))); - } - if (criteria.getAction() != null && !criteria.getAction().isBlank()) { - predicates.add(cb.like(root.get(Permission_.action), QueryCriteriaUtil.wildcard(criteria.getAction()))); - } if (criteria.getAppId() != null && !criteria.getAppId().isBlank()) { predicates.add(cb.like(root.get(Permission_.appId), QueryCriteriaUtil.wildcard(criteria.getAppId()))); } - if (criteria.getResource() != null && !criteria.getResource().isBlank()) { - predicates.add(cb.like(root.get(Permission_.resource), QueryCriteriaUtil.wildcard(criteria.getResource()))); - } if (!predicates.isEmpty()) { cq.where(predicates.toArray(new Predicate[] {})); @@ -64,8 +56,51 @@ public List loadByAppId(String appId) { } } + public List findPermissionForUser(String appId, List roles) { + try { + var cb = this.getEntityManager().getCriteriaBuilder(); + var cq = cb.createQuery(Permission.class); + var root = cq.from(Permission.class); + + Subquery sq = cq.subquery(String.class); + var subRoot = sq.from(Assignment.class); + sq.select(subRoot.get(Assignment_.PERMISSION_ID)); + sq.where( + subRoot.get(Assignment_.role).get(Role_.name).in(roles), + cb.equal(subRoot.get(Assignment_.permission).get(Permission_.appId), appId)); + + cq.where(root.get(TraceableEntity_.id).in(sq)); + + return this.getEntityManager().createQuery(cq).getResultList(); + } catch (Exception ex) { + throw new DAOException(ErrorKeys.ERROR_FIND_PERMISSION_FOR_USER, ex); + } + } + + public List findAllPermissionForUser(List roles) { + try { + var cb = this.getEntityManager().getCriteriaBuilder(); + var cq = cb.createQuery(Permission.class); + var root = cq.from(Permission.class); + + Subquery sq = cq.subquery(String.class); + var subRoot = sq.from(Assignment.class); + sq.select(subRoot.get(Assignment_.PERMISSION_ID)); + sq.where( + subRoot.get(Assignment_.role).get(Role_.name).in(roles)); + + cq.where(root.get(TraceableEntity_.id).in(sq)); + + return this.getEntityManager().createQuery(cq).getResultList(); + } catch (Exception ex) { + throw new DAOException(ErrorKeys.ERROR_FIND_ALL_PERMISSION_FOR_USER, ex); + } + } + public enum ErrorKeys { + ERROR_FIND_ALL_PERMISSION_FOR_USER, + ERROR_FIND_PERMISSION_FOR_USER, ERROR_LOAD_BY_APP_ID, ERROR_FIND_PERMISSION_BY_CRITERIA; } diff --git a/src/main/java/io/github/onecx/permission/domain/daos/RoleDAO.java b/src/main/java/io/github/onecx/permission/domain/daos/RoleDAO.java new file mode 100644 index 0000000..462b765 --- /dev/null +++ b/src/main/java/io/github/onecx/permission/domain/daos/RoleDAO.java @@ -0,0 +1,72 @@ +package io.github.onecx.permission.domain.daos; + +import java.util.ArrayList; +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.persistence.NoResultException; +import jakarta.persistence.criteria.Predicate; +import jakarta.transaction.Transactional; + +import org.tkit.quarkus.jpa.daos.AbstractDAO; +import org.tkit.quarkus.jpa.daos.Page; +import org.tkit.quarkus.jpa.daos.PageResult; +import org.tkit.quarkus.jpa.exceptions.DAOException; +import org.tkit.quarkus.jpa.models.TraceableEntity_; +import org.tkit.quarkus.jpa.utils.QueryCriteriaUtil; + +import io.github.onecx.permission.domain.criteria.RoleSearchCriteria; +import io.github.onecx.permission.domain.models.Role; +import io.github.onecx.permission.domain.models.Role_; + +@ApplicationScoped +public class RoleDAO extends AbstractDAO { + + // https://hibernate.atlassian.net/browse/HHH-16830#icft=HHH-16830 + @Override + public Role findById(Object id) throws DAOException { + try { + var cb = this.getEntityManager().getCriteriaBuilder(); + var cq = cb.createQuery(Role.class); + var root = cq.from(Role.class); + cq.where(cb.equal(root.get(TraceableEntity_.ID), id)); + return this.getEntityManager().createQuery(cq).getSingleResult(); + } catch (NoResultException nre) { + return null; + } catch (Exception e) { + throw new DAOException(ErrorKeys.FIND_ENTITY_BY_ID_FAILED, e, entityName, id); + } + } + + @Transactional(Transactional.TxType.NOT_SUPPORTED) + public PageResult findByCriteria(RoleSearchCriteria criteria) { + try { + var cb = this.getEntityManager().getCriteriaBuilder(); + var cq = cb.createQuery(Role.class); + var root = cq.from(Role.class); + + List predicates = new ArrayList<>(); + + if (criteria.getName() != null && !criteria.getName().isBlank()) { + predicates.add(cb.like(root.get(Role_.name), QueryCriteriaUtil.wildcard(criteria.getName()))); + } + if (criteria.getDescription() != null && !criteria.getDescription().isBlank()) { + predicates.add(cb.like(root.get(Role_.description), QueryCriteriaUtil.wildcard(criteria.getDescription()))); + } + + if (!predicates.isEmpty()) { + cq.where(predicates.toArray(new Predicate[] {})); + } + + return createPageQuery(cq, Page.of(criteria.getPageNumber(), criteria.getPageSize())).getPageResult(); + } catch (Exception ex) { + throw new DAOException(ErrorKeys.ERROR_FIND_ROLE_BY_CRITERIA, ex); + } + } + + public enum ErrorKeys { + + FIND_ENTITY_BY_ID_FAILED, + ERROR_FIND_ROLE_BY_CRITERIA; + } +} diff --git a/src/main/java/io/github/onecx/permission/domain/daos/WorkspaceAssignmentDAO.java b/src/main/java/io/github/onecx/permission/domain/daos/WorkspaceAssignmentDAO.java new file mode 100644 index 0000000..1e3631f --- /dev/null +++ b/src/main/java/io/github/onecx/permission/domain/daos/WorkspaceAssignmentDAO.java @@ -0,0 +1,72 @@ +package io.github.onecx.permission.domain.daos; + +import java.util.ArrayList; +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.persistence.NoResultException; +import jakarta.persistence.criteria.Predicate; +import jakarta.transaction.Transactional; + +import org.tkit.quarkus.jpa.daos.AbstractDAO; +import org.tkit.quarkus.jpa.daos.Page; +import org.tkit.quarkus.jpa.daos.PageResult; +import org.tkit.quarkus.jpa.exceptions.DAOException; +import org.tkit.quarkus.jpa.models.AbstractTraceableEntity_; +import org.tkit.quarkus.jpa.models.TraceableEntity_; + +import io.github.onecx.permission.domain.criteria.WorkspaceAssignmentSearchCriteria; +import io.github.onecx.permission.domain.models.*; + +@ApplicationScoped +public class WorkspaceAssignmentDAO extends AbstractDAO { + + // https://hibernate.atlassian.net/browse/HHH-16830#icft=HHH-16830 + @Override + public WorkspaceAssignment findById(Object id) throws DAOException { + try { + var cb = this.getEntityManager().getCriteriaBuilder(); + var cq = cb.createQuery(WorkspaceAssignment.class); + var root = cq.from(WorkspaceAssignment.class); + cq.where(cb.equal(root.get(TraceableEntity_.ID), id)); + return this.getEntityManager().createQuery(cq).getSingleResult(); + } catch (NoResultException nre) { + return null; + } catch (Exception e) { + throw new DAOException(ErrorKeys.FIND_ENTITY_BY_ID_FAILED, e, entityName, id); + } + } + + @Transactional(Transactional.TxType.NOT_SUPPORTED) + public PageResult findByCriteria(WorkspaceAssignmentSearchCriteria criteria) { + try { + var cb = this.getEntityManager().getCriteriaBuilder(); + var cq = cb.createQuery(WorkspaceAssignment.class); + var root = cq.from(WorkspaceAssignment.class); + + List predicates = new ArrayList<>(); + + if (criteria.getWorkspaceId() != null && !criteria.getWorkspaceId().isBlank()) { + predicates.add(cb.equal(root.get(WorkspaceAssignment_.permission).get(WorkspacePermission_.WORKSPACE_ID), + criteria.getWorkspaceId())); + } + + if (!predicates.isEmpty()) { + cq.where(predicates.toArray(new Predicate[] {})); + } + + cq.orderBy(cb.asc(root.get(AbstractTraceableEntity_.creationDate))); + + return createPageQuery(cq, Page.of(criteria.getPageNumber(), criteria.getPageSize())).getPageResult(); + } catch (Exception ex) { + throw new DAOException(ErrorKeys.ERROR_FIND_ASSIGNMENT_BY_CRITERIA, ex); + } + } + + public enum ErrorKeys { + + ERROR_FIND_ASSIGNMENT_BY_CRITERIA, + + FIND_ENTITY_BY_ID_FAILED; + } +} diff --git a/src/main/java/io/github/onecx/permission/domain/daos/WorkspacePermissionDAO.java b/src/main/java/io/github/onecx/permission/domain/daos/WorkspacePermissionDAO.java new file mode 100644 index 0000000..f5d3ea0 --- /dev/null +++ b/src/main/java/io/github/onecx/permission/domain/daos/WorkspacePermissionDAO.java @@ -0,0 +1,92 @@ +package io.github.onecx.permission.domain.daos; + +import java.util.ArrayList; +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.persistence.NoResultException; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Subquery; +import jakarta.transaction.Transactional; + +import org.tkit.quarkus.jpa.daos.AbstractDAO; +import org.tkit.quarkus.jpa.daos.Page; +import org.tkit.quarkus.jpa.daos.PageResult; +import org.tkit.quarkus.jpa.exceptions.DAOException; +import org.tkit.quarkus.jpa.models.TraceableEntity_; +import org.tkit.quarkus.jpa.utils.QueryCriteriaUtil; + +import io.github.onecx.permission.domain.criteria.WorkspacePermissionSearchCriteria; +import io.github.onecx.permission.domain.models.*; + +@ApplicationScoped +public class WorkspacePermissionDAO extends AbstractDAO { + + // https://hibernate.atlassian.net/browse/HHH-16830#icft=HHH-16830 + @Override + public WorkspacePermission findById(Object id) throws DAOException { + try { + var cb = this.getEntityManager().getCriteriaBuilder(); + var cq = cb.createQuery(WorkspacePermission.class); + var root = cq.from(WorkspacePermission.class); + cq.where(cb.equal(root.get(TraceableEntity_.ID), id)); + return this.getEntityManager().createQuery(cq).getSingleResult(); + } catch (NoResultException nre) { + return null; + } catch (Exception e) { + throw new DAOException(ErrorKeys.FIND_ENTITY_BY_ID_FAILED, e, entityName, id); + } + } + + @Transactional(Transactional.TxType.NOT_SUPPORTED) + public PageResult findByCriteria(WorkspacePermissionSearchCriteria criteria) { + try { + var cb = this.getEntityManager().getCriteriaBuilder(); + var cq = cb.createQuery(WorkspacePermission.class); + var root = cq.from(WorkspacePermission.class); + + List predicates = new ArrayList<>(); + + if (criteria.getWorkspaceId() != null && !criteria.getWorkspaceId().isBlank()) { + predicates.add(cb.like(root.get(WorkspacePermission_.workspaceId), + QueryCriteriaUtil.wildcard(criteria.getWorkspaceId()))); + } + + if (!predicates.isEmpty()) { + cq.where(predicates.toArray(new Predicate[] {})); + } + + return createPageQuery(cq, Page.of(criteria.getPageNumber(), criteria.getPageSize())).getPageResult(); + } catch (Exception ex) { + throw new DAOException(ErrorKeys.ERROR_FIND_PERMISSION_BY_CRITERIA, ex); + } + } + + public List findWorkspacePermissionForUser(String workspaceId, List roles) { + try { + var cb = this.getEntityManager().getCriteriaBuilder(); + var cq = cb.createQuery(WorkspacePermission.class); + var root = cq.from(WorkspacePermission.class); + + Subquery sq = cq.subquery(String.class); + var subRoot = sq.from(WorkspaceAssignment.class); + sq.select(subRoot.get(WorkspaceAssignment_.PERMISSION_ID)); + sq.where( + subRoot.get(WorkspaceAssignment_.role).get(Role_.name).in(roles), + cb.equal(subRoot.get(WorkspaceAssignment_.permission).get(WorkspacePermission_.workspaceId), workspaceId)); + + cq.where(root.get(TraceableEntity_.id).in(sq)); + + return this.getEntityManager().createQuery(cq).getResultList(); + } catch (Exception ex) { + throw new DAOException(ErrorKeys.ERROR_FIND_WORKSPACE_PERMISSION_FOR_USER, ex); + } + } + + public enum ErrorKeys { + + ERROR_FIND_WORKSPACE_PERMISSION_FOR_USER, + ERROR_FIND_PERMISSION_BY_CRITERIA, + FIND_ENTITY_BY_ID_FAILED; + } +} diff --git a/src/main/java/io/github/onecx/permission/domain/di/PermissionDataImportV1.java b/src/main/java/io/github/onecx/permission/domain/di/PermissionDataImportV1.java new file mode 100644 index 0000000..c21ffe9 --- /dev/null +++ b/src/main/java/io/github/onecx/permission/domain/di/PermissionDataImportV1.java @@ -0,0 +1,91 @@ +package io.github.onecx.permission.domain.di; + +import java.util.function.Consumer; + +import jakarta.inject.Inject; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.tkit.quarkus.dataimport.DataImport; +import org.tkit.quarkus.dataimport.DataImportConfig; +import org.tkit.quarkus.dataimport.DataImportService; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import gen.io.github.onecx.permission.domain.di.v1.model.DataImportDTOV1; + +@DataImport("permission") +public class PermissionDataImportV1 implements DataImportService { + + private static final Logger log = LoggerFactory.getLogger(PermissionDataImportV1.class); + + @Inject + ObjectMapper objectMapper; + + @Inject + PermissionImportService service; + + @Override + public void importData(DataImportConfig config) { + log.info("Import permissions from configuration {}", config); + try { + var operation = config.getMetadata().getOrDefault("operation", "NONE"); + + Consumer action = null; + if ("CLEAN_INSERT".equals(operation)) { + action = this::cleanInsert; + } + + if (action == null) { + log.warn("Not supported operation '{}' for the import configuration key '{}'", operation, config.getKey()); + return; + } + + if (config.getData() == null || config.getData().length == 0) { + log.warn("Import configuration key {} does not contains any data to import", config.getKey()); + return; + } + + DataImportDTOV1 data = objectMapper.readValue(config.getData(), DataImportDTOV1.class); + + if (checkIsEmpty(data)) { + log.warn("Import configuration key {} does not contains any JSON data to import", config.getKey()); + return; + } + + // execute the import + action.accept(data); + } catch (Exception ex) { + throw new ImportException(ex.getMessage(), ex); + } + } + + static boolean checkIsEmpty(DataImportDTOV1 data) { + return (data.getPermissions() == null || data.getPermissions().isEmpty()) + && (data.getTenants() == null || data.getTenants().isEmpty()); + } + + public void cleanInsert(DataImportDTOV1 data) { + + // delete all tenant data + var tenants = data.getTenants().keySet(); + tenants.forEach(tenant -> service.deleteAllData(tenant)); + + // delete all permission + service.deleteAllPermissions(); + + // create permissions + var permissionMap = service.createAllPermissions(data.getPermissions()); + + // create tenant data + data.getTenants().forEach((tenantId, dto) -> service.createTenantData(tenantId, dto, permissionMap)); + + } + + public static class ImportException extends RuntimeException { + + public ImportException(String message, Throwable ex) { + super(message, ex); + } + } +} diff --git a/src/main/java/io/github/onecx/permission/domain/di/PermissionImportService.java b/src/main/java/io/github/onecx/permission/domain/di/PermissionImportService.java new file mode 100644 index 0000000..ecb6953 --- /dev/null +++ b/src/main/java/io/github/onecx/permission/domain/di/PermissionImportService.java @@ -0,0 +1,107 @@ +package io.github.onecx.permission.domain.di; + +import java.util.Map; +import java.util.stream.Collectors; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; + +import org.tkit.quarkus.context.ApplicationContext; +import org.tkit.quarkus.context.Context; + +import gen.io.github.onecx.permission.domain.di.v1.model.DataImportTenantWrapperDTOV1; +import io.github.onecx.permission.domain.daos.*; +import io.github.onecx.permission.domain.di.mappers.DataImportV1Mapper; +import io.github.onecx.permission.domain.models.Permission; +import io.github.onecx.permission.domain.models.Role; + +@ApplicationScoped +public class PermissionImportService { + + @Inject + PermissionDAO permissionDAO; + + @Inject + AssignmentDAO assignmentDAO; + + @Inject + RoleDAO roleDAO; + + @Inject + WorkspacePermissionDAO workspacePermissionDAO; + + @Inject + WorkspaceAssignmentDAO workspaceAssignmentDAO; + + @Inject + DataImportV1Mapper mapper; + + @Transactional(Transactional.TxType.REQUIRES_NEW) + public void deleteAllData(String tenantId) { + try { + var ctx = Context.builder() + .principal("data-import") + .tenantId(tenantId) + .build(); + + ApplicationContext.start(ctx); + + assignmentDAO.deleteQueryAll(); + workspaceAssignmentDAO.deleteQueryAll(); + workspacePermissionDAO.deleteQueryAll(); + roleDAO.deleteQueryAll(); + + } finally { + ApplicationContext.close(); + } + } + + @Transactional(Transactional.TxType.REQUIRES_NEW) + public void deleteAllPermissions() { + permissionDAO.deleteQueryAll(); + } + + @Transactional(Transactional.TxType.REQUIRES_NEW) + public Map createAllPermissions(Map>> permissions) { + var items = mapper.map(permissions); + permissionDAO.create(items); + return items.stream().collect(Collectors.toMap(r -> r.getAppId() + r.getResource() + r.getAction(), r -> r)); + } + + @Transactional(Transactional.TxType.REQUIRES_NEW) + public void createTenantData(String tenantId, DataImportTenantWrapperDTOV1 dto, Map permissionMap) { + try { + var ctx = Context.builder() + .principal("data-import") + .tenantId(tenantId) + .build(); + + ApplicationContext.start(ctx); + + // create workspace permissions + var workspacePermissions = mapper.mapWorkspace(dto.getWorkspacesPermissions()); + workspacePermissionDAO.create(workspacePermissions); + var workspacePermissionsMap = workspacePermissions.stream() + .collect(Collectors.toMap(r -> r.getWorkspaceId() + r.getResource() + r.getAction(), r -> r)); + + // create tenant roles + var roles = mapper.createRoles(dto.getRoles()); + roleDAO.create(roles); + var rolesMap = roles.stream().collect(Collectors.toMap(Role::getName, r -> r)); + + // create tenant assignments + var mapping = mapper.createMapping(dto.getRoles()); + var assignments = mapper.createAssignments(mapping, rolesMap, permissionMap); + assignmentDAO.create(assignments); + + // create tenant workspace assignments + var workspaceMapping = mapper.createWorkspaceMapping(dto.getRoles()); + var workspaceAssignments = mapper.createWorkspaceAssignments(workspaceMapping, rolesMap, workspacePermissionsMap); + workspaceAssignmentDAO.create(workspaceAssignments); + + } finally { + ApplicationContext.close(); + } + } +} diff --git a/src/main/java/io/github/onecx/permission/domain/di/mappers/DataImportV1Mapper.java b/src/main/java/io/github/onecx/permission/domain/di/mappers/DataImportV1Mapper.java new file mode 100644 index 0000000..ece39a7 --- /dev/null +++ b/src/main/java/io/github/onecx/permission/domain/di/mappers/DataImportV1Mapper.java @@ -0,0 +1,165 @@ +package io.github.onecx.permission.domain.di.mappers; + +import java.util.*; + +import org.mapstruct.*; + +import gen.io.github.onecx.permission.domain.di.v1.model.DataImportRoleDTOV1; +import io.github.onecx.permission.domain.models.*; + +@Mapper +public interface DataImportV1Mapper { + + default List createAssignments(Map> mapping, Map roles, + Map permissions) { + if (permissions == null || roles == null || mapping == null) { + return List.of(); + } + List result = new ArrayList<>(); + mapping.forEach( + (role, perms) -> perms.forEach(perm -> result.add(createAssignment(roles.get(role), permissions.get(perm))))); + return result; + } + + default Map> createMapping(Map dtoRoles) { + if (dtoRoles == null || dtoRoles.isEmpty()) { + return Map.of(); + } + Map> mapping = new HashMap<>(); + dtoRoles.forEach((role, item) -> { + Set perms = new HashSet<>(); + + item.getAssignments().forEach((appId, an) -> an + .forEach((resource, actions) -> actions.forEach(action -> perms.add(appId + resource + action)))); + + mapping.put(role, perms); + }); + return mapping; + } + + default Map> createWorkspaceMapping(Map dtoRoles) { + if (dtoRoles == null || dtoRoles.isEmpty()) { + return Map.of(); + } + Map> mapping = new HashMap<>(); + dtoRoles.forEach((role, item) -> { + Set perms = new HashSet<>(); + + item.getWorkspacesAssignments().forEach((workspaceId, an) -> an + .forEach((resource, actions) -> actions.forEach(action -> perms.add(workspaceId + resource + action)))); + + mapping.put(role, perms); + }); + return mapping; + } + + default List createWorkspaceAssignments(Map> mapping, Map roles, + Map permissions) { + if (permissions == null || roles == null || mapping == null) { + return List.of(); + } + + List result = new ArrayList<>(); + mapping.forEach((role, perms) -> perms.forEach(perm -> result.add(create(roles.get(role), permissions.get(perm))))); + return result; + } + + @Mapping(target = "id", ignore = true) + @Mapping(target = "creationDate", ignore = true) + @Mapping(target = "creationUser", ignore = true) + @Mapping(target = "modificationDate", ignore = true) + @Mapping(target = "modificationUser", ignore = true) + @Mapping(target = "controlTraceabilityManual", ignore = true) + @Mapping(target = "modificationCount", ignore = true) + @Mapping(target = "persisted", ignore = true) + @Mapping(target = "roleId", ignore = true) + @Mapping(target = "permissionId", ignore = true) + @Mapping(target = "tenantId", ignore = true) + Assignment createAssignment(Role role, Permission permission); + + @Mapping(target = "id", ignore = true) + @Mapping(target = "creationDate", ignore = true) + @Mapping(target = "creationUser", ignore = true) + @Mapping(target = "modificationDate", ignore = true) + @Mapping(target = "modificationUser", ignore = true) + @Mapping(target = "controlTraceabilityManual", ignore = true) + @Mapping(target = "modificationCount", ignore = true) + @Mapping(target = "persisted", ignore = true) + @Mapping(target = "roleId", ignore = true) + @Mapping(target = "permissionId", ignore = true) + @Mapping(target = "tenantId", ignore = true) + WorkspaceAssignment create(Role role, WorkspacePermission permission); + + default List createRoles(Map dto) { + if (dto == null) { + return List.of(); + } + return dto.entrySet().stream().map(entry -> create(entry.getKey(), entry.getValue().getDescription())).toList(); + } + + @Mapping(target = "id", ignore = true) + @Mapping(target = "creationDate", ignore = true) + @Mapping(target = "creationUser", ignore = true) + @Mapping(target = "modificationDate", ignore = true) + @Mapping(target = "modificationUser", ignore = true) + @Mapping(target = "controlTraceabilityManual", ignore = true) + @Mapping(target = "modificationCount", ignore = true) + @Mapping(target = "persisted", ignore = true) + @Mapping(target = "tenantId", ignore = true) + Role create(String name, String description); + + default List mapWorkspace(Map>> permissions) { + if (permissions == null) { + return List.of(); + } + List result = new ArrayList<>(); + permissions.forEach((workspaceId, perm) -> perm.forEach((resource, actions) -> actions + .forEach((action, description) -> { + var tmp = mapWorkspace(workspaceId, resource, action); + if (tmp != null) { + tmp.setDescription(description); + result.add(tmp); + } + }))); + return result; + } + + @Mapping(target = "id", ignore = true) + @Mapping(target = "creationDate", ignore = true) + @Mapping(target = "creationUser", ignore = true) + @Mapping(target = "modificationDate", ignore = true) + @Mapping(target = "modificationUser", ignore = true) + @Mapping(target = "controlTraceabilityManual", ignore = true) + @Mapping(target = "modificationCount", ignore = true) + @Mapping(target = "persisted", ignore = true) + @Mapping(target = "tenantId", ignore = true) + @Mapping(target = "description", ignore = true) + WorkspacePermission mapWorkspace(String workspaceId, String resource, String action); + + default List map(Map>> permissions) { + if (permissions == null) { + return List.of(); + } + List result = new ArrayList<>(); + permissions.forEach((appId, perm) -> perm.forEach((resource, actions) -> actions + .forEach((action, description) -> { + var tmp = map(appId, resource, action); + if (tmp != null) { + tmp.setDescription(description); + result.add(tmp); + } + }))); + return result; + } + + @Mapping(target = "id", ignore = true) + @Mapping(target = "creationDate", ignore = true) + @Mapping(target = "creationUser", ignore = true) + @Mapping(target = "modificationDate", ignore = true) + @Mapping(target = "modificationUser", ignore = true) + @Mapping(target = "controlTraceabilityManual", ignore = true) + @Mapping(target = "modificationCount", ignore = true) + @Mapping(target = "persisted", ignore = true) + @Mapping(target = "description", ignore = true) + Permission map(String appId, String resource, String action); +} diff --git a/src/main/java/io/github/onecx/permission/domain/models/Assignment.java b/src/main/java/io/github/onecx/permission/domain/models/Assignment.java new file mode 100644 index 0000000..85c013a --- /dev/null +++ b/src/main/java/io/github/onecx/permission/domain/models/Assignment.java @@ -0,0 +1,45 @@ +package io.github.onecx.permission.domain.models; + +import jakarta.persistence.*; + +import org.hibernate.annotations.TenantId; +import org.tkit.quarkus.jpa.models.TraceableEntity; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Entity +@Table(name = "ASSIGNMENT", uniqueConstraints = { + @UniqueConstraint(name = "ASSIGNMENT_KEY", columnNames = { "TENANT_ID", "ROLE_ID", "PERMISSION_ID" }) +}, indexes = { + @Index(name = "ASSIGNMENT_TENANT_ID", columnList = "TENANT_ID") +}) +@SuppressWarnings("java:S2160") +public class Assignment extends TraceableEntity { + + @TenantId + @Column(name = "TENANT_ID") + private String tenantId; + + @Column(name = "ROLE_ID", insertable = false, updatable = false) + private String roleId; + + @Column(name = "PERMISSION_ID", insertable = false, updatable = false) + private String permissionId; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "ROLE_ID") + private Role role; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "PERMISSION_ID") + private Permission permission; + + @PostPersist + void postPersist() { + roleId = role.getId(); + permissionId = permission.getId(); + } +} diff --git a/src/main/java/io/github/onecx/permission/domain/models/Permission.java b/src/main/java/io/github/onecx/permission/domain/models/Permission.java index 0da39ee..bd9c60b 100644 --- a/src/main/java/io/github/onecx/permission/domain/models/Permission.java +++ b/src/main/java/io/github/onecx/permission/domain/models/Permission.java @@ -1,9 +1,6 @@ package io.github.onecx.permission.domain.models; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.Table; -import jakarta.persistence.UniqueConstraint; +import jakarta.persistence.*; import org.tkit.quarkus.jpa.models.TraceableEntity; @@ -14,7 +11,9 @@ @Setter @Entity @Table(name = "PERMISSION", uniqueConstraints = { - @UniqueConstraint(name = "PERMISSION_KEY", columnNames = { "APP_ID", "RESOURCE", "ACTION" }) + @UniqueConstraint(name = "PERMISSION_KEY", columnNames = { "APP_ID", "RESOURCE", "ACTION" }), +}, indexes = { + @Index(name = "PERMISSION_APP_ID", columnList = "APP_ID") }) @SuppressWarnings("squid:S2160") public class Permission extends TraceableEntity { @@ -34,12 +33,6 @@ public class Permission extends TraceableEntity { @Column(name = "RESOURCE") private String resource; - /** - * The permission name. - */ - @Column(name = "NAME") - private String name; - /** * The permission description. */ diff --git a/src/main/java/io/github/onecx/permission/domain/models/Role.java b/src/main/java/io/github/onecx/permission/domain/models/Role.java new file mode 100644 index 0000000..633c706 --- /dev/null +++ b/src/main/java/io/github/onecx/permission/domain/models/Role.java @@ -0,0 +1,38 @@ +package io.github.onecx.permission.domain.models; + +import jakarta.persistence.*; + +import org.hibernate.annotations.TenantId; +import org.tkit.quarkus.jpa.models.TraceableEntity; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Entity +@Table(name = "ROLE", uniqueConstraints = { + @UniqueConstraint(name = "ROLE_NAME", columnNames = { "TENANT_ID", "NAME" }) +}, indexes = { + @Index(name = "ROLE_NAME", columnList = "NAME") +}) +@SuppressWarnings("java:S2160") +public class Role extends TraceableEntity { + + @TenantId + @Column(name = "TENANT_ID") + private String tenantId; + + /** + * The role name. + */ + @Column(name = "NAME") + private String name; + + /** + * The role description. + */ + @Column(name = "DESCRIPTION") + private String description; + +} diff --git a/src/main/java/io/github/onecx/permission/domain/models/WorkspaceAssignment.java b/src/main/java/io/github/onecx/permission/domain/models/WorkspaceAssignment.java new file mode 100644 index 0000000..0a9cd2c --- /dev/null +++ b/src/main/java/io/github/onecx/permission/domain/models/WorkspaceAssignment.java @@ -0,0 +1,43 @@ +package io.github.onecx.permission.domain.models; + +import jakarta.persistence.*; + +import org.hibernate.annotations.TenantId; +import org.tkit.quarkus.jpa.models.TraceableEntity; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Entity +@Table(name = "WORKSPACE_ASSIGNMENT", uniqueConstraints = { + @UniqueConstraint(name = "WORKSPACE_ASSIGNMENT_KEY", columnNames = { "TENANT_ID", "ROLE_ID", "PERMISSION_ID" }) +}) +@SuppressWarnings("java:S2160") +public class WorkspaceAssignment extends TraceableEntity { + + @TenantId + @Column(name = "TENANT_ID") + private String tenantId; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "ROLE_ID") + private Role role; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "PERMISSION_ID") + private WorkspacePermission permission; + + @Column(name = "ROLE_ID", insertable = false, updatable = false) + private String roleId; + + @Column(name = "PERMISSION_ID", insertable = false, updatable = false) + private String permissionId; + + @PostPersist + void postPersist() { + roleId = role.getId(); + permissionId = permission.getId(); + } +} diff --git a/src/main/java/io/github/onecx/permission/domain/models/WorkspacePermission.java b/src/main/java/io/github/onecx/permission/domain/models/WorkspacePermission.java new file mode 100644 index 0000000..9f4b839 --- /dev/null +++ b/src/main/java/io/github/onecx/permission/domain/models/WorkspacePermission.java @@ -0,0 +1,49 @@ +package io.github.onecx.permission.domain.models; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; + +import org.hibernate.annotations.TenantId; +import org.tkit.quarkus.jpa.models.TraceableEntity; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@Entity +@Table(name = "WORKSPACE_PERMISSION", uniqueConstraints = { + @UniqueConstraint(name = "WORKSPACE_PERMISSION_KEY", columnNames = { "TENANT_ID", "WORKSPACE_ID", "RESOURCE", + "ACTION" }) +}) +@SuppressWarnings("squid:S2160") +public class WorkspacePermission extends TraceableEntity { + + @Column(name = "WORKSPACE_ID") + private String workspaceId; + + @TenantId + @Column(name = "TENANT_ID") + private String tenantId; + + /** + * The permission action. + */ + @Column(name = "ACTION") + private String action; + + /** + * The permission object. + */ + @Column(name = "RESOURCE") + private String resource; + + /** + * The permission description. + */ + @Column(name = "DESCRIPTION") + private String description; + +} diff --git a/src/main/java/io/github/onecx/permission/rs/external/v1/controllers/PermissionRestController.java b/src/main/java/io/github/onecx/permission/rs/external/v1/controllers/PermissionRestController.java new file mode 100644 index 0000000..cb62ed4 --- /dev/null +++ b/src/main/java/io/github/onecx/permission/rs/external/v1/controllers/PermissionRestController.java @@ -0,0 +1,70 @@ +package io.github.onecx.permission.rs.external.v1.controllers; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.validation.ConstraintViolationException; +import jakarta.ws.rs.core.Response; + +import org.jboss.resteasy.reactive.RestResponse; +import org.jboss.resteasy.reactive.server.ServerExceptionMapper; +import org.tkit.quarkus.log.cdi.LogExclude; +import org.tkit.quarkus.log.cdi.LogService; + +import gen.io.github.onecx.permission.rs.external.v1.PermissionApiV1; +import gen.io.github.onecx.permission.rs.external.v1.model.PermissionRequestDTOV1; +import gen.io.github.onecx.permission.rs.external.v1.model.ProblemDetailResponseDTOV1; +import io.github.onecx.permission.common.services.TokenService; +import io.github.onecx.permission.domain.daos.PermissionDAO; +import io.github.onecx.permission.domain.daos.WorkspacePermissionDAO; +import io.github.onecx.permission.rs.external.v1.mappers.ExceptionMapper; +import io.github.onecx.permission.rs.external.v1.mappers.PermissionMapper; + +@LogService +@ApplicationScoped +public class PermissionRestController implements PermissionApiV1 { + + @Inject + TokenService tokenService; + + @Inject + PermissionDAO permissionDAO; + + @Inject + PermissionMapper mapper; + + @Inject + WorkspacePermissionDAO workspacePermissionDAO; + + @Inject + ExceptionMapper exceptionMapper; + + @Override + public Response getApplicationPermissions(String appId, @LogExclude PermissionRequestDTOV1 permissionRequestDTOV1) { + var roles = tokenService.getTokenRoles(permissionRequestDTOV1.getToken()); + var permissions = permissionDAO.findPermissionForUser(appId, roles); + return Response.ok(mapper.create(appId, permissions)).build(); + } + + @Override + public Response getWorkspacePermission(String workspace, @LogExclude PermissionRequestDTOV1 permissionRequestDTOV1) { + var roles = tokenService.getTokenRoles(permissionRequestDTOV1.getToken()); + var permissions = workspacePermissionDAO.findWorkspacePermissionForUser(workspace, roles); + return Response.ok(mapper.createWorkspace(workspace, permissions)).build(); + } + + @Override + public Response getWorkspacePermissionApplications(String workspace, + @LogExclude PermissionRequestDTOV1 permissionRequestDTOV1) { + + var roles = tokenService.getTokenRoles(permissionRequestDTOV1.getToken()); + var workspacePermissions = workspacePermissionDAO.findWorkspacePermissionForUser(workspace, roles); + var permissions = permissionDAO.findAllPermissionForUser(roles); + + return Response.ok(mapper.createWorkspaceApps(workspace, workspacePermissions, permissions)).build(); + } + + @ServerExceptionMapper + public RestResponse constraint(ConstraintViolationException ex) { + return exceptionMapper.constraint(ex); + } +} diff --git a/src/main/java/io/github/onecx/permission/rs/external/v1/mappers/ExceptionMapper.java b/src/main/java/io/github/onecx/permission/rs/external/v1/mappers/ExceptionMapper.java new file mode 100644 index 0000000..713a58d --- /dev/null +++ b/src/main/java/io/github/onecx/permission/rs/external/v1/mappers/ExceptionMapper.java @@ -0,0 +1,68 @@ +package io.github.onecx.permission.rs.external.v1.mappers; + +import java.util.List; +import java.util.Map; +import java.util.Set; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import jakarta.validation.Path; +import jakarta.ws.rs.core.Response; + +import org.jboss.resteasy.reactive.RestResponse; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +import gen.io.github.onecx.permission.rs.external.v1.model.ProblemDetailInvalidParamDTOV1; +import gen.io.github.onecx.permission.rs.external.v1.model.ProblemDetailParamDTOV1; +import gen.io.github.onecx.permission.rs.external.v1.model.ProblemDetailResponseDTOV1; + +@Mapper +public abstract class ExceptionMapper { + + @Mapping(target = "removeParamsItem", ignore = true) + @Mapping(target = "params", ignore = true) + @Mapping(target = "invalidParams", ignore = true) + @Mapping(target = "removeInvalidParamsItem", ignore = true) + public abstract ProblemDetailResponseDTOV1 exception(String errorCode, String detail); + + public RestResponse constraint(ConstraintViolationException ex) { + var dto = exception(ErrorKeys.CONSTRAINT_VIOLATIONS.name(), ex.getMessage()); + dto.setInvalidParams(createErrorValidationResponse(ex.getConstraintViolations())); + return RestResponse.status(Response.Status.BAD_REQUEST, dto); + } + + public abstract List createErrorValidationResponse( + Set> constraintViolation); + + @Mapping(target = "name", source = "propertyPath") + @Mapping(target = "message", source = "message") + public abstract ProblemDetailInvalidParamDTOV1 createError(ConstraintViolation constraintViolation); + + public String mapPath(Path path) { + return path.toString(); + } + + public enum ErrorKeys { + CONSTRAINT_VIOLATIONS; + } + + public List map(Map params) { + if (params == null) { + return List.of(); + } + return params.entrySet().stream().map(e -> { + var item = create(e.getKey()); + if (e.getValue() != null) { + item.setValue(e.getValue().toString()); + } + return item; + }).toList(); + } + + private ProblemDetailParamDTOV1 create(String key) { + var item = new ProblemDetailParamDTOV1(); + item.setKey(key); + return item; + } +} diff --git a/src/main/java/io/github/onecx/permission/rs/external/v1/mappers/PermissionMapper.java b/src/main/java/io/github/onecx/permission/rs/external/v1/mappers/PermissionMapper.java new file mode 100644 index 0000000..c3f5b00 --- /dev/null +++ b/src/main/java/io/github/onecx/permission/rs/external/v1/mappers/PermissionMapper.java @@ -0,0 +1,68 @@ +package io.github.onecx.permission.rs.external.v1.mappers; + +import java.util.*; + +import org.mapstruct.Mapper; + +import gen.io.github.onecx.permission.rs.external.v1.model.ApplicationPermissionsDTOV1; +import gen.io.github.onecx.permission.rs.external.v1.model.WorkspacePermissionApplicationsDTOV1; +import gen.io.github.onecx.permission.rs.external.v1.model.WorkspacePermissionsDTOV1; +import io.github.onecx.permission.domain.models.Permission; +import io.github.onecx.permission.domain.models.WorkspacePermission; + +@Mapper +public interface PermissionMapper { + + default WorkspacePermissionApplicationsDTOV1 createWorkspaceApps(String workspaceId, + List workspacePermissions, List permissions) { + return new WorkspacePermissionApplicationsDTOV1() + .workspace(createWorkspace(workspaceId, workspacePermissions)) + .applications(splitToApps(permissions)); + } + + default List splitToApps(List permissions) { + if (permissions == null) { + return List.of(); + } + Map> items = new HashMap<>(); + permissions.forEach(permission -> items.computeIfAbsent(permission.getAppId(), k -> new ArrayList<>()) + .add(permission)); + return items.entrySet().stream().map(e -> create(e.getKey(), e.getValue())).toList(); + } + + default Map> permissions(List permissions) { + if (permissions == null) { + return Map.of(); + } + Map> result = new HashMap<>(); + permissions.forEach(permission -> result.computeIfAbsent(permission.getResource(), k -> new HashSet<>()) + .add(permission.getAction())); + return result; + } + + default ApplicationPermissionsDTOV1 create(String appId, Map> permissions) { + return new ApplicationPermissionsDTOV1().appId(appId).permissions(permissions); + } + + default ApplicationPermissionsDTOV1 create(String appId, List permissions) { + return create(appId, permissions(permissions)); + } + + default Map> permissionsWorkspace(List permissions) { + if (permissions == null) { + return Map.of(); + } + Map> result = new HashMap<>(); + permissions.forEach(permission -> result.computeIfAbsent(permission.getResource(), k -> new HashSet<>()) + .add(permission.getAction())); + return result; + } + + default WorkspacePermissionsDTOV1 createWorkspace(String workspaceId, Map> permissions) { + return new WorkspacePermissionsDTOV1().workspaceId(workspaceId).permissions(permissions); + } + + default WorkspacePermissionsDTOV1 createWorkspace(String workspaceId, List permissions) { + return createWorkspace(workspaceId, permissionsWorkspace(permissions)); + } +} diff --git a/src/main/java/io/github/onecx/permission/rs/internal/controllers/AssignmentRestController.java b/src/main/java/io/github/onecx/permission/rs/internal/controllers/AssignmentRestController.java new file mode 100644 index 0000000..be202ac --- /dev/null +++ b/src/main/java/io/github/onecx/permission/rs/internal/controllers/AssignmentRestController.java @@ -0,0 +1,100 @@ +package io.github.onecx.permission.rs.internal.controllers; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import jakarta.validation.ConstraintViolationException; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.UriInfo; + +import org.jboss.resteasy.reactive.RestResponse; +import org.jboss.resteasy.reactive.server.ServerExceptionMapper; +import org.tkit.quarkus.jpa.exceptions.ConstraintException; +import org.tkit.quarkus.log.cdi.LogService; + +import gen.io.github.onecx.permission.rs.internal.AssignmentInternalApi; +import gen.io.github.onecx.permission.rs.internal.model.AssignmentSearchCriteriaDTO; +import gen.io.github.onecx.permission.rs.internal.model.CreateAssignmentRequestDTO; +import gen.io.github.onecx.permission.rs.internal.model.ProblemDetailResponseDTO; +import io.github.onecx.permission.domain.daos.AssignmentDAO; +import io.github.onecx.permission.domain.daos.PermissionDAO; +import io.github.onecx.permission.domain.daos.RoleDAO; +import io.github.onecx.permission.rs.internal.mappers.AssignmentMapper; +import io.github.onecx.permission.rs.internal.mappers.ExceptionMapper; + +@LogService +@ApplicationScoped +public class AssignmentRestController implements AssignmentInternalApi { + + @Inject + AssignmentMapper mapper; + + @Inject + AssignmentDAO dao; + + @Inject + ExceptionMapper exceptionMapper; + + @Context + UriInfo uriInfo; + + @Inject + RoleDAO roleDAO; + + @Inject + PermissionDAO permissionDAO; + + @Override + public Response getAssignment(String id) { + var data = dao.findById(id); + if (data == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + return Response.ok(mapper.map(data)).build(); + } + + @Override + public Response searchAssignments(AssignmentSearchCriteriaDTO assignmentSearchCriteriaDTO) { + var criteria = mapper.map(assignmentSearchCriteriaDTO); + var result = dao.findByCriteria(criteria); + return Response.ok(mapper.map(result)).build(); + } + + @Override + @Transactional + public Response createAssignment(CreateAssignmentRequestDTO createAssignmentRequestDTO) { + var role = roleDAO.findById(createAssignmentRequestDTO.getRoleId()); + if (role == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + var permission = permissionDAO.findById(createAssignmentRequestDTO.getPermissionId()); + if (permission == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + + var data = mapper.create(role, permission); + data = dao.create(data); + return Response + .created(uriInfo.getAbsolutePathBuilder().path(data.getId()).build()) + .entity(mapper.map(data)) + .build(); + } + + @Override + @Transactional + public Response deleteAssignment(String id) { + dao.deleteQueryById(id); + return Response.noContent().build(); + } + + @ServerExceptionMapper + public RestResponse constraint(ConstraintViolationException ex) { + return exceptionMapper.constraint(ex); + } + + @ServerExceptionMapper + public RestResponse exception(ConstraintException ex) { + return exceptionMapper.exception(ex); + } +} diff --git a/src/main/java/io/github/onecx/permission/rs/internal/controllers/RoleRestController.java b/src/main/java/io/github/onecx/permission/rs/internal/controllers/RoleRestController.java new file mode 100644 index 0000000..f54ac19 --- /dev/null +++ b/src/main/java/io/github/onecx/permission/rs/internal/controllers/RoleRestController.java @@ -0,0 +1,97 @@ +package io.github.onecx.permission.rs.internal.controllers; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import jakarta.validation.ConstraintViolationException; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.UriInfo; + +import org.jboss.resteasy.reactive.RestResponse; +import org.jboss.resteasy.reactive.server.ServerExceptionMapper; +import org.tkit.quarkus.jpa.exceptions.ConstraintException; +import org.tkit.quarkus.log.cdi.LogService; + +import gen.io.github.onecx.permission.rs.internal.RoleInternalApi; +import gen.io.github.onecx.permission.rs.internal.model.CreateRoleRequestDTO; +import gen.io.github.onecx.permission.rs.internal.model.ProblemDetailResponseDTO; +import gen.io.github.onecx.permission.rs.internal.model.RoleSearchCriteriaDTO; +import gen.io.github.onecx.permission.rs.internal.model.UpdateRoleRequestDTO; +import io.github.onecx.permission.domain.daos.RoleDAO; +import io.github.onecx.permission.rs.internal.mappers.ExceptionMapper; +import io.github.onecx.permission.rs.internal.mappers.RoleMapper; + +@LogService +@ApplicationScoped +public class RoleRestController implements RoleInternalApi { + + @Inject + ExceptionMapper exceptionMapper; + + @Inject + RoleDAO dao; + + @Inject + RoleMapper mapper; + + @Context + UriInfo uriInfo; + + @Override + @Transactional + public Response createRole(CreateRoleRequestDTO createRoleRequestDTO) { + var role = mapper.create(createRoleRequestDTO); + role = dao.create(role); + return Response + .created(uriInfo.getAbsolutePathBuilder().path(role.getId()).build()) + .entity(mapper.map(role)) + .build(); + } + + @Override + @Transactional + public Response deleteRole(String id) { + dao.deleteQueryById(id); + return Response.noContent().build(); + } + + @Override + public Response getRoleById(String id) { + var role = dao.findById(id); + if (role == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + return Response.ok(mapper.map(role)).build(); + } + + @Override + public Response searchRoles(RoleSearchCriteriaDTO roleSearchCriteriaDTO) { + var criteria = mapper.map(roleSearchCriteriaDTO); + var result = dao.findByCriteria(criteria); + return Response.ok(mapper.mapPage(result)).build(); + } + + @Override + @Transactional + public Response updateRole(String id, UpdateRoleRequestDTO updateRoleRequestDTO) { + var role = dao.findById(id); + if (role == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + + mapper.update(updateRoleRequestDTO, role); + dao.update(role); + return Response.ok(mapper.map(role)).build(); + } + + @ServerExceptionMapper + public RestResponse constraint(ConstraintViolationException ex) { + return exceptionMapper.constraint(ex); + } + + @ServerExceptionMapper + public RestResponse exception(ConstraintException ex) { + return exceptionMapper.exception(ex); + } +} diff --git a/src/main/java/io/github/onecx/permission/rs/internal/controllers/WorkspaceAssignmentRestController.java b/src/main/java/io/github/onecx/permission/rs/internal/controllers/WorkspaceAssignmentRestController.java new file mode 100644 index 0000000..a0d1e12 --- /dev/null +++ b/src/main/java/io/github/onecx/permission/rs/internal/controllers/WorkspaceAssignmentRestController.java @@ -0,0 +1,97 @@ +package io.github.onecx.permission.rs.internal.controllers; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.validation.ConstraintViolationException; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.UriInfo; + +import org.jboss.resteasy.reactive.RestResponse; +import org.jboss.resteasy.reactive.server.ServerExceptionMapper; +import org.tkit.quarkus.jpa.exceptions.ConstraintException; +import org.tkit.quarkus.log.cdi.LogService; + +import gen.io.github.onecx.permission.rs.internal.WorkspaceAssignmentInternalApi; +import gen.io.github.onecx.permission.rs.internal.model.CreateWorkspaceAssignmentRequestDTO; +import gen.io.github.onecx.permission.rs.internal.model.ProblemDetailResponseDTO; +import gen.io.github.onecx.permission.rs.internal.model.WorkspaceAssignmentSearchCriteriaDTO; +import io.github.onecx.permission.domain.daos.RoleDAO; +import io.github.onecx.permission.domain.daos.WorkspaceAssignmentDAO; +import io.github.onecx.permission.domain.daos.WorkspacePermissionDAO; +import io.github.onecx.permission.rs.internal.mappers.ExceptionMapper; +import io.github.onecx.permission.rs.internal.mappers.WorkspaceAssignmentMapper; + +@LogService +@ApplicationScoped +public class WorkspaceAssignmentRestController implements WorkspaceAssignmentInternalApi { + + @Inject + ExceptionMapper exceptionMapper; + + @Inject + RoleDAO roleDAO; + + @Inject + WorkspacePermissionDAO workspacePermissionDAO; + + @Inject + WorkspaceAssignmentMapper mapper; + + @Context + UriInfo uriInfo; + + @Inject + WorkspaceAssignmentDAO dao; + + @Override + public Response createWorkspaceAssignment(CreateWorkspaceAssignmentRequestDTO createWorkspaceAssignmentRequestDTO) { + var role = roleDAO.findById(createWorkspaceAssignmentRequestDTO.getRoleId()); + if (role == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + var permission = workspacePermissionDAO.findById(createWorkspaceAssignmentRequestDTO.getPermissionId()); + if (permission == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + + var data = mapper.create(role, permission); + data = dao.create(data); + return Response + .created(uriInfo.getAbsolutePathBuilder().path(data.getId()).build()) + .entity(mapper.map(data)) + .build(); + } + + @Override + public Response deleteWorkspaceAssignment(String id) { + dao.deleteQueryById(id); + return Response.noContent().build(); + } + + @Override + public Response getWorkspaceAssignment(String id) { + var data = dao.findById(id); + if (data == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + return Response.ok(mapper.map(data)).build(); + } + + @Override + public Response searchWorkspaceAssignments(WorkspaceAssignmentSearchCriteriaDTO workspaceAssignmentSearchCriteriaDTO) { + var criteria = mapper.map(workspaceAssignmentSearchCriteriaDTO); + var result = dao.findByCriteria(criteria); + return Response.ok(mapper.map(result)).build(); + } + + @ServerExceptionMapper + public RestResponse constraint(ConstraintViolationException ex) { + return exceptionMapper.constraint(ex); + } + + @ServerExceptionMapper + public RestResponse exception(ConstraintException ex) { + return exceptionMapper.exception(ex); + } +} diff --git a/src/main/java/io/github/onecx/permission/rs/internal/controllers/WorkspacePermissionRestController.java b/src/main/java/io/github/onecx/permission/rs/internal/controllers/WorkspacePermissionRestController.java new file mode 100644 index 0000000..2778f9a --- /dev/null +++ b/src/main/java/io/github/onecx/permission/rs/internal/controllers/WorkspacePermissionRestController.java @@ -0,0 +1,94 @@ +package io.github.onecx.permission.rs.internal.controllers; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.validation.ConstraintViolationException; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.UriInfo; + +import org.jboss.resteasy.reactive.RestResponse; +import org.jboss.resteasy.reactive.server.ServerExceptionMapper; +import org.tkit.quarkus.jpa.exceptions.ConstraintException; +import org.tkit.quarkus.log.cdi.LogService; + +import gen.io.github.onecx.permission.rs.internal.WorkspacePermissionInternalApi; +import gen.io.github.onecx.permission.rs.internal.model.CreateWorkspacePermissionRequestDTO; +import gen.io.github.onecx.permission.rs.internal.model.ProblemDetailResponseDTO; +import gen.io.github.onecx.permission.rs.internal.model.UpdateWorkspacePermissionRequestDTO; +import gen.io.github.onecx.permission.rs.internal.model.WorkspacePermissionSearchCriteriaDTO; +import io.github.onecx.permission.domain.daos.WorkspacePermissionDAO; +import io.github.onecx.permission.rs.internal.mappers.ExceptionMapper; +import io.github.onecx.permission.rs.internal.mappers.WorkspacePermissionMapper; + +@LogService +@ApplicationScoped +public class WorkspacePermissionRestController implements WorkspacePermissionInternalApi { + + @Inject + ExceptionMapper exceptionMapper; + + @Inject + WorkspacePermissionMapper mapper; + + @Inject + WorkspacePermissionDAO dao; + + @Context + UriInfo uriInfo; + + @Override + public Response createWorkspacePermission(CreateWorkspacePermissionRequestDTO createWorkspacePermissionRequestDTO) { + var item = mapper.create(createWorkspacePermissionRequestDTO); + item = dao.create(item); + return Response + .created(uriInfo.getAbsolutePathBuilder().path(item.getId()).build()) + .entity(mapper.map(item)) + .build(); + } + + @Override + public Response deleteWorkspacePermission(String id) { + dao.deleteQueryById(id); + return Response.noContent().build(); + } + + @Override + public Response getWorkspacePermissionById(String id) { + var data = dao.findById(id); + if (data == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + return Response.ok(mapper.map(data)).build(); + } + + @Override + public Response searchWorkspacePermissions(WorkspacePermissionSearchCriteriaDTO workspacePermissionSearchCriteriaDTO) { + var criteria = mapper.map(workspacePermissionSearchCriteriaDTO); + var result = dao.findByCriteria(criteria); + return Response.ok(mapper.map(result)).build(); + } + + @Override + public Response updateWorkspacePermission(String id, + UpdateWorkspacePermissionRequestDTO updateWorkspacePermissionRequestDTO) { + var item = dao.findById(id); + if (item == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + + mapper.update(updateWorkspacePermissionRequestDTO, item); + dao.update(item); + return Response.ok(mapper.map(item)).build(); + } + + @ServerExceptionMapper + public RestResponse constraint(ConstraintViolationException ex) { + return exceptionMapper.constraint(ex); + } + + @ServerExceptionMapper + public RestResponse exception(ConstraintException ex) { + return exceptionMapper.exception(ex); + } +} diff --git a/src/main/java/io/github/onecx/permission/rs/internal/log/InternalLogParam.java b/src/main/java/io/github/onecx/permission/rs/internal/log/InternalLogParam.java index 9cddc46..b47759d 100644 --- a/src/main/java/io/github/onecx/permission/rs/internal/log/InternalLogParam.java +++ b/src/main/java/io/github/onecx/permission/rs/internal/log/InternalLogParam.java @@ -6,7 +6,7 @@ import org.tkit.quarkus.log.cdi.LogParam; -import gen.io.github.onecx.permission.rs.internal.model.PermissionSearchCriteriaDTO; +import gen.io.github.onecx.permission.rs.internal.model.*; @ApplicationScoped public class InternalLogParam implements LogParam { @@ -18,6 +18,25 @@ public List getClasses() { PermissionSearchCriteriaDTO d = (PermissionSearchCriteriaDTO) x; return PermissionSearchCriteriaDTO.class.getSimpleName() + "[" + d.getPageNumber() + "," + d.getPageSize() + "]"; - })); + }), + item(10, AssignmentSearchCriteriaDTO.class, x -> { + AssignmentSearchCriteriaDTO d = (AssignmentSearchCriteriaDTO) x; + return AssignmentSearchCriteriaDTO.class.getSimpleName() + "[" + d.getPageNumber() + "," + d.getPageSize() + + "]"; + }), + item(10, RoleSearchCriteriaDTO.class, x -> { + RoleSearchCriteriaDTO d = (RoleSearchCriteriaDTO) x; + return RoleSearchCriteriaDTO.class.getSimpleName() + "[" + d.getPageNumber() + "," + d.getPageSize() + + "]"; + }), + item(10, CreateAssignmentRequestDTO.class, x -> { + CreateAssignmentRequestDTO d = (CreateAssignmentRequestDTO) x; + return CreateAssignmentRequestDTO.class.getSimpleName() + ":r=" + d.getRoleId() + ",p=" + + d.getPermissionId(); + }), + item(10, CreateRoleRequestDTO.class, + x -> x.getClass().getSimpleName() + ":" + ((CreateRoleRequestDTO) x).getName()), + item(10, UpdateRoleRequestDTO.class, + x -> x.getClass().getSimpleName() + ":" + ((UpdateRoleRequestDTO) x).getName())); } } diff --git a/src/main/java/io/github/onecx/permission/rs/internal/mappers/AssignmentMapper.java b/src/main/java/io/github/onecx/permission/rs/internal/mappers/AssignmentMapper.java new file mode 100644 index 0000000..b00b583 --- /dev/null +++ b/src/main/java/io/github/onecx/permission/rs/internal/mappers/AssignmentMapper.java @@ -0,0 +1,38 @@ +package io.github.onecx.permission.rs.internal.mappers; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.tkit.quarkus.jpa.daos.PageResult; +import org.tkit.quarkus.rs.mappers.OffsetDateTimeMapper; + +import gen.io.github.onecx.permission.rs.internal.model.AssignmentDTO; +import gen.io.github.onecx.permission.rs.internal.model.AssignmentPageResultDTO; +import gen.io.github.onecx.permission.rs.internal.model.AssignmentSearchCriteriaDTO; +import io.github.onecx.permission.domain.criteria.AssignmentSearchCriteria; +import io.github.onecx.permission.domain.models.Assignment; +import io.github.onecx.permission.domain.models.Permission; +import io.github.onecx.permission.domain.models.Role; + +@Mapper(uses = { OffsetDateTimeMapper.class }) +public interface AssignmentMapper { + + @Mapping(target = "removeStreamItem", ignore = true) + AssignmentPageResultDTO map(PageResult page); + + AssignmentSearchCriteria map(AssignmentSearchCriteriaDTO dto); + + @Mapping(target = "id", ignore = true) + @Mapping(target = "creationDate", ignore = true) + @Mapping(target = "creationUser", ignore = true) + @Mapping(target = "modificationDate", ignore = true) + @Mapping(target = "modificationUser", ignore = true) + @Mapping(target = "controlTraceabilityManual", ignore = true) + @Mapping(target = "modificationCount", ignore = true) + @Mapping(target = "persisted", ignore = true) + @Mapping(target = "tenantId", ignore = true) + @Mapping(target = "roleId", ignore = true) + @Mapping(target = "permissionId", ignore = true) + Assignment create(Role role, Permission permission); + + AssignmentDTO map(Assignment data); +} diff --git a/src/main/java/io/github/onecx/permission/rs/internal/mappers/ExceptionMapper.java b/src/main/java/io/github/onecx/permission/rs/internal/mappers/ExceptionMapper.java index 9e3fe83..7badaea 100644 --- a/src/main/java/io/github/onecx/permission/rs/internal/mappers/ExceptionMapper.java +++ b/src/main/java/io/github/onecx/permission/rs/internal/mappers/ExceptionMapper.java @@ -12,6 +12,7 @@ import org.jboss.resteasy.reactive.RestResponse; import org.mapstruct.Mapper; import org.mapstruct.Mapping; +import org.tkit.quarkus.jpa.exceptions.ConstraintException; import org.tkit.quarkus.rs.mappers.OffsetDateTimeMapper; import gen.io.github.onecx.permission.rs.internal.model.ProblemDetailInvalidParamDTO; @@ -27,6 +28,12 @@ public RestResponse constraint(ConstraintViolationExce return RestResponse.status(Response.Status.BAD_REQUEST, dto); } + public RestResponse exception(ConstraintException ex) { + var dto = exception(ex.getMessageKey().name(), ex.getConstraints()); + dto.setParams(map(ex.namedParameters)); + return RestResponse.status(Response.Status.BAD_REQUEST, dto); + } + @Mapping(target = "removeParamsItem", ignore = true) @Mapping(target = "params", ignore = true) @Mapping(target = "invalidParams", ignore = true) diff --git a/src/main/java/io/github/onecx/permission/rs/internal/mappers/RoleMapper.java b/src/main/java/io/github/onecx/permission/rs/internal/mappers/RoleMapper.java new file mode 100644 index 0000000..d6c4bb4 --- /dev/null +++ b/src/main/java/io/github/onecx/permission/rs/internal/mappers/RoleMapper.java @@ -0,0 +1,43 @@ +package io.github.onecx.permission.rs.internal.mappers; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; +import org.tkit.quarkus.jpa.daos.PageResult; +import org.tkit.quarkus.rs.mappers.OffsetDateTimeMapper; + +import gen.io.github.onecx.permission.rs.internal.model.*; +import io.github.onecx.permission.domain.criteria.RoleSearchCriteria; +import io.github.onecx.permission.domain.models.Role; + +@Mapper(uses = { OffsetDateTimeMapper.class }) +public interface RoleMapper { + + @Mapping(target = "removeStreamItem", ignore = true) + public abstract RolePageResultDTO mapPage(PageResult page); + + RoleSearchCriteria map(RoleSearchCriteriaDTO dto); + + @Mapping(target = "id", ignore = true) + @Mapping(target = "creationDate", ignore = true) + @Mapping(target = "creationUser", ignore = true) + @Mapping(target = "modificationDate", ignore = true) + @Mapping(target = "modificationUser", ignore = true) + @Mapping(target = "controlTraceabilityManual", ignore = true) + @Mapping(target = "modificationCount", ignore = true) + @Mapping(target = "persisted", ignore = true) + @Mapping(target = "tenantId", ignore = true) + Role create(CreateRoleRequestDTO dto); + + RoleDTO map(Role data); + + @Mapping(target = "id", ignore = true) + @Mapping(target = "creationDate", ignore = true) + @Mapping(target = "creationUser", ignore = true) + @Mapping(target = "modificationDate", ignore = true) + @Mapping(target = "modificationUser", ignore = true) + @Mapping(target = "controlTraceabilityManual", ignore = true) + @Mapping(target = "persisted", ignore = true) + @Mapping(target = "tenantId", ignore = true) + void update(UpdateRoleRequestDTO dto, @MappingTarget Role role); +} diff --git a/src/main/java/io/github/onecx/permission/rs/internal/mappers/WorkspaceAssignmentMapper.java b/src/main/java/io/github/onecx/permission/rs/internal/mappers/WorkspaceAssignmentMapper.java new file mode 100644 index 0000000..fef7a98 --- /dev/null +++ b/src/main/java/io/github/onecx/permission/rs/internal/mappers/WorkspaceAssignmentMapper.java @@ -0,0 +1,36 @@ +package io.github.onecx.permission.rs.internal.mappers; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.tkit.quarkus.jpa.daos.PageResult; +import org.tkit.quarkus.rs.mappers.OffsetDateTimeMapper; + +import gen.io.github.onecx.permission.rs.internal.model.*; +import io.github.onecx.permission.domain.criteria.WorkspaceAssignmentSearchCriteria; +import io.github.onecx.permission.domain.models.Role; +import io.github.onecx.permission.domain.models.WorkspaceAssignment; +import io.github.onecx.permission.domain.models.WorkspacePermission; + +@Mapper(uses = { OffsetDateTimeMapper.class }) +public interface WorkspaceAssignmentMapper { + + @Mapping(target = "removeStreamItem", ignore = true) + WorkspaceAssignmentPageResultDTO map(PageResult page); + + WorkspaceAssignmentSearchCriteria map(WorkspaceAssignmentSearchCriteriaDTO dto); + + @Mapping(target = "id", ignore = true) + @Mapping(target = "creationDate", ignore = true) + @Mapping(target = "creationUser", ignore = true) + @Mapping(target = "modificationDate", ignore = true) + @Mapping(target = "modificationUser", ignore = true) + @Mapping(target = "controlTraceabilityManual", ignore = true) + @Mapping(target = "modificationCount", ignore = true) + @Mapping(target = "persisted", ignore = true) + @Mapping(target = "tenantId", ignore = true) + @Mapping(target = "roleId", ignore = true) + @Mapping(target = "permissionId", ignore = true) + WorkspaceAssignment create(Role role, WorkspacePermission permission); + + WorkspaceAssignmentDTO map(WorkspaceAssignment data); +} diff --git a/src/main/java/io/github/onecx/permission/rs/internal/mappers/WorkspacePermissionMapper.java b/src/main/java/io/github/onecx/permission/rs/internal/mappers/WorkspacePermissionMapper.java new file mode 100644 index 0000000..bdc042d --- /dev/null +++ b/src/main/java/io/github/onecx/permission/rs/internal/mappers/WorkspacePermissionMapper.java @@ -0,0 +1,46 @@ +package io.github.onecx.permission.rs.internal.mappers; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; +import org.tkit.quarkus.jpa.daos.PageResult; +import org.tkit.quarkus.rs.mappers.OffsetDateTimeMapper; + +import gen.io.github.onecx.permission.rs.internal.model.*; +import io.github.onecx.permission.domain.criteria.WorkspacePermissionSearchCriteria; +import io.github.onecx.permission.domain.models.WorkspacePermission; + +@Mapper(uses = { OffsetDateTimeMapper.class }) +public interface WorkspacePermissionMapper { + + WorkspacePermissionSearchCriteria map(WorkspacePermissionSearchCriteriaDTO dto); + + @Mapping(target = "id", ignore = true) + @Mapping(target = "creationDate", ignore = true) + @Mapping(target = "creationUser", ignore = true) + @Mapping(target = "modificationDate", ignore = true) + @Mapping(target = "modificationUser", ignore = true) + @Mapping(target = "controlTraceabilityManual", ignore = true) + @Mapping(target = "persisted", ignore = true) + @Mapping(target = "tenantId", ignore = true) + @Mapping(target = "workspaceId", ignore = true) + @Mapping(target = "action", ignore = true) + @Mapping(target = "resource", ignore = true) + void update(UpdateWorkspacePermissionRequestDTO dto, @MappingTarget WorkspacePermission data); + + @Mapping(target = "id", ignore = true) + @Mapping(target = "creationDate", ignore = true) + @Mapping(target = "creationUser", ignore = true) + @Mapping(target = "modificationDate", ignore = true) + @Mapping(target = "modificationUser", ignore = true) + @Mapping(target = "controlTraceabilityManual", ignore = true) + @Mapping(target = "modificationCount", ignore = true) + @Mapping(target = "persisted", ignore = true) + @Mapping(target = "tenantId", ignore = true) + WorkspacePermission create(CreateWorkspacePermissionRequestDTO dto); + + @Mapping(target = "removeStreamItem", ignore = true) + WorkspacePermissionPageResultDTO map(PageResult page); + + WorkspacePermissionDTO map(WorkspacePermission data); +} diff --git a/src/main/java/io/github/onecx/permission/rs/operator/v1/controllers/OperatorRestController.java b/src/main/java/io/github/onecx/permission/rs/operator/v1/controllers/OperatorRestController.java index 7de4280..910a733 100644 --- a/src/main/java/io/github/onecx/permission/rs/operator/v1/controllers/OperatorRestController.java +++ b/src/main/java/io/github/onecx/permission/rs/operator/v1/controllers/OperatorRestController.java @@ -53,7 +53,6 @@ public Response createOrUpdatePermission(String appId, PermissionRequestDTOV1 pe } else { // update existing permission permission.setDescription(item.getDescription()); - permission.setName(item.getName()); dao.update(permission); } } diff --git a/src/main/openapi/onecx-permission-di-v1.yaml b/src/main/openapi/onecx-permission-di-v1.yaml new file mode 100644 index 0000000..ba6049b --- /dev/null +++ b/src/main/openapi/onecx-permission-di-v1.yaml @@ -0,0 +1,88 @@ +--- +openapi: 3.0.3 +info: + title: onecx-permission permission import from file during the start of the application + version: 1.0.0 +servers: + - url: "http://localhost" +paths: + /import/permission: + post: + operationId: importPermission + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/DataImport' + responses: + 200: + description: ok +components: + schemas: + DataImport: + type: object + properties: + tenants: + $ref: '#/components/schemas/DataImportTenant' + permissions: + $ref: '#/components/schemas/DataImportPermissionWrapper' + DataImportPermissionWrapper: + type: object + nullable: false + description: application or workspace id + additionalProperties: + $ref: '#/components/schemas/DataImportPermission' + DataImportPermission: + type: object + nullable: false + description: resource id + additionalProperties: + $ref: '#/components/schemas/DataImportPermissionAction' + DataImportPermissionAction: + type: object + nullable: false + description: action id + additionalProperties: + type: string + DataImportTenant: + type: object + nullable: false + description: tenant id + additionalProperties: + $ref: '#/components/schemas/DataImportTenantWrapper' + DataImportTenantWrapper: + type: object + properties: + workspaces-permissions: + $ref: '#/components/schemas/DataImportPermissionWrapper' + roles: + $ref: '#/components/schemas/DataImportRoleWrapper' + DataImportRoleWrapper: + type: object + nullable: false + description: role name + additionalProperties: + $ref: '#/components/schemas/DataImportRole' + DataImportRole: + type: object + properties: + description: + type: string + assignments: + $ref: '#/components/schemas/DataImportAssignmentWrapper' + workspaces-assignments: + $ref: '#/components/schemas/DataImportAssignmentWrapper' + DataImportAssignmentWrapper: + type: object + nullable: false + description: application or workspaces id + additionalProperties: + $ref: '#/components/schemas/DataImportAssignment' + DataImportAssignment: + type: object + nullable: false + description: action + additionalProperties: + type: array + items: + type: string \ No newline at end of file diff --git a/src/main/openapi/onecx-permission-internal-openapi.yaml b/src/main/openapi/onecx-permission-internal-openapi.yaml index ca80c07..e2599e5 100644 --- a/src/main/openapi/onecx-permission-internal-openapi.yaml +++ b/src/main/openapi/onecx-permission-internal-openapi.yaml @@ -7,7 +7,451 @@ servers: - url: "http://onecx-permission-svc:8080" tags: - name: permissionInternal + - name: roleInternal + - name: assignmentInternal + - name: workspaceAssignmentInternal + - name: workspacePermissionInternal paths: + /internal/workspace/permissions/search: + post: + tags: + - workspacePermissionInternal + description: Search for workspace permissions + operationId: searchWorkspacePermissions + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/WorkspacePermissionSearchCriteria' + responses: + 200: + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/WorkspacePermissionPageResult' + 400: + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetailResponse' + /internal/workspace/permissions: + post: + tags: + - workspacePermissionInternal + description: Create workspace permission + operationId: createWorkspacePermission + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateWorkspacePermissionRequest' + responses: + 201: + description: New role created + headers: + Location: + required: true + schema: + type: string + format: url + content: + application/json: + schema: + $ref: '#/components/schemas/WorkspacePermission' + 400: + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetailResponse' + /internal/workspace/permissions/{id}: + get: + tags: + - workspacePermissionInternal + description: Return Workspace permission by ID + operationId: getWorkspacePermissionById + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/WorkspacePermission' + 404: + description: Workspace permission not found + put: + tags: + - workspacePermissionInternal + description: Update workspace permission by ID + operationId: updateWorkspacePermission + parameters: + - name: id + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateWorkspacePermissionRequest' + responses: + 204: + description: Theme updated + content: + application/json: + schema: + $ref: '#/components/schemas/WorkspacePermission' + 400: + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetailResponse' + 404: + description: Workspace permission not found + delete: + tags: + - workspacePermissionInternal + description: Delete workspace permission by ID + operationId: deleteWorkspacePermission + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + 204: + description: Workspace permission deleted + /internal/workspace/assignments/search: + post: + tags: + - workspaceAssignmentInternal + description: Search for workspace assignments + operationId: searchWorkspaceAssignments + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/WorkspaceAssignmentSearchCriteria' + responses: + 200: + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/WorkspaceAssignmentPageResult' + 400: + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetailResponse' + /internal/workspace/assignments: + post: + tags: + - workspaceAssignmentInternal + description: Create new workspace assignment + operationId: createWorkspaceAssignment + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateWorkspaceAssignmentRequest' + responses: + 201: + description: New workspace assignment created + headers: + Location: + required: true + schema: + type: string + format: url + content: + application/json: + schema: + $ref: '#/components/schemas/WorkspaceAssignment' + 404: + description: Data not found + 400: + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetailResponse' + /internal/workspace/assignments/{id}: + get: + tags: + - workspaceAssignmentInternal + description: Get workspace assignment + operationId: getWorkspaceAssignment + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + 200: + description: Get workspace assignment + content: + application/json: + schema: + $ref: '#/components/schemas/WorkspaceAssignment' + 404: + description: Workspace assignment not found + delete: + tags: + - workspaceAssignmentInternal + description: Delete workspace assignment + operationId: deleteWorkspaceAssignment + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + 204: + description: Workspace assignment deleted + /internal/assignments/search: + post: + tags: + - assignmentInternal + description: Search for assignments + operationId: searchAssignments + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AssignmentSearchCriteria' + responses: + 200: + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/AssignmentPageResult' + 400: + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetailResponse' + /internal/assignments: + post: + tags: + - assignmentInternal + description: Create new assignment + operationId: createAssignment + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateAssignmentRequest' + responses: + 201: + description: New assignment created + headers: + Location: + required: true + schema: + type: string + format: url + content: + application/json: + schema: + $ref: '#/components/schemas/Assignment' + 404: + description: Data not found + 400: + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetailResponse' + /internal/assignments/{id}: + get: + tags: + - assignmentInternal + description: Get assignment + operationId: getAssignment + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + 200: + description: Get assignment + content: + application/json: + schema: + $ref: '#/components/schemas/Assignment' + 404: + description: Assignment not found + delete: + tags: + - assignmentInternal + description: Delete assignment + operationId: deleteAssignment + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + 204: + description: Assignment deleted + /internal/roles: + post: + tags: + - roleInternal + description: Create new role + operationId: createRole + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateRoleRequest' + responses: + 201: + description: New role created + headers: + Location: + required: true + schema: + type: string + format: url + content: + application/json: + schema: + $ref: '#/components/schemas/Role' + 400: + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetailResponse' + /internal/roles/{id}: + get: + tags: + - roleInternal + description: Return role by ID + operationId: getRoleById + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + 200: + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Role' + 404: + description: Role not found + put: + tags: + - roleInternal + description: Update role by ID + operationId: updateRole + parameters: + - name: id + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateRoleRequest' + responses: + 204: + description: Theme updated + content: + application/json: + schema: + $ref: '#/components/schemas/Role' + 400: + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetailResponse' + 404: + description: Role not found + delete: + tags: + - roleInternal + description: Delete role by ID + operationId: deleteRole + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + 204: + description: Role deleted + /internal/roles/search: + post: + tags: + - roleInternal + description: Search for roles + operationId: searchRoles + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RoleSearchCriteria' + responses: + 200: + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/RolePageResult' + 400: + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetailResponse' /internal/permissions/search: post: tags: @@ -21,7 +465,7 @@ paths: schema: $ref: '#/components/schemas/PermissionSearchCriteria' responses: - "200": + 200: description: OK content: application/json: @@ -29,7 +473,7 @@ paths: type: array items: $ref: '#/components/schemas/PermissionPageResult' - "400": + 400: description: Bad request content: application/json: @@ -37,17 +481,290 @@ paths: $ref: '#/components/schemas/ProblemDetailResponse' components: schemas: - PermissionSearchCriteria: + UpdateWorkspacePermissionRequest: type: object + required: + - description properties: - appId: + modificationCount: + format: int32 + type: integer + description: type: string - name: + CreateWorkspacePermissionRequest: + type: object + required: + - resource + - action + - workspaceId + properties: + workspaceId: + type: string + resource: + type: string + action: + type: string + description: + type: string + WorkspacePermissionSearchCriteria: + type: object + properties: + workspaceId: + type: string + pageNumber: + format: int32 + description: The number of page. + default: 0 + type: integer + pageSize: + format: int32 + description: The size of page + default: 100 + type: integer + WorkspacePermissionPageResult: + type: object + properties: + totalElements: + format: int64 + description: The total elements in the resource. + type: integer + number: + format: int32 + type: integer + size: + format: int32 + type: integer + totalPages: + format: int64 + type: integer + stream: + type: array + items: + $ref: '#/components/schemas/WorkspacePermission' + WorkspacePermission: + type: object + required: + - resource + - action + - workspaceId + properties: + modificationCount: + format: int32 + type: integer + creationDate: + $ref: '#/components/schemas/OffsetDateTime' + creationUser: + type: string + modificationDate: + $ref: '#/components/schemas/OffsetDateTime' + modificationUser: + type: string + id: + type: string + workspaceId: type: string resource: type: string action: type: string + description: + type: string + CreateWorkspaceAssignmentRequest: + type: object + required: + - roleId + - permissionId + properties: + roleId: + type: string + permissionId: + type: string + WorkspaceAssignment: + type: object + properties: + modificationCount: + format: int32 + type: integer + creationDate: + $ref: '#/components/schemas/OffsetDateTime' + creationUser: + type: string + modificationDate: + $ref: '#/components/schemas/OffsetDateTime' + modificationUser: + type: string + id: + type: string + roleId: + type: string + permissionId: + type: string + WorkspaceAssignmentSearchCriteria: + type: object + properties: + workspaceId: + type: string + pageNumber: + format: int32 + description: The number of page. + default: 0 + type: integer + pageSize: + format: int32 + description: The size of page + default: 100 + type: integer + WorkspaceAssignmentPageResult: + type: object + properties: + totalElements: + format: int64 + description: The total elements in the resource. + type: integer + number: + format: int32 + type: integer + size: + format: int32 + type: integer + totalPages: + format: int64 + type: integer + stream: + type: array + items: + $ref: '#/components/schemas/WorkspaceAssignment' + AssignmentSearchCriteria: + type: object + properties: + appId: + type: string + pageNumber: + format: int32 + description: The number of page. + default: 0 + type: integer + pageSize: + format: int32 + description: The size of page + default: 100 + type: integer + AssignmentPageResult: + type: object + properties: + totalElements: + format: int64 + description: The total elements in the resource. + type: integer + number: + format: int32 + type: integer + size: + format: int32 + type: integer + totalPages: + format: int64 + type: integer + stream: + type: array + items: + $ref: '#/components/schemas/Assignment' + CreateAssignmentRequest: + type: object + required: + - roleId + - permissionId + properties: + roleId: + type: string + permissionId: + type: string + Assignment: + type: object + properties: + modificationCount: + format: int32 + type: integer + creationDate: + $ref: '#/components/schemas/OffsetDateTime' + creationUser: + type: string + modificationDate: + $ref: '#/components/schemas/OffsetDateTime' + modificationUser: + type: string + roleId: + type: string + permissionId: + type: string + UpdateRoleRequest: + type: object + properties: + modificationCount: + format: int32 + type: integer + name: + type: string + shortDescription: + type: string + description: + type: string + CreateRoleRequest: + type: object + properties: + name: + type: string + shortDescription: + type: string + description: + type: string + Role: + type: object + properties: + modificationCount: + format: int32 + type: integer + creationDate: + $ref: '#/components/schemas/OffsetDateTime' + creationUser: + type: string + modificationDate: + $ref: '#/components/schemas/OffsetDateTime' + modificationUser: + type: string + id: + type: string + name: + type: string + description: + type: string + RolePageResult: + type: object + properties: + totalElements: + format: int64 + description: The total elements in the resource. + type: integer + number: + format: int32 + type: integer + size: + format: int32 + type: integer + totalPages: + format: int64 + type: integer + stream: + type: array + items: + $ref: '#/components/schemas/Role' + RoleSearchCriteria: + type: object + properties: + name: + type: string + description: + type: string pageNumber: format: int32 description: The number of page. @@ -56,7 +773,22 @@ components: pageSize: format: int32 description: The size of page - default: 10 + default: 100 + type: integer + PermissionSearchCriteria: + type: object + properties: + appId: + type: string + pageNumber: + format: int32 + description: The number of page. + default: 0 + type: integer + pageSize: + format: int32 + description: The size of page + default: 100 type: integer PermissionPageResult: type: object @@ -96,8 +828,6 @@ components: type: string appId: type: string - name: - type: string resource: type: string action: diff --git a/src/main/openapi/onecx-permission-operator-v1.yaml b/src/main/openapi/onecx-permission-operator-v1.yaml index 55b13f4..b017e25 100644 --- a/src/main/openapi/onecx-permission-operator-v1.yaml +++ b/src/main/openapi/onecx-permission-operator-v1.yaml @@ -51,8 +51,6 @@ components: - resource - action properties: - name: - type: string resource: type: string action: diff --git a/src/main/openapi/onecx-permission-v1.yaml b/src/main/openapi/onecx-permission-v1.yaml new file mode 100644 index 0000000..86fcb60 --- /dev/null +++ b/src/main/openapi/onecx-permission-v1.yaml @@ -0,0 +1,187 @@ +--- +openapi: 3.0.3 +info: + title: onecx-permission service + version: 1.0.0 +servers: + - url: "http://onecx-permission-svc:8080" +tags: + - name: permission +paths: + /v1/permissions/user/workspace/{workspace}: + post: + tags: + - permission + description: Get permission of the workspace for the user + operationId: getWorkspacePermission + parameters: + - name: workspace + in: path + description: Workspace ID + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionRequest' + responses: + 200: + description: Workspace permission + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/WorkspacePermissions' + 400: + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetailResponse' + /v1/permissions/user/workspace/{workspace}/applications: + post: + tags: + - permission + description: Get permission of the workspace and application for the user + operationId: getWorkspacePermissionApplications + parameters: + - name: workspace + in: path + description: Workspace ID + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionRequest' + responses: + 200: + description: Workspace permission + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/WorkspacePermissionApplications' + 400: + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetailResponse' + /v1/permissions/user/application/{appId}: + post: + tags: + - permission + description: Get permissions of the application for the user + operationId: getApplicationPermissions + parameters: + - name: appId + in: path + description: Application ID + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/PermissionRequest' + responses: + 200: + description: Permission updated + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ApplicationPermissions' + 400: + description: Bad request + content: + application/json: + schema: + $ref: '#/components/schemas/ProblemDetailResponse' +components: + schemas: + PermissionRequest: + type: object + required: + - token + properties: + token: + type: string + WorkspacePermissionApplications: + type: object + properties: + workspace: + $ref: '#/components/schemas/WorkspacePermissions' + applications: + type: array + items: + $ref: '#/components/schemas/ApplicationPermissions' + WorkspacePermissions: + type: object + properties: + workspaceId: + type: string + permissions: + type: object + nullable: false + description: resources + additionalProperties: + type: array + items: + type: string + uniqueItems: true + ApplicationPermissions: + type: object + properties: + appId: + type: string + permissions: + type: object + nullable: false + description: resources + additionalProperties: + type: array + items: + type: string + uniqueItems: true + ProblemDetailResponse: + type: object + properties: + errorCode: + type: string + detail: + type: string + params: + type: array + items: + $ref: '#/components/schemas/ProblemDetailParam' + invalidParams: + type: array + items: + $ref: '#/components/schemas/ProblemDetailInvalidParam' + ProblemDetailParam: + type: object + properties: + key: + type: string + value: + type: string + ProblemDetailInvalidParam: + type: object + properties: + name: + type: string + message: + type: string diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 4cfda36..17e8343 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -11,34 +11,48 @@ quarkus.liquibase.validate-on-migrate=true # enable or disable multi-tenancy support tkit.rs.context.tenant-id.enabled=true +onecx.permission.token.verified=true +onecx.permission.token.issuer.public-key-location.suffix=/protocol/openid-connect/certs +onecx.permission.token.issuer.public-key-location.enabled=false +onecx.permission.token.claim.path=realm_access/roles + +tkit.dataimport.enabled=false +tkit.dataimport.configurations.permission.file=dev-data.import.json +tkit.dataimport.configurations.permission.metadata.operation=CLEAN_INSERT +tkit.dataimport.configurations.permission.enabled=false +tkit.dataimport.configurations.permission.stop-at-error=true + # PROD %prod.quarkus.datasource.jdbc.url=${DB_URL:jdbc:postgresql://postgresdb:5432/onecx-permission?sslmode=disable} %prod.quarkus.datasource.username=${DB_USER:onecx-permission} %prod.quarkus.datasource.password=${DB_PWD:onecx-permission} + # DEV +%dev.onecx.permission.token.verified=false %dev.tkit.rs.context.tenant-id.enabled=true %dev.tkit.rs.context.tenant-id.mock.enabled=true %dev.tkit.rs.context.tenant-id.mock.default-tenant=test %dev.tkit.rs.context.tenant-id.mock.data.org1=tenant100 # TEST +quarkus.test.integration-test-profile=test +%test.onecx.permission.token.verified=false %test.tkit.rs.context.tenant-id.enabled=true %test.tkit.rs.context.tenant-id.mock.enabled=true %test.tkit.rs.context.tenant-id.mock.default-tenant=default %test.tkit.rs.context.tenant-id.mock.claim-org-id=orgId %test.tkit.rs.context.tenant-id.mock.token-header-param=apm-principal-token -%test.tkit.rs.context.tenant-id.mock.data.org1=tenant-100 -%test.tkit.rs.context.tenant-id.mock.data.org2=tenant-200 - -# TEST-IT -quarkus.test.integration-test-profile=test-it -%test-it.tkit.log.json.enabled=false -%test-it.tkit.rs.context.tenant-id.mock.enabled=true -%test-it.tkit.rs.context.tenant-id.mock.default-tenant=default -%test-it.tkit.rs.context.tenant-id.mock.claim-org-id=orgId -%test-it.tkit.rs.context.tenant-id.mock.token-header-param=apm-principal-token -%test-it.tkit.rs.context.tenant-id.mock.data.org1=tenant-100 -%test-it.tkit.rs.context.tenant-id.mock.data.org2=tenant-200 +%test.tkit.rs.context.tenant-id.mock.data.org1=100 +%test.tkit.rs.context.tenant-id.mock.data.org2=200 +%test.tkit.rs.context.tenant-id.mock.data.i100=i100 +%test.quarkus.keycloak.devservices.roles.bob=n3 +%test.smallrye.jwt.verify.key.location=${keycloak.url}/realms/quarkus/protocol/openid-connect/certs + +%test.tkit.dataimport.enabled=true +%test.tkit.dataimport.configurations.permission.enabled=true +%test.tkit.dataimport.configurations.permission.file=./src/test/resources/import/permission-import.json +%test.tkit.dataimport.configurations.permission.metadata.operation=CLEAN_INSERT +%test.tkit.dataimport.configurations.permission.stop-at-error=true # PIPE CONFIG diff --git a/src/main/resources/db/changeLog.xml b/src/main/resources/db/changeLog.xml index 44c297b..889fa2b 100644 --- a/src/main/resources/db/changeLog.xml +++ b/src/main/resources/db/changeLog.xml @@ -4,5 +4,5 @@ xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> - + \ No newline at end of file diff --git a/src/main/resources/db/v1/2024-01-04-create-tables.xml b/src/main/resources/db/v1/2024-01-04-create-tables.xml index 0ce4e72..88c912e 100644 --- a/src/main/resources/db/v1/2024-01-04-create-tables.xml +++ b/src/main/resources/db/v1/2024-01-04-create-tables.xml @@ -19,12 +19,110 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/db/v1/2024-01-10-data-import-log.xml b/src/main/resources/db/v1/2024-01-10-data-import-log.xml new file mode 100644 index 0000000..7132b55 --- /dev/null +++ b/src/main/resources/db/v1/2024-01-10-data-import-log.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + diff --git a/src/test/java/io/github/onecx/permission/AfterStartPermissionDataImportTest.java b/src/test/java/io/github/onecx/permission/AfterStartPermissionDataImportTest.java new file mode 100644 index 0000000..3f5c8a9 --- /dev/null +++ b/src/test/java/io/github/onecx/permission/AfterStartPermissionDataImportTest.java @@ -0,0 +1,65 @@ +package io.github.onecx.permission; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.tkit.quarkus.context.ApplicationContext; +import org.tkit.quarkus.context.Context; + +import io.github.onecx.permission.domain.daos.*; +import io.github.onecx.permission.test.AbstractTest; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +@DisplayName("Permission data import test from example file") +class AfterStartPermissionDataImportTest extends AbstractTest { + + @Inject + PermissionDAO permissionDAO; + + @Inject + RoleDAO roleDAO; + + @Inject + WorkspacePermissionDAO workspacePermissionDAO; + + @Inject + WorkspaceAssignmentDAO workspaceAssignmentDAO; + + @Inject + AssignmentDAO assignmentDAO; + + @Test + @DisplayName("Import permission from data file") + void importDataFromFileTest() { + try { + var ctx = Context.builder() + .principal("data-import") + .tenantId("i100") + .build(); + + ApplicationContext.start(ctx); + + var permissions = permissionDAO.findAll().toList(); + assertThat(permissions).hasSize(8); + + var roles = roleDAO.findAll().toList(); + assertThat(roles).hasSize(2); + + var workspacePermissions = workspacePermissionDAO.findAll(); + assertThat(workspacePermissions).hasSize(3); + + var assignments = assignmentDAO.findAll(); + assertThat(assignments).hasSize(6); + + var workspaceAssignments = workspaceAssignmentDAO.findAll(); + assertThat(workspaceAssignments).hasSize(4); + + } finally { + ApplicationContext.close(); + } + } +} diff --git a/src/test/java/io/github/onecx/permission/common/services/ClaimServiceTest.java b/src/test/java/io/github/onecx/permission/common/services/ClaimServiceTest.java new file mode 100644 index 0000000..076e181 --- /dev/null +++ b/src/test/java/io/github/onecx/permission/common/services/ClaimServiceTest.java @@ -0,0 +1,22 @@ +package io.github.onecx.permission.common.services; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +import io.github.onecx.permission.test.AbstractTest; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +class ClaimServiceTest extends AbstractTest { + + @Test + void claimPathTest() { + + var out = ClaimService.splitClaimPath("realms/roles"); + assertThat(out).isNotNull().hasSize(2).containsExactly("realms", "roles"); + + out = ClaimService.splitClaimPath("groups"); + assertThat(out).isNotNull().hasSize(1); + } +} diff --git a/src/test/java/io/github/onecx/permission/common/utils/TokenUtilTest.java b/src/test/java/io/github/onecx/permission/common/utils/TokenUtilTest.java new file mode 100644 index 0000000..0c31c29 --- /dev/null +++ b/src/test/java/io/github/onecx/permission/common/utils/TokenUtilTest.java @@ -0,0 +1,79 @@ +package io.github.onecx.permission.common.utils; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Optional; + +import jakarta.json.Json; +import jakarta.json.JsonValue; + +import org.junit.jupiter.api.Test; + +import io.github.onecx.permission.common.models.TokenConfig; +import io.github.onecx.permission.test.AbstractTest; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +class TokenUtilTest extends AbstractTest { + + @Test + void tokenUtilityTest() { + + var config = new TokenConfig() { + @Override + public boolean tokenVerified() { + return false; + } + + @Override + public String tokenPublicKeyLocationSuffix() { + return null; + } + + @Override + public boolean tokenPublicKeyEnabled() { + return false; + } + + @Override + public Optional tokenClaimSeparator() { + return Optional.empty(); + } + + @Override + public String tokenClaimPath() { + return null; + } + }; + + var tmp = TokenUtil.findClaimWithRoles(config, null, new String[] { "test" }); + assertThat(tmp).isNotNull().isEmpty(); + + var value = Json.createValue(32); + tmp = TokenUtil.findClaimWithRoles(config, value, new String[] { "test1", "test2" }); + assertThat(tmp).isNotNull().containsExactly("32"); + + JsonValue emptyValue = new JsonValue() { + @Override + public ValueType getValueType() { + return null; + } + + @Override + public String toString() { + return " "; + } + }; + + tmp = TokenUtil.findClaimWithRoles(config, emptyValue, new String[] { "test1", "test2" }); + assertThat(tmp).isNotNull().isEmpty(); + + var list = Json.createArrayBuilder(); + list.add("s1"); + list.add(" "); + list.add(""); + + tmp = TokenUtil.findClaimWithRoles(config, list.build(), new String[] { "test1", "test2" }); + assertThat(tmp).isNotNull().containsExactly("s1"); + } +} diff --git a/src/test/java/io/github/onecx/permission/domain/daos/AbstractDAOTest.java b/src/test/java/io/github/onecx/permission/domain/daos/AbstractDAOTest.java new file mode 100644 index 0000000..098ccf9 --- /dev/null +++ b/src/test/java/io/github/onecx/permission/domain/daos/AbstractDAOTest.java @@ -0,0 +1,28 @@ +package io.github.onecx.permission.domain.daos; + +import jakarta.persistence.EntityManager; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.function.Executable; +import org.mockito.Mockito; +import org.tkit.quarkus.jpa.exceptions.DAOException; + +import io.github.onecx.permission.test.AbstractTest; +import io.quarkus.test.InjectMock; + +abstract class AbstractDAOTest extends AbstractTest { + + @InjectMock + EntityManager em; + + @BeforeEach + void beforeAll() { + Mockito.when(em.getCriteriaBuilder()).thenThrow(new RuntimeException("Test technical error exception")); + } + + void methodExceptionTests(Executable fn, Enum key) { + var exc = Assertions.assertThrows(DAOException.class, fn); + Assertions.assertEquals(key, exc.key); + } +} diff --git a/src/test/java/io/github/onecx/permission/domain/daos/AssignmentDAOTest.java b/src/test/java/io/github/onecx/permission/domain/daos/AssignmentDAOTest.java new file mode 100644 index 0000000..2c4dc38 --- /dev/null +++ b/src/test/java/io/github/onecx/permission/domain/daos/AssignmentDAOTest.java @@ -0,0 +1,24 @@ +package io.github.onecx.permission.domain.daos; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +class AssignmentDAOTest extends AbstractDAOTest { + + @Inject + AssignmentDAO dao; + + @Test + @SuppressWarnings("java:S2699") + void methodExceptionTests() { + methodExceptionTests(() -> dao.findById(null), + AssignmentDAO.ErrorKeys.FIND_ENTITY_BY_ID_FAILED); + methodExceptionTests(() -> dao.findByCriteria(null), + AssignmentDAO.ErrorKeys.ERROR_FIND_ASSIGNMENT_BY_CRITERIA); + } + +} diff --git a/src/test/java/io/github/onecx/permission/domain/daos/PermissionDAOTest.java b/src/test/java/io/github/onecx/permission/domain/daos/PermissionDAOTest.java index 5a3a4a0..f8f061d 100644 --- a/src/test/java/io/github/onecx/permission/domain/daos/PermissionDAOTest.java +++ b/src/test/java/io/github/onecx/permission/domain/daos/PermissionDAOTest.java @@ -1,43 +1,28 @@ package io.github.onecx.permission.domain.daos; import jakarta.inject.Inject; -import jakarta.persistence.EntityManager; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.function.Executable; -import org.mockito.Mockito; -import org.tkit.quarkus.jpa.exceptions.DAOException; -import io.github.onecx.permission.test.AbstractTest; -import io.quarkus.test.InjectMock; import io.quarkus.test.junit.QuarkusTest; @QuarkusTest -class PermissionDAOTest extends AbstractTest { +class PermissionDAOTest extends AbstractDAOTest { @Inject PermissionDAO dao; - @InjectMock - EntityManager em; - - @BeforeEach - void beforeAll() { - Mockito.when(em.getCriteriaBuilder()).thenThrow(new RuntimeException("Test technical error exception")); - } - @Test + @SuppressWarnings("java:S2699") void methodExceptionTests() { + methodExceptionTests(() -> dao.findAllPermissionForUser(null), + PermissionDAO.ErrorKeys.ERROR_FIND_ALL_PERMISSION_FOR_USER); + methodExceptionTests(() -> dao.findPermissionForUser(null, null), + PermissionDAO.ErrorKeys.ERROR_FIND_PERMISSION_FOR_USER); methodExceptionTests(() -> dao.loadByAppId(null), PermissionDAO.ErrorKeys.ERROR_LOAD_BY_APP_ID); methodExceptionTests(() -> dao.findByCriteria(null), PermissionDAO.ErrorKeys.ERROR_FIND_PERMISSION_BY_CRITERIA); } - void methodExceptionTests(Executable fn, Enum key) { - var exc = Assertions.assertThrows(DAOException.class, fn); - Assertions.assertEquals(key, exc.key); - } } diff --git a/src/test/java/io/github/onecx/permission/domain/daos/RoleDAOTest.java b/src/test/java/io/github/onecx/permission/domain/daos/RoleDAOTest.java new file mode 100644 index 0000000..a65972d --- /dev/null +++ b/src/test/java/io/github/onecx/permission/domain/daos/RoleDAOTest.java @@ -0,0 +1,24 @@ +package io.github.onecx.permission.domain.daos; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +class RoleDAOTest extends AbstractDAOTest { + + @Inject + RoleDAO dao; + + @Test + @SuppressWarnings("java:S2699") + void methodExceptionTests() { + methodExceptionTests(() -> dao.findById(null), + RoleDAO.ErrorKeys.FIND_ENTITY_BY_ID_FAILED); + methodExceptionTests(() -> dao.findByCriteria(null), + RoleDAO.ErrorKeys.ERROR_FIND_ROLE_BY_CRITERIA); + } + +} diff --git a/src/test/java/io/github/onecx/permission/domain/daos/WorkspaceAssignmentDAOTest.java b/src/test/java/io/github/onecx/permission/domain/daos/WorkspaceAssignmentDAOTest.java new file mode 100644 index 0000000..f09df75 --- /dev/null +++ b/src/test/java/io/github/onecx/permission/domain/daos/WorkspaceAssignmentDAOTest.java @@ -0,0 +1,24 @@ +package io.github.onecx.permission.domain.daos; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +class WorkspaceAssignmentDAOTest extends AbstractDAOTest { + + @Inject + WorkspaceAssignmentDAO dao; + + @Test + @SuppressWarnings("java:S2699") + void methodExceptionTests() { + methodExceptionTests(() -> dao.findByCriteria(null), + WorkspaceAssignmentDAO.ErrorKeys.ERROR_FIND_ASSIGNMENT_BY_CRITERIA); + methodExceptionTests(() -> dao.findById(null), + WorkspaceAssignmentDAO.ErrorKeys.FIND_ENTITY_BY_ID_FAILED); + } + +} diff --git a/src/test/java/io/github/onecx/permission/domain/daos/WorkspacePermissionDAOTest.java b/src/test/java/io/github/onecx/permission/domain/daos/WorkspacePermissionDAOTest.java new file mode 100644 index 0000000..f9fe9ef --- /dev/null +++ b/src/test/java/io/github/onecx/permission/domain/daos/WorkspacePermissionDAOTest.java @@ -0,0 +1,25 @@ +package io.github.onecx.permission.domain.daos; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +class WorkspacePermissionDAOTest extends AbstractDAOTest { + + @Inject + WorkspacePermissionDAO dao; + + @Test + @SuppressWarnings("java:S2699") + void methodExceptionTests() { + methodExceptionTests(() -> dao.findWorkspacePermissionForUser(null, null), + WorkspacePermissionDAO.ErrorKeys.ERROR_FIND_WORKSPACE_PERMISSION_FOR_USER); + methodExceptionTests(() -> dao.findByCriteria(null), + WorkspacePermissionDAO.ErrorKeys.ERROR_FIND_PERMISSION_BY_CRITERIA); + methodExceptionTests(() -> dao.findById(null), + WorkspacePermissionDAO.ErrorKeys.FIND_ENTITY_BY_ID_FAILED); + } +} diff --git a/src/test/java/io/github/onecx/permission/domain/di/PermissionDataImportServiceTest.java b/src/test/java/io/github/onecx/permission/domain/di/PermissionDataImportServiceTest.java new file mode 100644 index 0000000..43d76f5 --- /dev/null +++ b/src/test/java/io/github/onecx/permission/domain/di/PermissionDataImportServiceTest.java @@ -0,0 +1,181 @@ +package io.github.onecx.permission.domain.di; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.tkit.quarkus.dataimport.DataImportConfig; +import org.tkit.quarkus.test.WithDBData; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import gen.io.github.onecx.permission.domain.di.v1.model.DataImportDTOV1; +import gen.io.github.onecx.permission.domain.di.v1.model.DataImportTenantWrapperDTOV1; +import io.github.onecx.permission.domain.daos.PermissionDAO; +import io.github.onecx.permission.domain.models.Permission; +import io.github.onecx.permission.test.AbstractTest; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +@WithDBData(value = "data/test-internal.xml", deleteBeforeInsert = true, deleteAfterTest = true, rinseAndRepeat = true) +class PermissionDataImportServiceTest extends AbstractTest { + + @Inject + PermissionDataImportV1 service; + + @Inject + ObjectMapper mapper; + + @Inject + PermissionDAO permissionDAO; + + @Test + void checkTest() { + var data = new DataImportDTOV1(); + data.setTenants(null); + data.setPermissions(null); + assertThat(PermissionDataImportV1.checkIsEmpty(data)).isTrue(); + + data.setTenants(null); + data.setPermissions(Map.of()); + assertThat(PermissionDataImportV1.checkIsEmpty(data)).isTrue(); + + data.setTenants(Map.of()); + data.setPermissions(Map.of()); + assertThat(PermissionDataImportV1.checkIsEmpty(data)).isTrue(); + + data.setTenants(Map.of()); + data.setPermissions(null); + assertThat(PermissionDataImportV1.checkIsEmpty(data)).isTrue(); + + data.setTenants(Map.of("2", new DataImportTenantWrapperDTOV1())); + data.setPermissions(Map.of("2", new HashMap<>())); + assertThat(PermissionDataImportV1.checkIsEmpty(data)).isFalse(); + + data.setTenants(Map.of("2", new DataImportTenantWrapperDTOV1())); + data.setPermissions(null); + assertThat(PermissionDataImportV1.checkIsEmpty(data)).isFalse(); + + data.setTenants(null); + data.setPermissions(Map.of("2", new HashMap<>())); + assertThat(PermissionDataImportV1.checkIsEmpty(data)).isFalse(); + } + + @Test + void importDataNotSupportedActionTest() { + + Map metadata = new HashMap<>(); + metadata.put("operation", "CUSTOM_NOT_SUPPORTED"); + DataImportConfig config = new DataImportConfig() { + @Override + public Map getMetadata() { + return metadata; + } + }; + + service.importData(config); + + List data = permissionDAO.findAll().toList(); + assertThat(data).isNotNull().hasSize(7); + + } + + @Test + void importEmptyDataTest() { + Assertions.assertDoesNotThrow(() -> { + service.importData(new DataImportConfig() { + @Override + public Map getMetadata() { + return Map.of("operation", "CLEAN_INSERT"); + } + }); + + service.importData(new DataImportConfig() { + @Override + public Map getMetadata() { + return Map.of("operation", "CLEAN_INSERT"); + } + + @Override + public byte[] getData() { + return new byte[] {}; + } + }); + + service.importData(new DataImportConfig() { + @Override + public Map getMetadata() { + return Map.of("operation", "CLEAN_INSERT"); + } + + @Override + public byte[] getData() { + try { + return mapper.writeValueAsBytes(new DataImportDTOV1()); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + }); + + service.importData(new DataImportConfig() { + @Override + public Map getMetadata() { + return Map.of("operation", "CLEAN_INSERT"); + } + + @Override + public byte[] getData() { + try { + var data = new DataImportDTOV1(); + data.setPermissions(null); + data.setTenants(null); + return mapper.writeValueAsBytes(data); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + }); + + service.importData(new DataImportConfig() { + @Override + public Map getMetadata() { + return Map.of("operation", "CLEAN_INSERT"); + } + + @Override + public byte[] getData() { + try { + var data = new DataImportDTOV1(); + data.setPermissions(null); + data.setTenants(Map.of()); + return mapper.writeValueAsBytes(data); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + }); + + }); + + var config = new DataImportConfig() { + @Override + public Map getMetadata() { + return Map.of("operation", "CLEAN_INSERT"); + } + + @Override + public byte[] getData() { + return new byte[] { 0 }; + } + }; + Assertions.assertThrows(PermissionDataImportV1.ImportException.class, () -> service.importData(config)); + + } +} diff --git a/src/test/java/io/github/onecx/permission/rs/external/v1/PermissionRestControllerConfigIssuerTest.java b/src/test/java/io/github/onecx/permission/rs/external/v1/PermissionRestControllerConfigIssuerTest.java new file mode 100644 index 0000000..0448e0b --- /dev/null +++ b/src/test/java/io/github/onecx/permission/rs/external/v1/PermissionRestControllerConfigIssuerTest.java @@ -0,0 +1,72 @@ +package io.github.onecx.permission.rs.external.v1; + +import static io.restassured.RestAssured.given; +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; +import static org.assertj.core.api.Assertions.assertThat; +import static org.jboss.resteasy.reactive.RestResponse.Status.OK; + +import jakarta.inject.Inject; + +import org.eclipse.microprofile.config.Config; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.tkit.quarkus.test.WithDBData; + +import gen.io.github.onecx.permission.rs.external.v1.model.ApplicationPermissionsDTOV1; +import gen.io.github.onecx.permission.rs.external.v1.model.PermissionRequestDTOV1; +import io.github.onecx.permission.common.models.TokenConfig; +import io.github.onecx.permission.common.services.ClaimService; +import io.github.onecx.permission.rs.external.v1.controllers.PermissionRestController; +import io.github.onecx.permission.test.AbstractTest; +import io.quarkus.test.InjectMock; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.keycloak.client.KeycloakTestClient; +import io.smallrye.config.SmallRyeConfig; + +@QuarkusTest +@TestHTTPEndpoint(PermissionRestController.class) +@WithDBData(value = "data/test-v1.xml", deleteBeforeInsert = true, deleteAfterTest = true, rinseAndRepeat = true) +class PermissionRestControllerConfigIssuerTest extends AbstractTest { + + @InjectMock + TokenConfig tokenConfig; + + @InjectMock + ClaimService claimService; + + @Inject + Config config; + + @BeforeEach + void beforeEach() { + Mockito.when(claimService.getClaimPath()).thenReturn(new String[] { "groups" }); + var tmp = config.unwrap(SmallRyeConfig.class).getConfigMapping(TokenConfig.class); + Mockito.when(tokenConfig.tokenClaimSeparator()).thenReturn(tmp.tokenClaimSeparator()); + Mockito.when(tokenConfig.tokenClaimPath()).thenReturn(tmp.tokenClaimPath()); + Mockito.when(tokenConfig.tokenVerified()).thenReturn(true); + Mockito.when(tokenConfig.tokenPublicKeyLocationSuffix()).thenReturn(tmp.tokenPublicKeyLocationSuffix()); + Mockito.when(tokenConfig.tokenPublicKeyEnabled()).thenReturn(true); + } + + @Test + void skipTokenVerified() { + + KeycloakTestClient keycloakClient = new KeycloakTestClient(); + var accessToken = keycloakClient.getAccessToken("bob"); + + var dto = given() + .contentType(APPLICATION_JSON) + .body(new PermissionRequestDTOV1().token(accessToken)) + .post("/application/app1") + .then() + .statusCode(OK.getStatusCode()) + .extract() + .body().as(ApplicationPermissionsDTOV1.class); + + assertThat(dto).isNotNull(); + assertThat(dto.getPermissions()).isNotNull().hasSize(1); + assertThat(dto.getPermissions().get("o1")).isNotNull().hasSize(1).containsExactly("a3"); + } +} diff --git a/src/test/java/io/github/onecx/permission/rs/external/v1/PermissionRestControllerConfigPublicKeyTest.java b/src/test/java/io/github/onecx/permission/rs/external/v1/PermissionRestControllerConfigPublicKeyTest.java new file mode 100644 index 0000000..2942522 --- /dev/null +++ b/src/test/java/io/github/onecx/permission/rs/external/v1/PermissionRestControllerConfigPublicKeyTest.java @@ -0,0 +1,72 @@ +package io.github.onecx.permission.rs.external.v1; + +import static io.restassured.RestAssured.given; +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; +import static org.assertj.core.api.Assertions.assertThat; +import static org.jboss.resteasy.reactive.RestResponse.Status.OK; + +import jakarta.inject.Inject; + +import org.eclipse.microprofile.config.Config; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.tkit.quarkus.test.WithDBData; + +import gen.io.github.onecx.permission.rs.external.v1.model.ApplicationPermissionsDTOV1; +import gen.io.github.onecx.permission.rs.external.v1.model.PermissionRequestDTOV1; +import io.github.onecx.permission.common.models.TokenConfig; +import io.github.onecx.permission.common.services.ClaimService; +import io.github.onecx.permission.rs.external.v1.controllers.PermissionRestController; +import io.github.onecx.permission.test.AbstractTest; +import io.quarkus.test.InjectMock; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.keycloak.client.KeycloakTestClient; +import io.smallrye.config.SmallRyeConfig; + +@QuarkusTest +@TestHTTPEndpoint(PermissionRestController.class) +@WithDBData(value = "data/test-v1.xml", deleteBeforeInsert = true, deleteAfterTest = true, rinseAndRepeat = true) +class PermissionRestControllerConfigPublicKeyTest extends AbstractTest { + + @InjectMock + TokenConfig tokenConfig; + + @InjectMock + ClaimService claimService; + + @Inject + Config config; + + @BeforeEach + void beforeEach() { + Mockito.when(claimService.getClaimPath()).thenReturn(new String[] { "groups" }); + var tmp = config.unwrap(SmallRyeConfig.class).getConfigMapping(TokenConfig.class); + Mockito.when(tokenConfig.tokenClaimSeparator()).thenReturn(tmp.tokenClaimSeparator()); + Mockito.when(tokenConfig.tokenClaimPath()).thenReturn(tmp.tokenClaimPath()); + Mockito.when(tokenConfig.tokenVerified()).thenReturn(true); + Mockito.when(tokenConfig.tokenPublicKeyLocationSuffix()).thenReturn(tmp.tokenPublicKeyLocationSuffix()); + Mockito.when(tokenConfig.tokenPublicKeyEnabled()).thenReturn(false); + } + + @Test + void skipTokenVerified() { + + KeycloakTestClient keycloakClient = new KeycloakTestClient(); + var accessToken = keycloakClient.getAccessToken("bob"); + + var dto = given() + .contentType(APPLICATION_JSON) + .body(new PermissionRequestDTOV1().token(accessToken)) + .post("/application/app1") + .then() + .statusCode(OK.getStatusCode()) + .extract() + .body().as(ApplicationPermissionsDTOV1.class); + + assertThat(dto).isNotNull(); + assertThat(dto.getPermissions()).isNotNull().hasSize(1); + assertThat(dto.getPermissions().get("o1")).isNotNull().hasSize(1).containsExactly("a3"); + } +} diff --git a/src/test/java/io/github/onecx/permission/rs/external/v1/PermissionRestControllerTenantTest.java b/src/test/java/io/github/onecx/permission/rs/external/v1/PermissionRestControllerTenantTest.java new file mode 100644 index 0000000..544836d --- /dev/null +++ b/src/test/java/io/github/onecx/permission/rs/external/v1/PermissionRestControllerTenantTest.java @@ -0,0 +1,117 @@ +package io.github.onecx.permission.rs.external.v1; + +import static io.restassured.RestAssured.given; +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; +import static org.assertj.core.api.Assertions.assertThat; +import static org.jboss.resteasy.reactive.RestResponse.Status.*; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.tkit.quarkus.test.WithDBData; + +import gen.io.github.onecx.permission.rs.external.v1.model.*; +import io.github.onecx.permission.rs.external.v1.controllers.PermissionRestController; +import io.github.onecx.permission.test.AbstractTest; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +@TestHTTPEndpoint(PermissionRestController.class) +@WithDBData(value = "data/test-v1.xml", deleteBeforeInsert = true, deleteAfterTest = true, rinseAndRepeat = true) +class PermissionRestControllerTenantTest extends AbstractTest { + + @Test + void getApplicationPermissionsTest() { + + var accessToken = createToken("org1", List.of("n3-100")); + + var dto = given() + .contentType(APPLICATION_JSON) + .header(APM_HEADER_PARAM, accessToken) + .body(new PermissionRequestDTOV1().token(accessToken)) + .post("/application/app1") + .then() + .statusCode(OK.getStatusCode()) + .extract() + .body().as(ApplicationPermissionsDTOV1.class); + + assertThat(dto).isNotNull(); + assertThat(dto.getPermissions()).isNotNull().hasSize(1); + assertThat(dto.getPermissions().get("o1")).isNotNull().hasSize(1).containsExactly("a2"); + + } + + @Test + void getWorkspacePermissionsTest() { + + var accessToken = createToken("org1", List.of("n3-100")); + + var dto = given() + .contentType(APPLICATION_JSON) + .header(APM_HEADER_PARAM, accessToken) + .body(new PermissionRequestDTOV1().token(accessToken)) + .post("/workspace/wapp1") + .then() + .statusCode(OK.getStatusCode()) + .extract() + .body().as(WorkspacePermissionsDTOV1.class); + + assertThat(dto).isNotNull(); + assertThat(dto.getPermissions()).isNotNull().isEmpty(); + + dto = given() + .contentType(APPLICATION_JSON) + .header(APM_HEADER_PARAM, accessToken) + .body(new PermissionRequestDTOV1().token(accessToken)) + .post("/workspace/wapp1-100") + .then() + .statusCode(OK.getStatusCode()) + .extract() + .body().as(WorkspacePermissionsDTOV1.class); + + assertThat(dto).isNotNull(); + assertThat(dto.getPermissions()).isNotNull().hasSize(1); + assertThat(dto.getPermissions().get("o1")).isNotNull().hasSize(1).containsExactly("a1"); + + } + + @Test + void getWorkspacePermissionApplicationsTest() { + + var accessToken = createToken("org1", List.of("n3-100")); + + var dto = given() + .contentType(APPLICATION_JSON) + .header(APM_HEADER_PARAM, accessToken) + .body(new PermissionRequestDTOV1().token(accessToken)) + .post("/workspace/wapp1/applications") + .then() + .statusCode(OK.getStatusCode()) + .extract() + .body().as(WorkspacePermissionApplicationsDTOV1.class); + + assertThat(dto).isNotNull(); + assertThat(dto.getWorkspace()).isNotNull(); + assertThat(dto.getWorkspace().getPermissions()).isNotNull().isEmpty(); + + dto = given() + .contentType(APPLICATION_JSON) + .header(APM_HEADER_PARAM, accessToken) + .body(new PermissionRequestDTOV1().token(accessToken)) + .post("/workspace/wapp1-100/applications") + .then() + .statusCode(OK.getStatusCode()) + .extract() + .body().as(WorkspacePermissionApplicationsDTOV1.class); + + assertThat(dto).isNotNull(); + assertThat(dto.getWorkspace()).isNotNull(); + assertThat(dto.getWorkspace().getPermissions()).isNotNull().hasSize(1); + assertThat(dto.getWorkspace().getPermissions().get("o1")).isNotNull().hasSize(1).containsExactly("a1"); + assertThat(dto.getApplications()).isNotNull().hasSize(1); + assertThat(dto.getApplications().get(0)).isNotNull(); + assertThat(dto.getApplications().get(0).getPermissions()).isNotNull().hasSize(1); + assertThat(dto.getApplications().get(0).getPermissions().get("o1")).isNotNull().hasSize(1).containsExactly("a2"); + } +} diff --git a/src/test/java/io/github/onecx/permission/rs/external/v1/PermissionRestControllerTenantTestIT.java b/src/test/java/io/github/onecx/permission/rs/external/v1/PermissionRestControllerTenantTestIT.java new file mode 100644 index 0000000..9f39bb2 --- /dev/null +++ b/src/test/java/io/github/onecx/permission/rs/external/v1/PermissionRestControllerTenantTestIT.java @@ -0,0 +1,7 @@ +package io.github.onecx.permission.rs.external.v1; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +class PermissionRestControllerTenantTestIT extends PermissionRestControllerTenantTest { +} diff --git a/src/test/java/io/github/onecx/permission/rs/external/v1/PermissionRestControllerTest.java b/src/test/java/io/github/onecx/permission/rs/external/v1/PermissionRestControllerTest.java new file mode 100644 index 0000000..25b0949 --- /dev/null +++ b/src/test/java/io/github/onecx/permission/rs/external/v1/PermissionRestControllerTest.java @@ -0,0 +1,129 @@ +package io.github.onecx.permission.rs.external.v1; + +import static io.restassured.RestAssured.given; +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; +import static org.assertj.core.api.Assertions.assertThat; +import static org.jboss.resteasy.reactive.RestResponse.Status.*; + +import java.util.List; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.tkit.quarkus.test.WithDBData; + +import gen.io.github.onecx.permission.rs.external.v1.model.*; +import io.github.onecx.permission.rs.external.v1.controllers.PermissionRestController; +import io.github.onecx.permission.rs.internal.mappers.ExceptionMapper; +import io.github.onecx.permission.test.AbstractTest; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +@TestHTTPEndpoint(PermissionRestController.class) +@WithDBData(value = "data/test-v1.xml", deleteBeforeInsert = true, deleteAfterTest = true, rinseAndRepeat = true) +class PermissionRestControllerTest extends AbstractTest { + + @Test + void getApplicationPermissionsTest() { + + var accessToken = createToken(List.of("n3")); + + var dto = given() + .contentType(APPLICATION_JSON) + .body(new PermissionRequestDTOV1().token(accessToken)) + .post("/application/app1") + .then() + .statusCode(OK.getStatusCode()) + .extract() + .body().as(ApplicationPermissionsDTOV1.class); + + assertThat(dto).isNotNull(); + assertThat(dto.getPermissions()).isNotNull().hasSize(1); + assertThat(dto.getPermissions().get("o1")).isNotNull().hasSize(1).containsExactly("a3"); + + } + + private static Stream badRequestData() { + return Stream.of( + Arguments.of("/application/app1", "getApplicationPermissions.permissionRequestDTOV1: must not be null"), + Arguments.of("/workspace/wapp1", "getWorkspacePermission.permissionRequestDTOV1: must not be null"), + Arguments.of("/workspace/wapp1/applications", + "getWorkspacePermissionApplications.permissionRequestDTOV1: must not be null")); + } + + @ParameterizedTest + @MethodSource("badRequestData") + void getApplicationPermissionsNoBodyTest(String post, String check) { + + var exception = given() + .contentType(APPLICATION_JSON) + .post(post) + .then() + .statusCode(BAD_REQUEST.getStatusCode()) + .extract() + .body().as(ProblemDetailResponseDTOV1.class); + + assertThat(exception).isNotNull(); + assertThat(exception.getErrorCode()).isEqualTo(ExceptionMapper.ErrorKeys.CONSTRAINT_VIOLATIONS.name()); + assertThat(exception.getDetail()).isEqualTo(check); + + } + + @Test + void getApplicationPermissionsWrongTongTest() { + + given() + .contentType(APPLICATION_JSON) + .body(new PermissionRequestDTOV1().token("this-is-not-token")) + .post("/application/app1") + .then() + .statusCode(INTERNAL_SERVER_ERROR.getStatusCode()); + } + + @Test + void getWorkspacePermissionsTest() { + + var accessToken = createToken(List.of("n3")); + + var dto = given() + .contentType(APPLICATION_JSON) + .body(new PermissionRequestDTOV1().token(accessToken)) + .post("/workspace/wapp1") + .then() + .statusCode(OK.getStatusCode()) + .extract() + .body().as(WorkspacePermissionsDTOV1.class); + + assertThat(dto).isNotNull(); + assertThat(dto.getPermissions()).isNotNull().hasSize(1); + assertThat(dto.getPermissions().get("o1")).isNotNull().hasSize(1).containsExactly("a3"); + + } + + @Test + void getWorkspacePermissionApplicationsTest() { + + var accessToken = createToken(List.of("n3")); + + var dto = given() + .contentType(APPLICATION_JSON) + .body(new PermissionRequestDTOV1().token(accessToken)) + .post("/workspace/wapp1/applications") + .then() + .statusCode(OK.getStatusCode()) + .extract() + .body().as(WorkspacePermissionApplicationsDTOV1.class); + + assertThat(dto).isNotNull(); + assertThat(dto.getWorkspace()).isNotNull(); + assertThat(dto.getWorkspace().getPermissions()).isNotNull().hasSize(1); + assertThat(dto.getWorkspace().getPermissions().get("o1")).isNotNull().hasSize(1).containsExactly("a3"); + assertThat(dto.getApplications()).isNotNull().hasSize(1); + assertThat(dto.getApplications().get(0)).isNotNull(); + assertThat(dto.getApplications().get(0).getPermissions()).isNotNull().hasSize(1); + assertThat(dto.getApplications().get(0).getPermissions().get("o1")).isNotNull().hasSize(1).containsExactly("a3"); + } +} diff --git a/src/test/java/io/github/onecx/permission/rs/external/v1/PermissionRestControllerTestIT.java b/src/test/java/io/github/onecx/permission/rs/external/v1/PermissionRestControllerTestIT.java new file mode 100644 index 0000000..0ba63fd --- /dev/null +++ b/src/test/java/io/github/onecx/permission/rs/external/v1/PermissionRestControllerTestIT.java @@ -0,0 +1,8 @@ +package io.github.onecx.permission.rs.external.v1; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +class PermissionRestControllerTestIT extends PermissionRestControllerTest { + +} diff --git a/src/test/java/io/github/onecx/permission/rs/internal/controllers/AssignmentRestControllerTest.java b/src/test/java/io/github/onecx/permission/rs/internal/controllers/AssignmentRestControllerTest.java new file mode 100644 index 0000000..36d046e --- /dev/null +++ b/src/test/java/io/github/onecx/permission/rs/internal/controllers/AssignmentRestControllerTest.java @@ -0,0 +1,204 @@ +package io.github.onecx.permission.rs.internal.controllers; + +import static io.restassured.RestAssured.given; +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.from; +import static org.jboss.resteasy.reactive.RestResponse.Status.*; +import static org.jboss.resteasy.reactive.RestResponse.Status.BAD_REQUEST; + +import jakarta.ws.rs.core.HttpHeaders; + +import org.junit.jupiter.api.Test; +import org.tkit.quarkus.test.WithDBData; + +import gen.io.github.onecx.permission.rs.internal.model.*; +import io.github.onecx.permission.rs.internal.mappers.ExceptionMapper; +import io.github.onecx.permission.test.AbstractTest; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +@TestHTTPEndpoint(AssignmentRestController.class) +@WithDBData(value = "data/test-internal.xml", deleteBeforeInsert = true, deleteAfterTest = true, rinseAndRepeat = true) +class AssignmentRestControllerTest extends AbstractTest { + + @Test + void createAssignment() { + // create Assignment + var requestDTO = new CreateAssignmentRequestDTO(); + requestDTO.setPermissionId("p11"); + requestDTO.setRoleId("r11"); + + var uri = given() + .when() + .contentType(APPLICATION_JSON) + .body(requestDTO) + .post() + .then() + .statusCode(CREATED.getStatusCode()) + .extract().header(HttpHeaders.LOCATION); + + var dto = given() + .contentType(APPLICATION_JSON) + .get(uri) + .then() + .statusCode(OK.getStatusCode()) + .extract() + .body().as(AssignmentDTO.class); + + assertThat(dto).isNotNull() + .returns(requestDTO.getRoleId(), from(AssignmentDTO::getRoleId)) + .returns(requestDTO.getPermissionId(), from(AssignmentDTO::getPermissionId)); + + // create Role without body + var exception = given() + .when() + .contentType(APPLICATION_JSON) + .post() + .then() + .statusCode(BAD_REQUEST.getStatusCode()) + .extract().as(ProblemDetailResponseDTO.class); + + assertThat(exception.getErrorCode()).isEqualTo(ExceptionMapper.ErrorKeys.CONSTRAINT_VIOLATIONS.name()); + assertThat(exception.getDetail()).isEqualTo("createAssignment.createAssignmentRequestDTO: must not be null"); + + // create Role with existing name + requestDTO = new CreateAssignmentRequestDTO(); + requestDTO.setPermissionId("p13"); + requestDTO.setRoleId("r13"); + + exception = given().when() + .contentType(APPLICATION_JSON) + .body(requestDTO) + .post() + .then() + .statusCode(BAD_REQUEST.getStatusCode()) + .extract().as(ProblemDetailResponseDTO.class); + + assertThat(exception.getErrorCode()).isEqualTo("PERSIST_ENTITY_FAILED"); + assertThat(exception.getDetail()).isEqualTo( + "could not execute statement [ERROR: duplicate key value violates unique constraint 'assignment_key' Detail: Key (tenant_id, role_id, permission_id)=(default, r13, p13) already exists.]"); + + } + + @Test + void createAssignmentWrong() { + // create Assignment + var requestDTO = new CreateAssignmentRequestDTO(); + requestDTO.setPermissionId("does-not-exists"); + requestDTO.setRoleId("r11"); + + given() + .when() + .contentType(APPLICATION_JSON) + .body(requestDTO) + .post() + .then() + .statusCode(NOT_FOUND.getStatusCode()); + + requestDTO.setPermissionId("p11"); + requestDTO.setRoleId("does-not-exists"); + + given() + .when() + .contentType(APPLICATION_JSON) + .body(requestDTO) + .post() + .then() + .statusCode(NOT_FOUND.getStatusCode()); + } + + @Test + void getNotFoundAssignment() { + given() + .contentType(APPLICATION_JSON) + .get("does-not-exists") + .then() + .statusCode(NOT_FOUND.getStatusCode()); + } + + @Test + void searchAssignmentTest() { + var criteria = new AssignmentSearchCriteriaDTO(); + + var data = given() + .contentType(APPLICATION_JSON) + .body(criteria) + .post("/search") + .then() + .statusCode(OK.getStatusCode()) + .contentType(APPLICATION_JSON) + .extract() + .as(AssignmentPageResultDTO.class); + + assertThat(data).isNotNull(); + assertThat(data.getTotalElements()).isEqualTo(3); + assertThat(data.getStream()).isNotNull().hasSize(3); + + criteria.setAppId(" "); + + data = given() + .contentType(APPLICATION_JSON) + .body(criteria) + .post("/search") + .then() + .statusCode(OK.getStatusCode()) + .contentType(APPLICATION_JSON) + .extract() + .as(AssignmentPageResultDTO.class); + + assertThat(data).isNotNull(); + assertThat(data.getTotalElements()).isEqualTo(3); + assertThat(data.getStream()).isNotNull().hasSize(3); + + criteria.setAppId("app1"); + + data = given() + .contentType(APPLICATION_JSON) + .body(criteria) + .post("/search") + .then().log().all() + .statusCode(OK.getStatusCode()) + .contentType(APPLICATION_JSON) + .extract() + .as(AssignmentPageResultDTO.class); + + assertThat(data).isNotNull(); + assertThat(data.getTotalElements()).isEqualTo(2); + assertThat(data.getStream()).isNotNull().hasSize(2); + } + + @Test + void deleteAssignmentTest() { + + // delete Assignment + given() + .contentType(APPLICATION_JSON) + .delete("DELETE_1") + .then() + .statusCode(NO_CONTENT.getStatusCode()); + + // check Assignment + given() + .contentType(APPLICATION_JSON) + .get("a11") + .then() + .statusCode(OK.getStatusCode()); + + // check if Assignment does not exist + given() + .contentType(APPLICATION_JSON) + .delete("a11") + .then() + .statusCode(NO_CONTENT.getStatusCode()); + + // check Assignment + given() + .contentType(APPLICATION_JSON) + .get("a11") + .then() + .statusCode(NOT_FOUND.getStatusCode()); + + } +} diff --git a/src/test/java/io/github/onecx/permission/rs/internal/controllers/AssignmentRestControllerTestIT.java b/src/test/java/io/github/onecx/permission/rs/internal/controllers/AssignmentRestControllerTestIT.java new file mode 100644 index 0000000..86b8ea7 --- /dev/null +++ b/src/test/java/io/github/onecx/permission/rs/internal/controllers/AssignmentRestControllerTestIT.java @@ -0,0 +1,8 @@ +package io.github.onecx.permission.rs.internal.controllers; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +class AssignmentRestControllerTestIT extends AssignmentRestControllerTest { + +} diff --git a/src/test/java/io/github/onecx/permission/rs/internal/controllers/PermissionRestControllerTest.java b/src/test/java/io/github/onecx/permission/rs/internal/controllers/PermissionRestControllerTest.java index 72c5709..e85cb32 100644 --- a/src/test/java/io/github/onecx/permission/rs/internal/controllers/PermissionRestControllerTest.java +++ b/src/test/java/io/github/onecx/permission/rs/internal/controllers/PermissionRestControllerTest.java @@ -41,9 +41,6 @@ void searchTest() { assertThat(data.getStream()).isNotNull().hasSize(7); criteria.setAppId(" "); - criteria.setName(" "); - criteria.setResource(" "); - criteria.setAction(" "); data = given() .contentType(APPLICATION_JSON) @@ -64,9 +61,6 @@ void searchTest() { void searchCriteriaTest() { var criteria = new PermissionSearchCriteriaDTO(); criteria.setAppId("app1"); - criteria.setName("n1"); - criteria.setResource("o1"); - criteria.setAction("a1"); var data = given() .contentType(APPLICATION_JSON) @@ -79,8 +73,11 @@ void searchCriteriaTest() { .as(PermissionPageResultDTO.class); assertThat(data).isNotNull(); - assertThat(data.getTotalElements()).isEqualTo(1); - assertThat(data.getStream()).isNotNull().hasSize(1); + assertThat(data.getTotalElements()).isEqualTo(5); + assertThat(data.getStream()).isNotNull().hasSize(5); + + criteria = new PermissionSearchCriteriaDTO(); + criteria.setAppId("app1"); } @Test diff --git a/src/test/java/io/github/onecx/permission/rs/internal/controllers/RoleRestControllerTest.java b/src/test/java/io/github/onecx/permission/rs/internal/controllers/RoleRestControllerTest.java new file mode 100644 index 0000000..332bac0 --- /dev/null +++ b/src/test/java/io/github/onecx/permission/rs/internal/controllers/RoleRestControllerTest.java @@ -0,0 +1,277 @@ +package io.github.onecx.permission.rs.internal.controllers; + +import static io.restassured.RestAssured.given; +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.from; +import static org.jboss.resteasy.reactive.RestResponse.Status.*; + +import jakarta.ws.rs.core.HttpHeaders; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.tkit.quarkus.test.WithDBData; + +import gen.io.github.onecx.permission.rs.internal.model.*; +import io.github.onecx.permission.rs.internal.mappers.ExceptionMapper; +import io.github.onecx.permission.test.AbstractTest; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +@TestHTTPEndpoint(RoleRestController.class) +@WithDBData(value = "data/test-internal.xml", deleteBeforeInsert = true, deleteAfterTest = true, rinseAndRepeat = true) +class RoleRestControllerTest extends AbstractTest { + + @Test + void createNewRoleTest() { + + // create Role + var requestDTO = new CreateRoleRequestDTO(); + requestDTO.setName("test01"); + requestDTO.setDescription("description"); + + var uri = given() + .when() + .contentType(APPLICATION_JSON) + .body(requestDTO) + .post() + .then().statusCode(CREATED.getStatusCode()) + .extract().header(HttpHeaders.LOCATION); + + var dto = given() + .contentType(APPLICATION_JSON) + .get(uri) + .then() + .statusCode(OK.getStatusCode()) + .extract() + .body().as(RoleDTO.class); + + assertThat(dto).isNotNull() + .returns(requestDTO.getName(), from(RoleDTO::getName)) + .returns(requestDTO.getDescription(), from(RoleDTO::getDescription)); + + // create Role without body + var exception = given() + .when() + .contentType(APPLICATION_JSON) + .post() + .then() + .statusCode(BAD_REQUEST.getStatusCode()) + .extract().as(ProblemDetailResponseDTO.class); + + assertThat(exception.getErrorCode()).isEqualTo(ExceptionMapper.ErrorKeys.CONSTRAINT_VIOLATIONS.name()); + assertThat(exception.getDetail()).isEqualTo("createRole.createRoleRequestDTO: must not be null"); + + // create Role with existing name + requestDTO = new CreateRoleRequestDTO(); + requestDTO.setName("n1"); + + exception = given().when() + .contentType(APPLICATION_JSON) + .body(requestDTO) + .post() + .then() + .statusCode(BAD_REQUEST.getStatusCode()) + .extract().as(ProblemDetailResponseDTO.class); + + assertThat(exception.getErrorCode()).isEqualTo("PERSIST_ENTITY_FAILED"); + assertThat(exception.getDetail()).isEqualTo( + "could not execute statement [ERROR: duplicate key value violates unique constraint 'role_name' Detail: Key (tenant_id, name)=(default, n1) already exists.]"); + } + + @Test + void deleteRoleTest() { + + // delete Role + given() + .contentType(APPLICATION_JSON) + .delete("DELETE_1") + .then().statusCode(NO_CONTENT.getStatusCode()); + + // check if Role exists + given() + .contentType(APPLICATION_JSON) + .get("DELETE_1") + .then().statusCode(NOT_FOUND.getStatusCode()); + + // delete Role in portal + given() + .contentType(APPLICATION_JSON) + .delete("r11") + .then() + .statusCode(NO_CONTENT.getStatusCode()); + + } + + @Test + void getRoleByIdTest() { + + var dto = given() + .contentType(APPLICATION_JSON) + .get("r12") + .then().statusCode(OK.getStatusCode()) + .contentType(APPLICATION_JSON) + .extract() + .body().as(RoleDTO.class); + + assertThat(dto).isNotNull(); + assertThat(dto.getName()).isEqualTo("n2"); + assertThat(dto.getId()).isEqualTo("r12"); + + given() + .contentType(APPLICATION_JSON) + .get("___") + .then().statusCode(NOT_FOUND.getStatusCode()); + + dto = given() + .contentType(APPLICATION_JSON) + .get("r11") + .then().statusCode(OK.getStatusCode()) + .contentType(APPLICATION_JSON) + .extract() + .body().as(RoleDTO.class); + + assertThat(dto).isNotNull(); + assertThat(dto.getName()).isEqualTo("n1"); + assertThat(dto.getId()).isEqualTo("r11"); + + } + + @Test + void searchRolesTest() { + var criteria = new RoleSearchCriteriaDTO(); + + var data = given() + .contentType(APPLICATION_JSON) + .body(criteria) + .post("/search") + .then() + .statusCode(OK.getStatusCode()) + .contentType(APPLICATION_JSON) + .extract() + .as(RolePageResultDTO.class); + + assertThat(data).isNotNull(); + assertThat(data.getTotalElements()).isEqualTo(4); + assertThat(data.getStream()).isNotNull().hasSize(4); + + criteria.setName(" "); + criteria.setDescription(" "); + data = given() + .contentType(APPLICATION_JSON) + .body(criteria) + .post("/search") + .then() + .statusCode(OK.getStatusCode()) + .contentType(APPLICATION_JSON) + .extract() + .as(RolePageResultDTO.class); + + assertThat(data).isNotNull(); + assertThat(data.getTotalElements()).isEqualTo(4); + assertThat(data.getStream()).isNotNull().hasSize(4); + + criteria.setName("n3"); + criteria.setDescription("d1"); + + data = given() + .contentType(APPLICATION_JSON) + .body(criteria) + .post("/search") + .then() + .statusCode(OK.getStatusCode()) + .contentType(APPLICATION_JSON) + .extract() + .as(RolePageResultDTO.class); + + assertThat(data).isNotNull(); + assertThat(data.getTotalElements()).isEqualTo(1); + assertThat(data.getStream()).isNotNull().hasSize(1); + + } + + @Test + void updateRoleTest() { + + // update none existing Role + var requestDto = new UpdateRoleRequestDTO(); + requestDto.setName("test01"); + requestDto.setDescription("description-update"); + + given() + .contentType(APPLICATION_JSON) + .body(requestDto) + .when() + .put("does-not-exists") + .then().statusCode(NOT_FOUND.getStatusCode()); + + // update Role + given() + .contentType(APPLICATION_JSON) + .body(requestDto) + .when() + .put("r11") + .then() + .statusCode(OK.getStatusCode()); + + // download Role + var dto = given().contentType(APPLICATION_JSON) + .body(requestDto) + .when() + .get("r11") + .then().statusCode(OK.getStatusCode()) + .contentType(APPLICATION_JSON) + .extract() + .body().as(RoleDTO.class); + + assertThat(dto).isNotNull(); + assertThat(dto.getDescription()).isEqualTo(requestDto.getDescription()); + + } + + @Test + void updateRoleWithExistingNameTest() { + + var dto = new UpdateRoleRequestDTO(); + dto.setName("n3"); + dto.setDescription("description"); + + var exception = given() + .contentType(APPLICATION_JSON) + .when() + .body(dto) + .put("r11") + .then() + .statusCode(BAD_REQUEST.getStatusCode()) + .extract().as(ProblemDetailResponseDTO.class); + + Assertions.assertNotNull(exception); + Assertions.assertEquals("MERGE_ENTITY_FAILED", exception.getErrorCode()); + Assertions.assertEquals( + "could not execute statement [ERROR: duplicate key value violates unique constraint 'role_name' Detail: Key (tenant_id, name)=(default, n3) already exists.]", + exception.getDetail()); + Assertions.assertNull(exception.getInvalidParams()); + + } + + @Test + void updateRoleWithoutBodyTest() { + + var exception = given() + .contentType(APPLICATION_JSON) + .when() + .pathParam("id", "update_create_new") + .put("{id}") + .then() + .statusCode(BAD_REQUEST.getStatusCode()) + .extract().as(ProblemDetailResponseDTO.class); + + Assertions.assertNotNull(exception); + Assertions.assertEquals(ExceptionMapper.ErrorKeys.CONSTRAINT_VIOLATIONS.name(), exception.getErrorCode()); + Assertions.assertEquals("updateRole.updateRoleRequestDTO: must not be null", + exception.getDetail()); + Assertions.assertNotNull(exception.getInvalidParams()); + Assertions.assertEquals(1, exception.getInvalidParams().size()); + } +} diff --git a/src/test/java/io/github/onecx/permission/rs/internal/controllers/RoleRestControllerTestIT.java b/src/test/java/io/github/onecx/permission/rs/internal/controllers/RoleRestControllerTestIT.java new file mode 100644 index 0000000..71ba0be --- /dev/null +++ b/src/test/java/io/github/onecx/permission/rs/internal/controllers/RoleRestControllerTestIT.java @@ -0,0 +1,8 @@ +package io.github.onecx.permission.rs.internal.controllers; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +class RoleRestControllerTestIT extends RoleRestControllerTest { + +} diff --git a/src/test/java/io/github/onecx/permission/rs/internal/controllers/WorkspaceAssignmentRestControllerTest.java b/src/test/java/io/github/onecx/permission/rs/internal/controllers/WorkspaceAssignmentRestControllerTest.java new file mode 100644 index 0000000..ccc910d --- /dev/null +++ b/src/test/java/io/github/onecx/permission/rs/internal/controllers/WorkspaceAssignmentRestControllerTest.java @@ -0,0 +1,203 @@ +package io.github.onecx.permission.rs.internal.controllers; + +import static io.restassured.RestAssured.given; +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.from; +import static org.jboss.resteasy.reactive.RestResponse.Status.*; + +import jakarta.ws.rs.core.HttpHeaders; + +import org.junit.jupiter.api.Test; +import org.tkit.quarkus.test.WithDBData; + +import gen.io.github.onecx.permission.rs.internal.model.*; +import io.github.onecx.permission.rs.internal.mappers.ExceptionMapper; +import io.github.onecx.permission.test.AbstractTest; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +@TestHTTPEndpoint(WorkspaceAssignmentRestController.class) +@WithDBData(value = "data/test-internal.xml", deleteBeforeInsert = true, deleteAfterTest = true, rinseAndRepeat = true) +class WorkspaceAssignmentRestControllerTest extends AbstractTest { + + @Test + void createWorkspaceAssignment() { + // create Assignment + var requestDTO = new CreateWorkspaceAssignmentRequestDTO(); + requestDTO.setPermissionId("wp11"); + requestDTO.setRoleId("r11"); + + var uri = given() + .when() + .contentType(APPLICATION_JSON) + .body(requestDTO) + .post() + .then() + .statusCode(CREATED.getStatusCode()) + .extract().header(HttpHeaders.LOCATION); + + var dto = given() + .contentType(APPLICATION_JSON) + .get(uri) + .then() + .statusCode(OK.getStatusCode()) + .extract() + .body().as(WorkspaceAssignmentDTO.class); + + assertThat(dto).isNotNull() + .returns(requestDTO.getRoleId(), from(WorkspaceAssignmentDTO::getRoleId)) + .returns(requestDTO.getPermissionId(), from(WorkspaceAssignmentDTO::getPermissionId)); + + // create Role without body + var exception = given() + .when() + .contentType(APPLICATION_JSON) + .post() + .then() + .statusCode(BAD_REQUEST.getStatusCode()) + .extract().as(ProblemDetailResponseDTO.class); + + assertThat(exception.getErrorCode()).isEqualTo(ExceptionMapper.ErrorKeys.CONSTRAINT_VIOLATIONS.name()); + assertThat(exception.getDetail()) + .isEqualTo("createWorkspaceAssignment.createWorkspaceAssignmentRequestDTO: must not be null"); + + // create Role with existing name + requestDTO = new CreateWorkspaceAssignmentRequestDTO(); + requestDTO.setPermissionId("wp13"); + requestDTO.setRoleId("r13"); + + exception = given().when() + .contentType(APPLICATION_JSON) + .body(requestDTO) + .post() + .then() + .statusCode(BAD_REQUEST.getStatusCode()) + .extract().as(ProblemDetailResponseDTO.class); + + assertThat(exception.getErrorCode()).isEqualTo("PERSIST_ENTITY_FAILED"); + assertThat(exception.getDetail()).isEqualTo( + "could not execute statement [ERROR: duplicate key value violates unique constraint 'workspace_assignment_key' Detail: Key (tenant_id, role_id, permission_id)=(default, r13, wp13) already exists.]"); + + } + + @Test + void createWorkspaceAssignmentWrong() { + // create Assignment + var requestDTO = new CreateWorkspaceAssignmentRequestDTO(); + requestDTO.setPermissionId("does-not-exists"); + requestDTO.setRoleId("r11"); + + given() + .when() + .contentType(APPLICATION_JSON) + .body(requestDTO) + .post() + .then() + .statusCode(NOT_FOUND.getStatusCode()); + + requestDTO.setPermissionId("wp11"); + requestDTO.setRoleId("does-not-exists"); + + given() + .when() + .contentType(APPLICATION_JSON) + .body(requestDTO) + .post() + .then() + .statusCode(NOT_FOUND.getStatusCode()); + } + + @Test + void getNotFoundWorkspaceAssignment() { + given() + .contentType(APPLICATION_JSON) + .get("does-not-exists") + .then() + .statusCode(NOT_FOUND.getStatusCode()); + } + + @Test + void searchWorkspaceAssignmentTest() { + var criteria = new WorkspaceAssignmentSearchCriteriaDTO(); + + var data = given() + .contentType(APPLICATION_JSON) + .body(criteria) + .post("/search") + .then() + .statusCode(OK.getStatusCode()) + .contentType(APPLICATION_JSON) + .extract() + .as(WorkspaceAssignmentPageResultDTO.class); + + assertThat(data).isNotNull(); + assertThat(data.getTotalElements()).isEqualTo(3); + assertThat(data.getStream()).isNotNull().hasSize(3); + + criteria.setWorkspaceId(" "); + data = given() + .contentType(APPLICATION_JSON) + .body(criteria) + .post("/search") + .then() + .statusCode(OK.getStatusCode()) + .contentType(APPLICATION_JSON) + .extract() + .as(WorkspaceAssignmentPageResultDTO.class); + + assertThat(data).isNotNull(); + assertThat(data.getTotalElements()).isEqualTo(3); + assertThat(data.getStream()).isNotNull().hasSize(3); + + criteria.setWorkspaceId("wapp1"); + + data = given() + .contentType(APPLICATION_JSON) + .body(criteria) + .post("/search") + .then() + .statusCode(OK.getStatusCode()) + .contentType(APPLICATION_JSON) + .extract() + .as(WorkspaceAssignmentPageResultDTO.class); + + assertThat(data).isNotNull(); + assertThat(data.getTotalElements()).isEqualTo(2); + assertThat(data.getStream()).isNotNull().hasSize(2); + } + + @Test + void deleteWorkspaceAssignmentTest() { + + // delete Assignment + given() + .contentType(APPLICATION_JSON) + .delete("DELETE_1") + .then() + .statusCode(NO_CONTENT.getStatusCode()); + + // check Assignment + given() + .contentType(APPLICATION_JSON) + .get("wa11") + .then() + .statusCode(OK.getStatusCode()); + + // check if Assignment does not exist + given() + .contentType(APPLICATION_JSON) + .delete("wa11") + .then() + .statusCode(NO_CONTENT.getStatusCode()); + + // check Assignment + given() + .contentType(APPLICATION_JSON) + .get("wa11") + .then() + .statusCode(NOT_FOUND.getStatusCode()); + + } +} diff --git a/src/test/java/io/github/onecx/permission/rs/internal/controllers/WorkspaceAssignmentRestControllerTestIT.java b/src/test/java/io/github/onecx/permission/rs/internal/controllers/WorkspaceAssignmentRestControllerTestIT.java new file mode 100644 index 0000000..217f1ce --- /dev/null +++ b/src/test/java/io/github/onecx/permission/rs/internal/controllers/WorkspaceAssignmentRestControllerTestIT.java @@ -0,0 +1,7 @@ +package io.github.onecx.permission.rs.internal.controllers; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +class WorkspaceAssignmentRestControllerTestIT extends WorkspaceAssignmentRestControllerTest { +} diff --git a/src/test/java/io/github/onecx/permission/rs/internal/controllers/WorkspacePermissionRestControllerTest.java b/src/test/java/io/github/onecx/permission/rs/internal/controllers/WorkspacePermissionRestControllerTest.java new file mode 100644 index 0000000..cb29d7c --- /dev/null +++ b/src/test/java/io/github/onecx/permission/rs/internal/controllers/WorkspacePermissionRestControllerTest.java @@ -0,0 +1,293 @@ +package io.github.onecx.permission.rs.internal.controllers; + +import static io.restassured.RestAssured.given; +import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.from; +import static org.jboss.resteasy.reactive.RestResponse.Status.*; + +import jakarta.ws.rs.core.HttpHeaders; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.tkit.quarkus.test.WithDBData; + +import gen.io.github.onecx.permission.rs.internal.model.*; +import io.github.onecx.permission.rs.internal.mappers.ExceptionMapper; +import io.github.onecx.permission.test.AbstractTest; +import io.quarkus.test.common.http.TestHTTPEndpoint; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +@TestHTTPEndpoint(WorkspacePermissionRestController.class) +@WithDBData(value = "data/test-internal.xml", deleteBeforeInsert = true, deleteAfterTest = true, rinseAndRepeat = true) +class WorkspacePermissionRestControllerTest extends AbstractTest { + + @Test + void searchTest() { + var criteria = new WorkspacePermissionSearchCriteriaDTO(); + + var data = given() + .contentType(APPLICATION_JSON) + .body(criteria) + .post("search") + .then() + .statusCode(OK.getStatusCode()) + .contentType(APPLICATION_JSON) + .extract() + .as(WorkspacePermissionPageResultDTO.class); + + assertThat(data).isNotNull(); + assertThat(data.getTotalElements()).isEqualTo(7); + assertThat(data.getStream()).isNotNull().hasSize(7); + + criteria.setWorkspaceId(" "); + + data = given() + .contentType(APPLICATION_JSON) + .body(criteria) + .post("search") + .then() + .statusCode(OK.getStatusCode()) + .contentType(APPLICATION_JSON) + .extract() + .as(WorkspacePermissionPageResultDTO.class); + + assertThat(data).isNotNull(); + assertThat(data.getTotalElements()).isEqualTo(7); + assertThat(data.getStream()).isNotNull().hasSize(7); + } + + @Test + void searchCriteriaTest() { + var criteria = new WorkspacePermissionSearchCriteriaDTO(); + criteria.setWorkspaceId("wapp1"); + + var data = given() + .contentType(APPLICATION_JSON) + .body(criteria) + .post("search") + .then() + .statusCode(OK.getStatusCode()) + .contentType(APPLICATION_JSON) + .extract() + .as(WorkspacePermissionPageResultDTO.class); + + assertThat(data).isNotNull(); + assertThat(data.getTotalElements()).isEqualTo(5); + assertThat(data.getStream()).isNotNull().hasSize(5); + } + + @Test + void searchNoBodyTest() { + var exception = given() + .contentType(APPLICATION_JSON) + .post("search") + .then() + .statusCode(BAD_REQUEST.getStatusCode()) + .contentType(APPLICATION_JSON) + .extract() + .as(ProblemDetailResponseDTO.class); + + assertThat(exception).isNotNull(); + assertThat(exception.getErrorCode()).isEqualTo(ExceptionMapper.ErrorKeys.CONSTRAINT_VIOLATIONS.name()); + assertThat(exception.getDetail()) + .isEqualTo("searchWorkspacePermissions.workspacePermissionSearchCriteriaDTO: must not be null"); + } + + @Test + void deleteWorkspacePermissionTest() { + + // delete Role + given() + .contentType(APPLICATION_JSON) + .delete("DELETE_1") + .then().statusCode(NO_CONTENT.getStatusCode()); + + // check if Role exists + given() + .contentType(APPLICATION_JSON) + .get("DELETE_1") + .then().statusCode(NOT_FOUND.getStatusCode()); + + // delete Role in portal + given() + .contentType(APPLICATION_JSON) + .delete("wp21") + .then() + .statusCode(NO_CONTENT.getStatusCode()); + + } + + @Test + void getWorkspacePermissionTest() { + + var dto = given() + .contentType(APPLICATION_JSON) + .get("wp21") + .then().statusCode(OK.getStatusCode()) + .contentType(APPLICATION_JSON) + .extract() + .body().as(WorkspacePermissionDTO.class); + + assertThat(dto).isNotNull(); + assertThat(dto.getWorkspaceId()).isEqualTo("wapp2"); + assertThat(dto.getId()).isEqualTo("wp21"); + + given() + .contentType(APPLICATION_JSON) + .get("___") + .then().statusCode(NOT_FOUND.getStatusCode()); + + dto = given() + .contentType(APPLICATION_JSON) + .get("wp11") + .then().statusCode(OK.getStatusCode()) + .contentType(APPLICATION_JSON) + .extract() + .body().as(WorkspacePermissionDTO.class); + + assertThat(dto).isNotNull(); + assertThat(dto.getWorkspaceId()).isEqualTo("wapp1"); + assertThat(dto.getId()).isEqualTo("wp11"); + + } + + @Test + void createNewWorkspacePermissionTest() { + + // create Role + var requestDTO = new CreateWorkspacePermissionRequestDTO(); + requestDTO.setWorkspaceId("w1"); + requestDTO.setAction("a1"); + requestDTO.setResource("r1"); + requestDTO.setDescription("d1"); + + var uri = given() + .when() + .contentType(APPLICATION_JSON) + .body(requestDTO) + .post() + .then().statusCode(CREATED.getStatusCode()) + .extract().header(HttpHeaders.LOCATION); + + var dto = given() + .contentType(APPLICATION_JSON) + .get(uri) + .then() + .statusCode(OK.getStatusCode()) + .extract() + .body().as(WorkspacePermissionDTO.class); + + assertThat(dto).isNotNull() + .returns(requestDTO.getAction(), from(WorkspacePermissionDTO::getAction)) + .returns(requestDTO.getResource(), from(WorkspacePermissionDTO::getResource)) + .returns(requestDTO.getWorkspaceId(), from(WorkspacePermissionDTO::getWorkspaceId)) + .returns(requestDTO.getDescription(), from(WorkspacePermissionDTO::getDescription)); + + // create Role without body + var exception = given() + .when() + .contentType(APPLICATION_JSON) + .post() + .then() + .statusCode(BAD_REQUEST.getStatusCode()) + .extract().as(ProblemDetailResponseDTO.class); + + assertThat(exception.getErrorCode()).isEqualTo(ExceptionMapper.ErrorKeys.CONSTRAINT_VIOLATIONS.name()); + assertThat(exception.getDetail()) + .isEqualTo("createWorkspacePermission.createWorkspacePermissionRequestDTO: must not be null"); + + // create WorkspacePermission with existing data + requestDTO = new CreateWorkspacePermissionRequestDTO(); + requestDTO.setWorkspaceId("wapp1"); + requestDTO.setAction("a1"); + requestDTO.setResource("o1"); + requestDTO.setDescription("d1"); + + exception = given().when() + .contentType(APPLICATION_JSON) + .body(requestDTO) + .post() + .then() + .statusCode(BAD_REQUEST.getStatusCode()) + .extract().as(ProblemDetailResponseDTO.class); + + assertThat(exception.getErrorCode()).isEqualTo("PERSIST_ENTITY_FAILED"); + assertThat(exception.getDetail()).isEqualTo( + "could not execute statement [ERROR: duplicate key value violates unique constraint 'workspace_permission_key' Detail: Key (tenant_id, workspace_id, resource, action)=(default, wapp1, o1, a1) already exists.]"); + } + + @Test + void updateWorkspacePermissionWithoutBodyTest() { + + var exception = given() + .contentType(APPLICATION_JSON) + .when() + .put("update_create_new") + .then() + .statusCode(BAD_REQUEST.getStatusCode()) + .extract().as(ProblemDetailResponseDTO.class); + + Assertions.assertNotNull(exception); + Assertions.assertEquals(ExceptionMapper.ErrorKeys.CONSTRAINT_VIOLATIONS.name(), exception.getErrorCode()); + Assertions.assertEquals("updateWorkspacePermission.updateWorkspacePermissionRequestDTO: must not be null", + exception.getDetail()); + Assertions.assertNotNull(exception.getInvalidParams()); + Assertions.assertEquals(1, exception.getInvalidParams().size()); + } + + @Test + void updateWorkspacePermissionTest() { + + var dto = given() + .contentType(APPLICATION_JSON) + .get("wp11") + .then().statusCode(OK.getStatusCode()) + .contentType(APPLICATION_JSON) + .extract() + .body().as(WorkspacePermissionDTO.class); + + var request = new UpdateWorkspacePermissionRequestDTO(); + request.setModificationCount(dto.getModificationCount()); + request.setDescription("description123"); + + given() + .contentType(APPLICATION_JSON) + .when() + .body(request) + .put("wp11") + .then() + .statusCode(OK.getStatusCode()); + + dto = given() + .contentType(APPLICATION_JSON) + .get("wp11") + .then().statusCode(OK.getStatusCode()) + .contentType(APPLICATION_JSON) + .extract() + .body().as(WorkspacePermissionDTO.class); + + assertThat(dto).isNotNull(); + assertThat(dto.getDescription()).isEqualTo(request.getDescription()); + assertThat(dto.getId()).isEqualTo("wp11"); + + } + + @Test + void updateWorkspacePermissionNotFoundTest() { + + var request = new UpdateWorkspacePermissionRequestDTO(); + request.setModificationCount(1); + request.setDescription("description123"); + + given() + .contentType(APPLICATION_JSON) + .when() + .body(request) + .put("does-not-exists") + .then() + .statusCode(NOT_FOUND.getStatusCode()); + + } +} diff --git a/src/test/java/io/github/onecx/permission/rs/internal/controllers/WorkspacePermissionRestControllerTestIT.java b/src/test/java/io/github/onecx/permission/rs/internal/controllers/WorkspacePermissionRestControllerTestIT.java new file mode 100644 index 0000000..63866fd --- /dev/null +++ b/src/test/java/io/github/onecx/permission/rs/internal/controllers/WorkspacePermissionRestControllerTestIT.java @@ -0,0 +1,7 @@ +package io.github.onecx.permission.rs.internal.controllers; + +import io.quarkus.test.junit.QuarkusIntegrationTest; + +@QuarkusIntegrationTest +class WorkspacePermissionRestControllerTestIT extends WorkspacePermissionRestControllerTest { +} diff --git a/src/test/java/io/github/onecx/permission/rs/operator/v1/controllers/OperatorRestControllerTest.java b/src/test/java/io/github/onecx/permission/rs/operator/v1/controllers/OperatorRestControllerTest.java index 0724d28..6f45a86 100644 --- a/src/test/java/io/github/onecx/permission/rs/operator/v1/controllers/OperatorRestControllerTest.java +++ b/src/test/java/io/github/onecx/permission/rs/operator/v1/controllers/OperatorRestControllerTest.java @@ -59,7 +59,6 @@ void requestEmptyListTest() { @Test void requestWrongPermissionTest() { var per = new PermissionDTOV1(); - per.setName("name"); per.setDescription("description"); var request = new PermissionRequestDTOV1(); @@ -84,8 +83,8 @@ void requestWrongPermissionTest() { @Test void requestPermissionTest() { - var per1 = new PermissionDTOV1().action("a1").resource("o1").name("name").description("description"); - var per2 = new PermissionDTOV1().action("new1").resource("o1").name("name1").description("description1"); + var per1 = new PermissionDTOV1().action("a1").resource("o1").description("description"); + var per2 = new PermissionDTOV1().action("new1").resource("o1").description("description1"); var request = new PermissionRequestDTOV1(); request.setPermissions(List.of(per1, per2)); @@ -102,8 +101,8 @@ void requestPermissionTest() { @Test void requestDuplicatePermissionTest() { - var per1 = new PermissionDTOV1().action("a1").resource("o1").name("name").description("description"); - var per2 = new PermissionDTOV1().action("a1").resource("o1").name("name1").description("description1"); + var per1 = new PermissionDTOV1().action("a1").resource("o1").description("description"); + var per2 = new PermissionDTOV1().action("a1").resource("o1").description("description1"); var request = new PermissionRequestDTOV1(); request.setPermissions(List.of(per1, per2)); diff --git a/src/test/java/io/github/onecx/permission/test/AbstractTest.java b/src/test/java/io/github/onecx/permission/test/AbstractTest.java index 43defa9..cbc2070 100644 --- a/src/test/java/io/github/onecx/permission/test/AbstractTest.java +++ b/src/test/java/io/github/onecx/permission/test/AbstractTest.java @@ -4,14 +4,37 @@ import static io.restassured.RestAssured.config; import static io.restassured.config.ObjectMapperConfig.objectMapperConfig; +import java.security.PrivateKey; +import java.util.List; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Inject; +import jakarta.json.Json; +import jakarta.json.JsonObjectBuilder; + +import org.eclipse.microprofile.config.Config; +import org.eclipse.microprofile.config.ConfigProvider; +import org.eclipse.microprofile.jwt.Claims; + import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import io.github.onecx.permission.common.models.TokenConfig; +import io.quarkus.test.Mock; import io.restassured.config.RestAssuredConfig; +import io.smallrye.config.SmallRyeConfig; +import io.smallrye.jwt.build.Jwt; +import io.smallrye.jwt.util.KeyUtils; @SuppressWarnings("java:S2187") public class AbstractTest { + protected static final String APM_HEADER_PARAM = ConfigProvider.getConfig() + .getValue("%test.tkit.rs.context.tenant-id.mock.token-header-param", String.class); + protected static final String CLAIMS_ORG_ID = ConfigProvider.getConfig() + .getValue("%test.tkit.rs.context.tenant-id.mock.claim-org-id", String.class); + static { config = RestAssuredConfig.config().objectMapperConfig( objectMapperConfig().jackson2ObjectMapperFactory( @@ -22,4 +45,47 @@ public class AbstractTest { return objectMapper; })); } + + protected static String createToken(String organizationId) { + return createToken(organizationId, null); + } + + protected static String createToken(List roles) { + return createToken(null, roles); + } + + protected static String createToken(String organizationId, List roles) { + try { + + String userName = "test-user"; + JsonObjectBuilder claims = Json.createObjectBuilder(); + claims.add(Claims.preferred_username.name(), userName); + claims.add(Claims.sub.name(), userName); + if (organizationId != null) { + claims.add(CLAIMS_ORG_ID, organizationId); + } + if (roles != null && !roles.isEmpty()) { + JsonObjectBuilder r = Json.createObjectBuilder(); + r.add("roles", Json.createArrayBuilder(roles)); + claims.add("realm_access", r); + } + PrivateKey privateKey = KeyUtils.generateKeyPair(2048).getPrivate(); + return Jwt.claims(claims.build()).sign(privateKey); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + public static class ConfigProducer { + + @Inject + Config config; + + @Produces + @ApplicationScoped + @Mock + TokenConfig config() { + return config.unwrap(SmallRyeConfig.class).getConfigMapping(TokenConfig.class); + } + } } diff --git a/src/test/resources/data/test-internal.xml b/src/test/resources/data/test-internal.xml index 45766b8..8f99e13 100644 --- a/src/test/resources/data/test-internal.xml +++ b/src/test/resources/data/test-internal.xml @@ -2,13 +2,43 @@ - - - + + + - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/resources/data/test-operator-v1.xml b/src/test/resources/data/test-operator-v1.xml index 45766b8..c391560 100644 --- a/src/test/resources/data/test-operator-v1.xml +++ b/src/test/resources/data/test-operator-v1.xml @@ -2,13 +2,13 @@ - - - + + + - - + + - - + + \ No newline at end of file diff --git a/src/test/resources/data/test-v1.xml b/src/test/resources/data/test-v1.xml new file mode 100644 index 0000000..fadfb01 --- /dev/null +++ b/src/test/resources/data/test-v1.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/test/resources/import/permission-import.json b/src/test/resources/import/permission-import.json new file mode 100644 index 0000000..4943fa4 --- /dev/null +++ b/src/test/resources/import/permission-import.json @@ -0,0 +1,82 @@ +{ + "permissions": { + "application1" : { + "resource1": { + "action1": "description11", + "action2": "description12" + }, + "resource2": { + "action1": "description21", + "action2": "description22" + } + }, + "application2" : { + "r3": { + "a1": "d31", + "a2": "d31" + }, + "r4": { + "a1": "d41", + "a2": "d41" + } + } + }, + "tenants": { + "i100": { + "workspaces-permissions": { + "workspace1": { + "menu1": { + "action1": "action-description11", + "action2": "action-description12" + } + }, + "workspace2": { + "menu2": { + "action1": "action-description21" + } + } + }, + "roles": { + "role1": { + "description": "description1", + "assignments": { + "application1": { + "resource1": ["action1","action2"] + }, + "application2": { + "r4": ["a1"] + } + }, + "workspaces-assignments": { + "workspace1": { + "menu1": ["action1"] + }, + "workspace2": { + "menu2": ["action1"] + } + } + }, + "role2": { + "description": "description1", + "assignments": { + "application1": { + "resource1": ["action1","action2"] + }, + "application2": { + "r4": ["a1"] + } + }, + "workspaces-assignments": { + "workspace1": { + "menu1": ["action1"] + }, + "workspace2": { + "menu2": ["action1"] + } + } + } + } + } + } + +} \ No newline at end of file