Compare commits

...

14 Commits

Author SHA1 Message Date
b9eb17e5c6 [RELEASE] Release v0.19.0 2024-12-12 09:16:17 +01:00
6d05b3444c [FEAT] update role management 2024-12-12 08:46:54 +01:00
dependabot[bot]
7b5e034ac2 [DEV-OPS] (dependabot) Bump actions/setup-java from 3 to 4
Bumps [actions/setup-java](https://github.com/actions/setup-java) from 3 to 4.
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](https://github.com/actions/setup-java/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/setup-java
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-08 15:25:10 +01:00
dependabot[bot]
b4554a8bdb [DEV-OPS] (dependabot) Bump actions/checkout from 3 to 4
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-08 15:25:07 +01:00
dependabot[bot]
ae84d1c6c8 [DEV-OPS] (dependabot) Bump advanced-security/maven-dependency-submission-action
Bumps [advanced-security/maven-dependency-submission-action](https://github.com/advanced-security/maven-dependency-submission-action) from 2.0.0 to 4.1.1.
- [Release notes](https://github.com/advanced-security/maven-dependency-submission-action/releases)
- [Commits](571e99aab1...4f64ddab9d)

---
updated-dependencies:
- dependency-name: advanced-security/maven-dependency-submission-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-12-08 15:25:03 +01:00
239763cf48 [DEV] update dev tag version 2024-12-08 15:20:42 +01:00
754c422be0 [RELEASE] new version 0.18.0 2024-12-08 15:20:23 +01:00
091ac4babd [FEAT,API] (Token) upgrade tocken management to permit to control more conplex token 2024-12-07 16:52:44 +01:00
dccb6b80d5 [FIX] check null pointer in condition for SQL 2024-12-07 16:52:42 +01:00
5633604d13 [FIX] Json field parsing 2024-12-07 16:52:39 +01:00
96cb8a6e16 [FEAT] add control on User JPA 2024-12-07 16:52:36 +01:00
ebe88e4a8d [FEAT] (test-tool) set public some test API to be more permisive 2024-12-07 16:52:32 +01:00
c82ab9f27f [FEAT] add Email generic contraint check 2024-12-07 16:52:27 +01:00
f914462460 [FEAT] add workflow 2024-11-06 17:35:03 +01:00
15 changed files with 322 additions and 39 deletions

View File

@@ -1,9 +1,10 @@
---
name: "Assign PR Author as Assignee"
"on":
on:
pull_request:
types: [opened, ready_for_review, reopened]
types:
- opened
jobs:
assign-pr-author-as-assignee:

33
.github/workflows/check-title.yml vendored Normal file
View File

@@ -0,0 +1,33 @@
---
name: "Check PR title"
on:
pull_request:
types:
- opened
- edited
- synchronize
- ready_for_review
- reopened
jobs:
check-title:
runs-on: ubuntu-latest
steps:
- name: "Check title"
uses: Slashgear/action-check-pr-title@v4.3.0
with:
regexp: "\\[(API,)?(API|DEV-OPS|DOC|FEAT|FIX|FIX\\-CI|STYLE)\\]( \\([A-Za-z0-9.\\-]+\\))? [A-Za-z0-9 ,.'\\-!]+$"
helpMessage: |
Title of the PR MUST respect format: "[{TYPE}] clear description without typos in english" with {TYPE}:
* [API] Change API that permit to access on the application (un-compatibility only). This one can specifically added with [API,{TYPE}]
* [DEV-OPS] Update automatic build system, method to deliver application/packages, ...
* [DOC] Update or add some documentation.
* [FEAT] Develop a new feature
* [FIX] When fixing issue
* [FIX-CI] When the CI fail to build and we apply a correction to set it work again.
* [STYLE] Update of the style tools/checker, or add/remove rules.
Examples:
[FEAT] My beautiful feature
[API,FIX] Change API to fix typo
[FIX] (module) Correct part of ...

View File

@@ -10,19 +10,17 @@ name: Java CI with Maven
on:
push:
branches: [ "develop" ]
branches:
- develop
pull_request:
branches: [ "develop" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
@@ -34,4 +32,4 @@ jobs:
# Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive
- name: Update dependency graph
uses: advanced-security/maven-dependency-submission-action@571e99aab1055c2e71a1e2309b9691de18d6b7d6
uses: advanced-security/maven-dependency-submission-action@4f64ddab9d742a4806eeb588d238e4c311a8397d

View File

@@ -3,7 +3,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>kangaroo-and-rabbit</groupId>
<artifactId>archidata</artifactId>
<version>0.17.1-SNAPSHOT</version>
<version>0.19.0</version>
<properties>
<java.version>21</java.version>
<maven.compiler.version>3.1</maven.compiler.version>

View File

@@ -21,6 +21,7 @@ import jakarta.persistence.ManyToMany;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
@@ -235,6 +236,14 @@ public class AnnotationTools {
return ((Pattern) annotation[0]).regexp();
}
public static boolean getConstraintsEmail(final Field element) throws DataAccessException {
final Annotation[] annotation = element.getDeclaredAnnotationsByType(Email.class);
if (annotation.length == 0) {
return false;
}
return true;
}
public static boolean isAnnotationGroup(final Field field, final Class<?> annotationType) {
try {
final Annotation[] anns = field.getAnnotations();

View File

@@ -1246,6 +1246,10 @@ public class DataAccess {
public static void addElement(final PreparedStatement ps, final Object value, final CountInOut iii)
throws Exception {
if (value == null) {
ps.setNull(iii.value, Types.INTEGER);
return;
}
if (value instanceof final UUID tmp) {
final byte[] dataByte = UuidUtils.asBytes(tmp);
ps.setBytes(iii.value, dataByte);

View File

@@ -30,7 +30,9 @@ import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.annotation.JsonValue;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.TypeFactory;
import jakarta.validation.constraints.NotNull;
@@ -152,7 +154,10 @@ public class AddOnDataJson implements DataAccessAddOn {
}
LOGGER.warn("Maybe fail to translate Model in datajson list: List<{}>", listClass.getCanonicalName());
}
final Object dataParsed = objectMapper.readValue(jsonData, field.getType());
final TypeFactory typeFactory = objectMapper.getTypeFactory();
final JavaType fieldType = typeFactory.constructType(field.getGenericType());
final Object dataParsed = objectMapper.readValue(jsonData, fieldType);
//final Object dataParsed = objectMapper.readValue(jsonData, field.getType());
field.set(data, dataParsed);
}
}

View File

@@ -401,6 +401,27 @@ public class CheckJPA<T> implements CheckFunctionInterface {
}
});
}
if (AnnotationTools.getConstraintsEmail(field)) {
final String emailPattern = "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$";
final Pattern pattern = Pattern.compile(emailPattern);
add(fieldName,
(
final String baseName,
final T data,
final List<String> modifiedValue,
final QueryOptions options) -> {
final Object elem = field.get(data);
if (elem == null) {
return;
}
final String elemTyped = (String) elem;
if (!pattern.matcher(elemTyped).find()) {
throw new InputException(baseName + fieldName,
"does not match the required pattern[email] (constraints) must be '"
+ emailPattern + "'");
}
});
}
} else if (type == JsonValue.class) {
final DataJson jsonAnnotation = AnnotationTools.getDataJson(field);
if (jsonAnnotation != null && jsonAnnotation.checker() != CheckFunctionVoid.class) {

View File

@@ -12,6 +12,7 @@ import java.util.Map.Entry;
import org.kar.archidata.annotation.security.PermitTokenInURI;
import org.kar.archidata.catcher.RestErrorResponse;
import org.kar.archidata.exception.SystemException;
import org.kar.archidata.model.UserByToken;
import org.kar.archidata.tools.JWTWrapper;
import org.slf4j.Logger;
@@ -23,6 +24,7 @@ import jakarta.annotation.Priority;
import jakarta.annotation.security.DenyAll;
import jakarta.annotation.security.PermitAll;
import jakarta.annotation.security.RolesAllowed;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Priorities;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
@@ -42,18 +44,40 @@ public class AuthenticationFilter implements ContainerRequestFilter {
@Context
private ResourceInfo resourceInfo;
protected final String applicationName;
protected final String issuer;
public static final String AUTHENTICATION_SCHEME = "Bearer";
public static final String APIKEY = "ApiKey";
public AuthenticationFilter(final String applicationName) {
this.applicationName = applicationName;
this.issuer = "KarAuth";
}
public AuthenticationFilter(final String applicationName, final String issuer) {
this.applicationName = applicationName;
this.issuer = issuer;
}
public String getRequestedPath(final ContainerRequestContext requestContext) {
final Class<?> resourceClass = this.resourceInfo.getResourceClass();
final Method resourceMethod = this.resourceInfo.getResourceMethod();
final String classPath = resourceClass.isAnnotationPresent(Path.class)
? resourceClass.getAnnotation(Path.class).value()
: "";
final String methodPath = resourceMethod.isAnnotationPresent(Path.class)
? resourceMethod.getAnnotation(Path.class).value()
: "";
final String fullPath = (classPath.startsWith("/") ? "" : "/") + classPath
+ (methodPath.startsWith("/") ? "" : "/") + methodPath;
return fullPath;
}
@Override
public void filter(final ContainerRequestContext requestContext) throws IOException {
/* logger.debug("-----------------------------------------------------"); logger.debug("---- Check if have authorization ----");
* logger.debug("-----------------------------------------------------"); logger.debug(" for:{}", requestContext.getUriInfo().getPath()); */
final Method method = this.resourceInfo.getResourceMethod();
// Access denied for all
if (method.isAnnotationPresent(DenyAll.class)) {
@@ -140,12 +164,13 @@ public class AuthenticationFilter implements ContainerRequestFilter {
final List<String> roles = Arrays.asList(rolesAnnotation.value());
// check if the user have the right:
boolean haveRight = false;
for (final String role : roles) {
if (userContext.isUserInRole(role)) {
haveRight = true;
break;
}
try {
haveRight = checkRight(requestContext, userContext, roles);
} catch (final SystemException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// Is user valid?
if (!haveRight) {
LOGGER.error("REJECTED not enought right : {} require: {}", requestContext.getUriInfo().getPath(), roles);
@@ -157,6 +182,18 @@ public class AuthenticationFilter implements ContainerRequestFilter {
// logger.debug("Get local user : {} / {}", user, userByToken);
}
protected boolean checkRight(
final ContainerRequestContext requestContext,
final MySecurityContext userContext,
final List<String> roles) throws SystemException {
for (final String role : roles) {
if (userContext.isUserInRole(this.applicationName + "/" + role)) {
return true;
}
}
return false;
}
private boolean isTokenBasedAuthentication(final String authorizationHeader) {
// Check if the Authorization header is valid
// It must not be null and must be prefixed with "Bearer" plus a whitespace
@@ -193,7 +230,7 @@ public class AuthenticationFilter implements ContainerRequestFilter {
// must be override to be good implementation
protected UserByToken validateJwtToken(final String authorization) throws Exception {
// logger.debug(" validate token : " + authorization);
final JWTClaimsSet ret = JWTWrapper.validateToken(authorization, "KarAuth", null);
final JWTClaimsSet ret = JWTWrapper.validateToken(authorization, this.issuer, null);
// check the token is valid !!! (signed and coherent issuer...
if (ret == null) {
LOGGER.error("The token is not valid: '{}'", authorization);
@@ -208,13 +245,16 @@ public class AuthenticationFilter implements ContainerRequestFilter {
user.type = UserByToken.TYPE_USER;
final Object rowRight = ret.getClaim("right");
if (rowRight != null) {
final Map<String, Map<String, Object>> rights = (Map<String, Map<String, Object>>) ret.getClaim("right");
LOGGER.info("Detect right in Authentication Filter: {}", rowRight);
user.right = (Map<String, Map<String, Object>>) ret.getClaim("right");
/*
if (rights.containsKey(this.applicationName)) {
user.right = rights.get(this.applicationName);
} else {
LOGGER.error("Connect with no right for this application='{}' full Right='{}'", this.applicationName,
rights);
}
*/
}
// logger.debug("request user: '{}' right: '{}' row='{}'", userUID, user.right, rowRight);
return user;

View File

@@ -1,13 +1,17 @@
package org.kar.archidata.filter;
import java.security.Principal;
import java.util.Set;
import org.kar.archidata.model.UserByToken;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import jakarta.ws.rs.core.SecurityContext;
// https://simplapi.wordpress.com/2015/09/19/jersey-jax-rs-securitycontext-in-action/
class MySecurityContext implements SecurityContext {
public class MySecurityContext implements SecurityContext {
private static final Logger LOGGER = LoggerFactory.getLogger(MySecurityContext.class);
private final GenericContext contextPrincipale;
private final String sheme;
@@ -22,17 +26,128 @@ class MySecurityContext implements SecurityContext {
return this.contextPrincipale;
}
@Override
public boolean isUserInRole(final String role) {
public Object getRightOfRoleInGroup(final String group, final String role) {
if (this.contextPrincipale.userByToken != null) {
final Object value = this.contextPrincipale.userByToken.right.get(role);
if (value instanceof final Boolean ret) {
return ret;
}
return this.contextPrincipale.userByToken.getRight(group, role);
}
return null;
}
public Set<String> getGroups() {
if (this.contextPrincipale.userByToken != null) {
return this.contextPrincipale.userByToken.getGroups();
}
return Set.of();
}
public boolean groupExist(final String group) {
if (this.contextPrincipale.userByToken != null) {
return this.contextPrincipale.userByToken.groupExist(group);
}
return false;
}
// Not sure the Long type is definitive.
public Long getUserID() {
if (this.contextPrincipale.userByToken != null) {
return this.contextPrincipale.userByToken.id;
}
return null;
}
public boolean checkRightInGroup(
final String group,
final String role,
final boolean needRead,
final boolean needWrite) {
if ("USER".equals(role)) {
if (groupExist(group)) {
return true;
}
return false;
}
// get associated Roles:
final Object rightPart = getRightOfRoleInGroup(group, role);
LOGGER.info("detect : {}", rightPart);
long dataRight = 0;
if (rightPart instanceof final Long rightPartCasted) {
dataRight = rightPartCasted;
} else if (rightPart instanceof final Integer rightPartCasted) {
dataRight = rightPartCasted;
}
if (dataRight == PartRight.READ_WRITE.getValue()) {
return true;
}
if (!needRead && needWrite && dataRight == PartRight.WRITE.getValue()) {
return true;
}
if (needRead && !needWrite && dataRight == PartRight.READ.getValue()) {
return true;
}
return false;
}
@Override
public boolean isUserInRole(final String role) {
String roleEdit = role;
boolean needRead = false;
boolean needWrite = false;
// Check if the API overwrite the right needed for this API.
if (roleEdit.contains(":")) {
if (roleEdit.endsWith(":w")) {
try {
roleEdit = roleEdit.substring(0, roleEdit.length() - 2);
} catch (final IndexOutOfBoundsException ex) {
LOGGER.error("Fail to extract role of '{}'", role);
ex.printStackTrace();
return false;
}
needWrite = true;
} else if (roleEdit.endsWith(":r")) {
try {
roleEdit = roleEdit.substring(0, roleEdit.length() - 2);
} catch (final IndexOutOfBoundsException ex) {
LOGGER.error("Fail to extract role of '{}'", role);
ex.printStackTrace();
return false;
}
needRead = true;
} else if (roleEdit.endsWith(":rw")) {
try {
roleEdit = roleEdit.substring(0, roleEdit.length() - 3);
} catch (final IndexOutOfBoundsException ex) {
LOGGER.error("Fail to extract role of '{}'", role);
ex.printStackTrace();
return false;
}
needRead = true;
needWrite = true;
} else {
LOGGER.error("Request check right of an unknow right mode: {} (after ':')", roleEdit);
return false;
}
}
if (roleEdit.contains("/")) {
final String[] elements = roleEdit.split("/");
return checkRightInGroup(elements[0], elements[1], needRead, needWrite);
}
// Special case, if the token is valid, it is an USER ...
if ("USER".equals(roleEdit)) {
return true;
}
return checkRightInGroup("?system?", roleEdit, needRead, needWrite);
}
public Object getRole(final String role) {
LOGGER.info("contextPrincipale={}", this.contextPrincipale);
if (this.contextPrincipale.userByToken != null) {
LOGGER.info("contextPrincipale.userByToken={}", this.contextPrincipale.userByToken);
LOGGER.info("contextPrincipale.userByToken.right={}", this.contextPrincipale.userByToken.right);
return this.contextPrincipale.userByToken.right.get(role);
}
return null;
}
@Override
public boolean isSecure() {
return "https".equalsIgnoreCase(this.sheme);

View File

@@ -0,0 +1,30 @@
package org.kar.archidata.filter;
import com.fasterxml.jackson.annotation.JsonValue;
public enum PartRight {
NONE(0), //
READ(1), //
WRITE(2), //
READ_WRITE(3);
private final int value;
PartRight(final int value) {
this.value = value;
}
@JsonValue
public int getValue() {
return this.value;
}
public static PartRight fromValue(final int value) {
for (final PartRight element : values()) {
if (element.getValue() == value) {
return element;
}
}
throw new IllegalArgumentException("PartRight: Unknown value: " + value);
}
}

View File

@@ -28,26 +28,29 @@ import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.annotation.Nullable;
import jakarta.persistence.Column;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import jakarta.ws.rs.DefaultValue;
@Table(name = "user")
@DataIfNotExists
@JsonInclude(JsonInclude.Include.NON_NULL)
public class User extends GenericDataSoftDelete {
@NotNull
@Column(length = 128)
@Size(min = 3, max = 128)
@Pattern(regexp = "^[a-zA-Z0-9-_ \\.]+$")
public String login = null;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
public Timestamp lastConnection = null;
@DefaultValue("'0'")
@Column(nullable = false)
public boolean admin = false;
@DefaultValue("'0'")
@Column(nullable = false)
public boolean blocked = false;
@DefaultValue("'0'")
@Column(nullable = false)
public boolean removed = false;
@Column(length = 512)
public String blockedReason;
@Schema(description = "List of Id of the specific covers")
@DataJson(targetEntity = Data.class)
@@ -56,7 +59,8 @@ public class User extends GenericDataSoftDelete {
@Override
public String toString() {
return "User [login=" + this.login + ", last=" + this.lastConnection + ", admin=" + this.admin + "]";
return "User [login=" + this.login + ", last=" + this.lastConnection + ", blocked=" + this.blocked
+ ", blockedReason=" + this.blockedReason + "]";
}
}

View File

@@ -2,6 +2,7 @@ package org.kar.archidata.model;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class UserByToken {
public static final int TYPE_USER = -1;
@@ -13,13 +14,35 @@ public class UserByToken {
public Long parentId = null; // FOr application, this is the id of the application, and of user token, this is the USERID
public String name = null;
// Right map
public Map<String, Object> right = new HashMap<>();
public Map<String, Map<String, Object>> right = new HashMap<>();
public boolean hasRight(final String key, final Object value) {
if (!this.right.containsKey(key)) {
public Set<String> getGroups() {
return this.right.keySet();
}
public boolean groupExist(final String group) {
if (!this.right.containsKey(group)) {
return false;
}
return this.right.containsKey(group);
}
public Object getRight(final String group, final String key) {
if (!this.right.containsKey(group)) {
return null;
}
final Map<String, Object> rightGroup = this.right.get(group);
if (!rightGroup.containsKey(key)) {
return null;
}
return rightGroup.get(key);
}
public boolean hasRight(final String group, final String key, final Object value) {
final Object data = getRight(group, key);
if (data == null) {
return false;
}
final Object data = this.right.get(key);
if (data instanceof final Boolean elem) {
if (value instanceof final Boolean castVal) {
if (elem.equals(castVal)) {

View File

@@ -121,7 +121,7 @@ public class RESTApi {
}
@SuppressWarnings("unchecked")
protected <T, U> T modelSendJson(final String model, final Class<T> clazz, final String urlOffset, String body)
public <T, U> T modelSendJson(final String model, final Class<T> clazz, final String urlOffset, String body)
throws RESTErrorResponseExeption, IOException, InterruptedException {
final HttpClient client = HttpClient.newHttpClient();
// client.property(HttpUrlConnectorProvider.SET_METHOD_WORKAROUND, true);
@@ -166,7 +166,7 @@ public class RESTApi {
}
@SuppressWarnings("unchecked")
protected <T> T modelSendMap(
public <T> T modelSendMap(
final String model,
final Class<T> clazz,
final String urlOffset,

View File

@@ -1 +1 @@
0.17.1-dev
0.19.0