Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add roles, workspace permissions #3

Merged
merged 13 commits into from
Jan 14, 2024
39 changes: 39 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
<artifactId>onecx-tenant</artifactId>
</dependency>
<!-- 1000kit -->
<dependency>
<groupId>org.tkit.quarkus.lib</groupId>
<artifactId>tkit-quarkus-data-import</artifactId>
</dependency>
<dependency>
<groupId>org.tkit.quarkus.lib</groupId>
<artifactId>tkit-quarkus-rest-context</artifactId>
Expand Down Expand Up @@ -105,6 +109,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-opentelemetry</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc</artifactId>
</dependency>

<!-- OTHER -->
<dependency>
Expand Down Expand Up @@ -137,6 +145,11 @@
<artifactId>tkit-quarkus-test-db-import</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-keycloak-server</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down Expand Up @@ -170,6 +183,19 @@
</configOptions>
</configuration>
<executions>
<execution>
<id>di-v1</id>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<inputSpec>src/main/openapi/onecx-permission-di-v1.yaml</inputSpec>
<apiPackage>gen.io.github.onecx.permission.domain.di.v1</apiPackage>
<modelPackage>gen.io.github.onecx.permission.domain.di.v1.model</modelPackage>
<modelNameSuffix>DTOV1</modelNameSuffix>
<generateApis>false</generateApis>
</configuration>
</execution>
<execution>
<id>internal</id>
<goals>
Expand All @@ -194,6 +220,19 @@
<modelNameSuffix>DTOV1</modelNameSuffix>
</configuration>
</execution>
<execution>
<id>v1</id>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<inputSpec>src/main/openapi/onecx-permission-v1.yaml</inputSpec>
<apiPackage>gen.io.github.onecx.permission.rs.external.v1</apiPackage>
<modelPackage>gen.io.github.onecx.permission.rs.external.v1.model</modelPackage>
<modelNameSuffix>DTOV1</modelNameSuffix>
<apiNameSuffix>ApiV1</apiNameSuffix>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<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,33 @@
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 {

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

private static String[] claimPath;

@Inject
TokenConfig config;

@PostConstruct
public void init() {
claimPath = splitClaimPath(config.tokenClaimPath());
}

public String[] getClaimPath() {
return claimPath;
}

static String[] splitClaimPath(String claimPath) {
return claimPath.indexOf('/') > 0 ? CLAIM_PATH_PATTERN.split(claimPath) : new String[] { claimPath };
}
}
Original file line number Diff line number Diff line change
@@ -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<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 {

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);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
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<String> findClaimWithRoles(TokenConfig config, Object value, String[] path) {
JsonValue first = JsonUtils.wrapValue(value);
JsonValue claimValue = findClaimValue(first, path, 1);

if (claimValue instanceof JsonArray) {
return convertJsonArrayToList((JsonArray) claimValue);
} 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<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.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) {
if (json instanceof JsonObject) {
JsonValue claimValue = json.asJsonObject().get(pathArray[step].replace("\"", ""));
return findClaimValue(claimValue, pathArray, step + 1);
}
}
return json;
}

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