Skip to content

Commit

Permalink
feat: add user permissions
Browse files Browse the repository at this point in the history
  • Loading branch information
andrejpetras committed Jan 11, 2024
1 parent 3d404f1 commit 5fc2e36
Show file tree
Hide file tree
Showing 17 changed files with 407 additions and 59 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package io.github.onecx.permission.common.models;

import java.util.Optional;

import io.smallrye.config.ConfigMapping;
import io.smallrye.config.WithDefault;
import io.smallrye.config.WithName;

@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<String> tokenClaimSeparator();

@WithName("token.claim.path")
@WithDefault("realm_access.roles")
String tokenClaimPath();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package io.github.onecx.permission.common.services;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.json.JsonArray;
import jakarta.json.JsonObject;

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 {

private static final Pattern CLAIM_PATH_PATTERN = Pattern.compile("\\/(?=(?:(?:[^\"]*\"){2})*[^\"]*$)");

@Inject
JWTAuthContextInfo authContextInfo;

@Inject
TokenConfig config;

@Inject
JWTParser parser;

public List<String> getTokenRoles(String tokenData) {
try {
return getRoles(tokenData);
} catch (Exception ex) {
throw new TokenException("Error parsing principal token", ex);
}
}

private List<String> getRoles(String tokenData)
throws JoseException, InvalidJwtException, MalformedClaimException, ParseException {

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);

}

info.setVerifyCertificateThumbprint(false);
var token = parser.parse(tokenData, info);

// return findClaimWithRoles(config, token);
return List.of();

} else {

var jws = (JsonWebSignature) JsonWebStructure.fromCompactSerialization(tokenData);
var jwtClaims = JwtClaims.parse(jws.getUnverifiedPayload());
List<?> list = (List<?>) jwtClaims.flattenClaims().get(config.tokenClaimPath());
return (List<String>) list;
// jwtClaims.flattenClaims()
// return findClaimWithRoles(config, jwtClaims);
// return List.of();
}
}

private static List<String> findClaimWithRoles(TokenConfig tokenConfig, JsonObject json) {

var path = tokenConfig.tokenClaimPath();
Object claimValue = findClaimValue(path, json, splitClaimPath(path), 0);

if (claimValue instanceof JsonArray) {
return convertJsonArrayToList((JsonArray) claimValue);
} else if (claimValue != null) {
String sep = tokenConfig.tokenClaimSeparator().isPresent() ? tokenConfig.tokenClaimSeparator().get() : " ";
if (claimValue.toString().isBlank()) {
return Collections.emptyList();
}
return Arrays.asList(claimValue.toString().split(sep));
} else {
return Collections.emptyList();
}
}

private static List<String> convertJsonArrayToList(JsonArray claimValue) {
List<String> list = new ArrayList<>(claimValue.size());
for (int i = 0; i < claimValue.size(); i++) {
String claimValueStr = claimValue.getString(i);
if (claimValueStr == null || claimValueStr.isBlank()) {
continue;
}
list.add(claimValue.getString(i));
}
return list;
}

private static String[] splitClaimPath(String claimPath) {
return claimPath.indexOf('/') > 0 ? CLAIM_PATH_PATTERN.split(claimPath) : new String[] { claimPath };
}

private static Object findClaimValue(String claimPath, JsonObject json, String[] pathArray, int step) {
Object claimValue = json.getValue(pathArray[step].replace("\"", ""));
if (claimValue == null) {
log.debug("No claim exists at the path '{}' at the path segment '{}'", claimPath, pathArray[step]);
} else if (step + 1 < pathArray.length) {
if (claimValue instanceof JsonObject) {
int nextStep = step + 1;
return findClaimValue(claimPath, (JsonObject) claimValue, pathArray, nextStep);
} else {
log.debug("Claim value at the path '{}' is not a json object", claimPath);
}
}

return claimValue;
}

public static class TokenException extends RuntimeException {

public TokenException(String message) {
super(message);
}

public TokenException(String message, Throwable t) {
super(message, t);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

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;
Expand All @@ -14,8 +15,7 @@
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<Permission> {
Expand Down Expand Up @@ -61,8 +61,31 @@ public List<Permission> loadByAppId(String appId) {
}
}

public List<Permission> findPermissionForUser(String appId, List<String> roles) {
try {
System.out.println("# " + appId + " R " + roles);
var cb = this.getEntityManager().getCriteriaBuilder();
var cq = cb.createQuery(Permission.class);
var root = cq.from(Permission.class);

Subquery<String> 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(Permission_.id).in(sq));

return this.getEntityManager().createQuery(cq).getResultList();
} catch (Exception ex) {
throw new DAOException(ErrorKeys.ERROR_FIND_PERMISSION_FOR_USER, ex);
}
}

public enum ErrorKeys {

ERROR_FIND_PERMISSION_FOR_USER,
ERROR_LOAD_BY_APP_ID,
ERROR_FIND_PERMISSION_BY_CRITERIA;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
@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")
})
public class Assignment extends TraceableEntity {

Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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.hibernate.annotations.TenantId;
import org.tkit.quarkus.jpa.models.TraceableEntity;
Expand All @@ -16,6 +13,8 @@
@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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package io.github.onecx.permission.rs.external.v1.controllers;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.core.Response;

import org.tkit.quarkus.log.cdi.LogExclude;
import org.tkit.quarkus.log.cdi.LogService;

import gen.io.github.onecx.permission.rs.v1.PermissionApiV1;
import io.github.onecx.permission.common.services.TokenService;
import io.github.onecx.permission.domain.daos.PermissionDAO;
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;

@Override
public Response getApplicationPermissions(String appId, @LogExclude String body) {
var roles = tokenService.getTokenRoles(body);
var permissions = permissionDAO.findPermissionForUser(appId, roles);
return Response.ok(mapper.create(appId, permissions)).build();
}

@Override
public Response getWorkspacePermission(String workspace, @LogExclude String body) {
return null;
}

@Override
public Response getWorkspacePermissionApplications(String workspace, @LogExclude String body) {
return null;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package io.github.onecx.permission.rs.external.v1.mappers;

import java.util.*;

import org.mapstruct.Mapper;

import gen.io.github.onecx.permission.rs.v1.model.ApplicationPermissionsDTOV1;
import io.github.onecx.permission.domain.models.Permission;

@Mapper
public interface PermissionMapper {

default Map<String, Set<String>> permissions(List<Permission> permissions) {
if (permissions == null) {
return null;
}
Map<String, Set<String>> result = new HashMap<>();
permissions.forEach(permission -> result.computeIfAbsent(permission.getResource(), k -> new HashSet<>())
.add(permission.getAction()));
return result;
}

default ApplicationPermissionsDTOV1 create(String appId, Map<String, Set<String>> permissions) {
return new ApplicationPermissionsDTOV1().appId(appId).permissions(permissions);
}

default ApplicationPermissionsDTOV1 create(String appId, List<Permission> permissions) {
return create(appId, permissions(permissions));
}
}

This file was deleted.

Loading

0 comments on commit 5fc2e36

Please sign in to comment.