Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f7be272
feat(agent): add the agent core that keeps policies current in systems
qianmoQ Oct 5, 2026
b6cd8ec
perf: page accounts along an index and look the console up once
qianmoQ Oct 5, 2026
d7b6422
perf(identity): index account search with trigrams on PostgreSQL
qianmoQ Oct 5, 2026
e8b651b
feat(hdfs): add the HDFS service type plugin shipped with the release
qianmoQ Oct 5, 2026
b7c3c9a
fix(identity): page accounts without Optional.get
qianmoQ Oct 5, 2026
e849559
perf(authz): work out snapshots in read-only transactions
qianmoQ Oct 5, 2026
4359141
feat(plugin-host): load the repository's plugins when run from sources
qianmoQ Oct 5, 2026
02bd6d1
feat(hdfs): talk to clusters with Hadoop's client like Apache Ranger
qianmoQ Oct 5, 2026
92aabb1
style(hdfs): clear the Error Prone warnings of the HDFS plugin
qianmoQ Oct 5, 2026
3796c31
fix(plugin-host): load only installable plugin modules from the sources
qianmoQ Oct 5, 2026
1437ec5
build: unpack the bundled HDFS plugin when packaging the server
qianmoQ Oct 5, 2026
607b6a8
ci: accept the Bouncy Castle licence in dependency review
qianmoQ Oct 5, 2026
455193b
ci: check commit messages after a force push
qianmoQ Oct 5, 2026
1d20eac
feat(hdfs): bound path lookups and validate cluster settings
qianmoQ Oct 5, 2026
0c10909
feat(agent): let agents credit GrantForge for a strict default deny
qianmoQ Oct 5, 2026
1445466
feat(hdfs): add the NameNode agent for Hadoop 3.5.0
qianmoQ Oct 5, 2026
362546c
feat(release): cut releases with one script and list their commits
qianmoQ Oct 5, 2026
4b3b27c
build: publish the Maven artifacts of a release
qianmoQ Oct 5, 2026
f7ab389
build: skip the release bundles in the database tests
qianmoQ Oct 5, 2026
fd9dace
ci: accept the licences of Hadoop's shaded protobuf
qianmoQ Oct 5, 2026
fcca96e
docs: rewrite the README in English with a Chinese edition
qianmoQ Oct 5, 2026
548b03e
ci: accept the licences of the Javadoc build plugin
qianmoQ Oct 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/dependency-review-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,6 @@ allow-dependencies-licenses:
# BSD-2-Clause together with the equally permissive BSD-2-Clause-Views.
- pkg:npm/robust-predicates
- pkg:npm/uri-js
# The Bouncy Castle Licence is the MIT license under its own name, which the review does not recognise.
- pkg:maven/org.bouncycastle/bcprov-jdk18on
comment-summary-in-pr: on-failure
12 changes: 9 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,11 @@ jobs:
with:
python-version: '3.12'
- name: Conventional commit messages
run: python3 script/ci/check_commit_messages.py --base "${{ github.event.pull_request.base.sha || github.event.before }}" --head "${{ github.event.pull_request.head.sha || github.sha }}"
run: >-
python3 script/ci/check_commit_messages.py
--base "${{ github.event.pull_request.base.sha || github.event.before }}"
--head "${{ github.event.pull_request.head.sha || github.sha }}"
--fallback "origin/${{ github.event.repository.default_branch }}"

ci-scripts:
name: CI script unit tests
Expand Down Expand Up @@ -103,8 +107,10 @@ jobs:
run: bash script/ci/java.sh test
- name: Check Java 17 bytecode compatibility
run: python3 script/ci/check_java_bytecode.py --max-major 61
- name: Check Java 8 bytecode of the policy engine agents embed
run: python3 script/ci/check_java_bytecode.py --max-major 52 --module core/grantforge-policy-engine --main-only
- name: Check Java 8 bytecode of the policy engine and agent core agents embed
run: |
python3 script/ci/check_java_bytecode.py --max-major 52 --module core/grantforge-policy-engine --main-only
python3 script/ci/check_java_bytecode.py --max-major 52 --module core/grantforge-agent-core --main-only
- name: Upload test reports
if: always()
uses: actions/upload-artifact@v4
Expand Down
10 changes: 10 additions & 0 deletions configure/assembly/server.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,16 @@
</dependencySets>

<fileSets>
<!-- Bundled service type plugins, unpacked by the server's build into plugins/<id> where the host finds them. -->
<fileSet>
<directory>target/bundled-plugins</directory>
<outputDirectory>plugins</outputDirectory>
</fileSet>
<!-- Agents are installed in the protected systems, separate from the server plugin class loaders. -->
<fileSet>
<directory>target/bundled-agents</directory>
<outputDirectory>agents</outputDirectory>
</fileSet>
<fileSet>
<directory>${project.parent.basedir}/script/bin</directory>
<outputDirectory>bin</outputDirectory>
Expand Down
66 changes: 66 additions & 0 deletions core/grantforge-agent-core/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright (c) 2026 devlive-community/grantforge

Licensed under the MIT License. See the LICENSE file in the
project root for full license text.
-->

<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>grantforge</artifactId>
<groupId>org.devlive.grantforge</groupId>
<version>2026.0.0</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>grantforge-agent-core</artifactId>
<name>GrantForge Agent Core</name>
<description>What every agent in a protected system shares: downloads and checks the signed policy snapshots of its
service, keeps the last one on disk, decides access with the policy engine and ships access events; runs on
Java 8 with the engine, Jackson 2 and Bouncy Castle</description>

<properties>
<!-- Agents run inside Hadoop and Hive, many of which still run on Java 8 (D-39); the tests may use Java 17. -->
<maven.compiler.release>8</maven.compiler.release>
<maven.compiler.testRelease>17</maven.compiler.testRelease>
</properties>

<dependencies>
<dependency>
<groupId>org.devlive.grantforge</groupId>
<artifactId>grantforge-policy-engine</artifactId>
</dependency>
<!-- Jackson 2, as Jackson 3 needs Java 17. The protected systems bring their own Jackson, so the agents that
embed this library relocate it when they package themselves. -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- Ed25519 signature checks: the JDK has them only from Java 15 on. Relocated by the agents like Jackson. -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
</dependency>
<!-- Annotations only, for null checking at compile time; nothing needed at run time. -->
<dependency>
<groupId>org.jspecify</groupId>
<artifactId>jspecify</artifactId>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.devlive.grantforge</groupId>
<artifactId>grantforge-test-support</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
// Copyright (c) 2026 devlive-community/grantforge
//
// Licensed under the MIT License. See the LICENSE file in the
// project root for full license text.

package org.devlive.grantforge.agent;

import org.jspecify.annotations.Nullable;

import java.time.Instant;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;

/**
* One access the agent saw, as the server's access audit stores it. Built with {@link #builder}; each event gets its
* own id, which makes sending it again harmless. Immutable.
*/
public final class AccessEvent
{
/** Longest request text kept, as the server stores it. */
public static final int MAX_REQUEST = 1000;

private final String eventId;
private final Instant occurredAt;
private final String user;
private final String resource;
private final String accessType;
private final boolean allowed;
private final @Nullable String clientIp;
private final @Nullable String resourceType;
private final @Nullable String action;
private final @Nullable Long policyId;
private final @Nullable Long policyVersion;
private final boolean byGrantForge;
private final @Nullable String request;

AccessEvent(Builder builder)
{
this.eventId = UUID.randomUUID().toString();
this.occurredAt = builder.occurredAt;
this.user = builder.user;
this.resource = builder.resource;
this.accessType = builder.accessType;
this.allowed = builder.allowed;
this.clientIp = builder.clientIp;
this.resourceType = builder.resourceType;
this.action = builder.action;
this.policyId = builder.policyId;
this.policyVersion = builder.policyVersion;
this.byGrantForge = builder.byGrantForge;
String text = builder.request;
this.request = text == null || text.length() <= MAX_REQUEST ? text : text.substring(0, MAX_REQUEST);
}

/**
* Starts an event.
*
* @param user who
* @param resource the resource, its levels joined as the system shows them, such as {@code /data/sales} or
* {@code sales.orders.ssn}
* @param accessType the access type checked
* @param allowed whether access was allowed in the end
* @return a builder
*/
public static Builder builder(String user, String resource, String accessType, boolean allowed)
{
return new Builder(user, resource, accessType, allowed);
}

/**
* Returns the event's id.
*
* @return a UUID
*/
public String eventId()
{
return eventId;
}

/**
* Returns the event as the server's access event API takes it.
*
* @return the fields, by name
*/
Map<String, @Nullable Object> fields()
{
Map<String, @Nullable Object> fields = new LinkedHashMap<>();
fields.put("eventId", eventId);
fields.put("occurredAt", occurredAt.toString());
fields.put("user", user);
fields.put("clientIp", clientIp);
fields.put("resource", resource);
fields.put("resourceType", resourceType);
fields.put("accessType", accessType);
fields.put("action", action);
fields.put("outcome", allowed ? "ALLOWED" : "DENIED");
fields.put("policyId", policyId == null ? null : Long.toString(policyId));
fields.put("policyVersion", policyVersion);
fields.put("enforcer", byGrantForge ? "GRANTFORGE" : "NATIVE");
fields.put("request", request);
return fields;
}

/** Collects an event. */
public static final class Builder
{
final String user;
final String resource;
final String accessType;
final boolean allowed;
Instant occurredAt = Instant.now();
@Nullable String clientIp;
@Nullable String resourceType;
@Nullable String action;
@Nullable Long policyId;
@Nullable Long policyVersion;
boolean byGrantForge;
@Nullable String request;

Builder(String user, String resource, String accessType, boolean allowed)
{
this.user = user;
this.resource = resource;
this.accessType = accessType;
this.allowed = allowed;
}

/**
* Says who decided: GrantForge when the decision was determined, the system's own checks otherwise.
*
* @param decision the agent's decision
* @return this builder
*/
public Builder decidedBy(AgentDecision decision)
{
this.byGrantForge = decision.determined();
this.policyId = decision.policyId();
this.policyVersion = decision.policyVersion();
return this;
}

/**
* Credits GrantForge for enforcing access without a matching policy, such as an agent's strict default deny.
* Call after {@link #decidedBy} when retaining the snapshot version of an undetermined decision.
*
* @return this builder
*/
public Builder enforcedByGrantForge()
{
this.byGrantForge = true;
return this;
}

/**
* Sets when it happened; now by default.
*
* @param value the moment
* @return this builder
*/
public Builder occurredAt(Instant value)
{
this.occurredAt = value;
return this;
}

/**
* Sets from where.
*
* @param value the client address
* @return this builder
*/
public Builder clientIp(@Nullable String value)
{
this.clientIp = value;
return this;
}

/**
* Sets the lowest level of the resource, such as {@code column}.
*
* @param value the level
* @return this builder
*/
public Builder resourceType(@Nullable String value)
{
this.resourceType = value;
return this;
}

/**
* Sets the system's operation, such as {@code open} or {@code SELECT}.
*
* @param value the operation
* @return this builder
*/
public Builder action(@Nullable String value)
{
this.action = value;
return this;
}

/**
* Sets the request, such as an SQL statement; cut to {@value #MAX_REQUEST} characters.
*
* @param value the request
* @return this builder
*/
public Builder request(@Nullable String value)
{
this.request = value;
return this;
}

/**
* Builds the event.
*
* @return the event
*/
public AccessEvent build()
{
return new AccessEvent(this);
}
}
}
Loading
Loading