服务类型插件描述一种外部系统:它有哪些资源层级、哪些访问类型、能否脱敏和行过滤、连接需要哪些配置。GrantForge 据此为这类系统提供数据服务、通用的策略编辑器、策略快照与访问审计(见 数据服务与策略)。
依赖
插件只依赖 grantforge-plugin-api(只依赖 JDK 与 JSpecify),以 provided 范围引入:
<dependency>
<groupId>org.devlive.grantforge</groupId>
<artifactId>grantforge-plugin-api</artifactId>
<version>${grantforge.version}</version>
<scope>provided</scope>
</dependency>实现 ServiceTypeProvider
public final class ExampleProvider implements ServiceTypeProvider
{
@Override
public ServiceTypeDefinition definition()
{
return ServiceTypeDefinition.builder("example").label("Example warehouse")
.resources(ResourceDefinition.builder("database").label("Database").lookupSupported(true).validLeaf(true)
.excludesSupported(false).build(),
ResourceDefinition.builder("table").label("Table").parent("database").lookupSupported(true).validLeaf(true).build(),
ResourceDefinition.builder("column").label("Column").parent("table").accessTypes("select").build(),
ResourceDefinition.builder("path").label("Path").matcher(MatcherType.PATH).recursiveSupported(true).build())
.accessTypes(AccessTypeDefinition.of("select", "Select"), AccessTypeDefinition.of("update", "Update"),
AccessTypeDefinition.of("all", "All", "select", "update"))
.dataMask(new DataMaskDefinition(Set.of("column"), List.of(new MaskTypeDefinition("redact", "Redact", "redact({col})"))))
.rowFilter(new RowFilterDefinition(Set.of("table")))
.conditions(ConditionDefinition.of("ip-range", "Client addresses", "ip-range"))
.configFields(ConfigField.builder("url").label("Address").type(ConfigFieldType.STRING).mandatory().pattern("example://.+").build(),
ConfigField.builder("timeout").label("Timeout (seconds)").type(ConfigFieldType.INTEGER).defaultValue("30").build(),
ConfigField.builder("password").label("Password").type(ConfigFieldType.SECRET).mandatory().build())
.build();
}
@Override
public ConnectionResult testConnection(ServiceConfig config)
{
return "example".equals(config.get("password")) ? ConnectionResult.succeeded()
: ConnectionResult.failed("the example warehouse refused the password");
}
@Override
public List<String> lookup(LookupRequest request)
{
// 返回 request.resource() 层级下以 request.userInput() 开头的候选值,最多 request.limit() 个
return List.of();
}
}以上摘自示例插件 plugins/grantforge-plugin-example,可以直接复制作为模板。
定义在构造时一次性校验并报告全部问题:父级未知或成环、重名、引用了未声明的访问类型或资源等。名称必须匹配 [a-z][a-z0-9_-]{0,63}。
| 部件 | 说明 |
|---|---|
| 资源 | 层级、匹配方式(精确、通配、路径、正则)、是否区分大小写、是否必填、是否支持排除与递归(仅路径)、是否支持查找、是否可作为叶子 |
| 访问类型 | 名称、显示名、蕴含的其他访问类型(如 all 蕴含 select),可限定资源 |
| 脱敏、行过滤 | 声明哪些资源支持、有哪些脱敏方式;执行在目标系统 |
| 条件 | 策略可附加的条件(如 IP 范围),由策略引擎的条件 SPI 求值 |
| 配置字段 | 字符串、长文本、整数、布尔、密钥、枚举;密钥字段加密保存,不能有默认值 |
提供者需要公开无参构造并且线程安全。validateConfig、testConnection、lookup 都有默认实现,按需覆盖。
描述符与打包
插件根目录放 grantforge-plugin.yaml:
id: example
version: 1.0.0
name: Example warehouse
description: A sample service type that shows what a plugin can declare
apiVersion: "1.0"
providers:
- org.devlive.grantforge.example.ExampleProvider插件可以是:
- 一个 jar,描述符在 jar 根目录;
- 一个目录或 zip:
grantforge-plugin.yaml、classes/与lib/*.jar。
把它放进 grantforge.plugins.directory(默认 plugins),在控制台的“插件”页点击重新扫描即可,无需重启。
兼容性
apiVersion 声明插件需要的契约版本。宿主当前提供 1.0.0,主版本相同且不低于所需版本的插件才会加载,否则标为“不兼容”。契约的每次变化都会提升版本,CI 用 japicmp 与上一个发行版比较(script/ci/check_plugin_api_compat.py),不兼容的改动必须提升主版本。
隔离
- 每个插件有自己的类加载器,父加载器是平台类加载器,只有
org.devlive.grantforge.plugin.api.与org.jspecify.annotations.委托给宿主;插件看不到 Spring 和服务端的类,可以自带任意版本的依赖。 - 读取失败、版本不兼容、重复、构造异常或超时只会让这个插件被标为失败并记录原因,服务照常运行。
- 对插件的每次调用都有超时(
grantforge.plugins.call-timeout,默认 10 秒)。
代理与快照
目标系统里的代理用代理令牌访问 /api/v1/agent/**:
| 接口 | 作用 |
|---|---|
POST /api/v1/agent/heartbeat |
报告代理的状态与当前快照版本 |
GET /api/v1/agent/policies |
下载策略快照;未变化时返回 304;响应头带 Ed25519 签名 |
GET /api/v1/agent/signing-key |
验证签名用的公钥 |
POST /api/v1/agent/access-events |
批量上报访问事件,进入访问审计 |
代理用 grantforge-policy-engine(Java 8 API,可以嵌入较老的系统)在本地求值,不必每次访问都调用 GrantForge。
示例
plugins/grantforge-plugin-example 是一个完整的插件:类型 example(database → table → column 与 path),访问类型 select、update、all,列脱敏、表行过滤、IP 范围条件,配置 url、timeout、password(密码为 example 时测试连接成功),并能查找示例库表。全栈端到端测试用它走完“添加服务 → 写策略 → 签发令牌 → 代理拉取 → 访问审计”。
