-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: support generating role templates based on OpenAPI docs (#30)
### What this PR does? 支持根据 OpenAPI Docs 生成 Role Templates 在 Halo 插件开发中,权限管理是一个关键问题,尤其是配置[角色模板](https://docs.halo.run/developer-guide/plugin/api-reference/server/role-template/#%E8%A7%92%E8%89%B2%E6%A8%A1%E6%9D%BF%E5%AE%9A%E4%B9%89)时,角色的 `rules` 部分往往让开发者感到困惑。具体来说,如何区分资源、apiGroup、verb 等概念是许多开发者的痛点。 `generateRoleTemplates` Task 的出现正是为了简化这一过程,该任务能够根据 [配置 Generate Api Client](https://github.com/halo-sigs/halo-gradle-plugin?tab=readme-ov-file#%E9%85%8D%E7%BD%AE-generateapiclient) 中的配置获取到 OpenAPI docs 的 JSON 文件,并自动生成 Halo 的 Role YAML 文件,让开发者可以专注于自己的业务逻辑,而不是纠结于复杂的角色 `rules` 配置。 ```release-note 支持根据 OpenAPI Docs 生成 Role Templates ```
- Loading branch information
Showing
13 changed files
with
946 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 1 addition & 1 deletion
2
...adle/openapi/GroupedOpenApiExtension.java → ...le/extension/GroupedOpenApiExtension.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
package run.halo.gradle.role; | ||
|
||
import java.util.Objects; | ||
import lombok.Getter; | ||
import lombok.ToString; | ||
import org.apache.commons.lang3.StringUtils; | ||
|
||
/** | ||
* @see | ||
* <a href="https://github.com/halo-dev/halo/blob/17e9f2be1f63cbfe328d806987a284c702be79fe/application/src/main/java/run/halo/app/security/authorization/RequestInfo.java#L17">Halo RequestInfo</a> | ||
*/ | ||
@Getter | ||
@ToString | ||
public class RequestInfo { | ||
boolean isResourceRequest; | ||
final String path; | ||
String namespace; | ||
String userspace; | ||
String verb; | ||
String apiPrefix; | ||
String apiGroup; | ||
String apiVersion; | ||
String resource; | ||
|
||
String name; | ||
|
||
String subresource; | ||
|
||
String subName; | ||
|
||
String[] parts; | ||
|
||
public RequestInfo(boolean isResourceRequest, String path, String verb) { | ||
this(isResourceRequest, path, null, null, verb, null, null, null, null, null, null, null, | ||
null); | ||
} | ||
|
||
public RequestInfo(boolean isResourceRequest, String path, String namespace, String userspace, | ||
String verb, | ||
String apiPrefix, | ||
String apiGroup, | ||
String apiVersion, String resource, String name, String subresource, String subName, | ||
String[] parts) { | ||
this.isResourceRequest = isResourceRequest; | ||
this.path = StringUtils.defaultString(path); | ||
this.namespace = StringUtils.defaultString(namespace); | ||
this.userspace = StringUtils.defaultString(userspace); | ||
this.verb = StringUtils.defaultString(verb); | ||
this.apiPrefix = StringUtils.defaultString(apiPrefix); | ||
this.apiGroup = StringUtils.defaultString(apiGroup); | ||
this.apiVersion = StringUtils.defaultString(apiVersion); | ||
this.resource = StringUtils.defaultString(resource); | ||
this.subresource = StringUtils.defaultString(subresource); | ||
this.subName = StringUtils.defaultString(subName); | ||
this.name = StringUtils.defaultString(name); | ||
this.parts = Objects.requireNonNullElseGet(parts, () -> new String[] {}); | ||
} | ||
} |
166 changes: 166 additions & 0 deletions
166
src/main/java/run/halo/gradle/role/RequestInfoFactory.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
package run.halo.gradle.role; | ||
|
||
import java.util.Arrays; | ||
import java.util.Objects; | ||
import java.util.Set; | ||
import org.apache.commons.lang3.StringUtils; | ||
|
||
/** | ||
* @see | ||
* <a href="https://github.com/halo-dev/halo/blob/17e9f2be1f63cbfe328d806987a284c702be79fe/application/src/main/java/run/halo/app/security/authorization/RequestInfoFactory.java#L17>Halo RequestInfoFactory</a> | ||
*/ | ||
public class RequestInfoFactory { | ||
public static final RequestInfoFactory INSTANCE = | ||
new RequestInfoFactory(Set.of("api", "apis"), Set.of("api")); | ||
|
||
/** | ||
* without leading and trailing slashes. | ||
*/ | ||
final Set<String> apiPrefixes; | ||
|
||
/** | ||
* without leading and trailing slashes. | ||
*/ | ||
final Set<String> grouplessApiPrefixes; | ||
|
||
/** | ||
* special verbs no subresources. | ||
*/ | ||
final Set<String> specialVerbs; | ||
|
||
public RequestInfoFactory(Set<String> apiPrefixes, Set<String> grouplessApiPrefixes) { | ||
this(apiPrefixes, grouplessApiPrefixes, Set.of("proxy", "watch")); | ||
} | ||
|
||
public RequestInfoFactory(Set<String> apiPrefixes, Set<String> grouplessApiPrefixes, | ||
Set<String> specialVerbs) { | ||
this.apiPrefixes = apiPrefixes; | ||
this.grouplessApiPrefixes = grouplessApiPrefixes; | ||
this.specialVerbs = specialVerbs; | ||
} | ||
|
||
public RequestInfo newRequestInfo(String requestPath, String requestMethod) { | ||
// non-resource request default | ||
RequestInfo requestInfo = | ||
new RequestInfo(false, requestPath, requestMethod.toLowerCase()); | ||
|
||
String[] currentParts = splitPath(requestPath); | ||
|
||
if (currentParts.length < 3) { | ||
// return a non-resource request | ||
return requestInfo; | ||
} | ||
|
||
if (!apiPrefixes.contains(currentParts[0])) { | ||
// return a non-resource request | ||
return requestInfo; | ||
} | ||
requestInfo.apiPrefix = currentParts[0]; | ||
currentParts = Arrays.copyOfRange(currentParts, 1, currentParts.length); | ||
|
||
if (!grouplessApiPrefixes.contains(requestInfo.apiPrefix)) { | ||
// one part (APIPrefix) has already been consumed, so this is actually "do we have | ||
// four parts?" | ||
if (currentParts.length < 3) { | ||
// return a non-resource request | ||
return requestInfo; | ||
} | ||
|
||
requestInfo.apiGroup = StringUtils.defaultString(currentParts[0]); | ||
currentParts = Arrays.copyOfRange(currentParts, 1, currentParts.length); | ||
} | ||
requestInfo.isResourceRequest = true; | ||
requestInfo.apiVersion = currentParts[0]; | ||
currentParts = Arrays.copyOfRange(currentParts, 1, currentParts.length); | ||
// handle input of form /{specialVerb}/* | ||
Set<String> specialVerbs = Set.of("proxy", "watch"); | ||
if (specialVerbs.contains(currentParts[0])) { | ||
if (currentParts.length < 2) { | ||
throw new IllegalArgumentException( | ||
String.format("unable to determine kind and namespace from url, %s", | ||
requestPath)); | ||
} | ||
requestInfo.verb = currentParts[0]; | ||
currentParts = Arrays.copyOfRange(currentParts, 1, currentParts.length); | ||
} else { | ||
requestInfo.verb = switch (requestMethod.toUpperCase()) { | ||
case "POST" -> "create"; | ||
case "GET", "HEAD" -> "get"; | ||
case "PUT" -> "update"; | ||
case "PATCH" -> "patch"; | ||
case "DELETE" -> "delete"; | ||
default -> ""; | ||
}; | ||
} | ||
// URL forms: /namespaces/{namespace}/{kind}/*, where parts are adjusted to be relative | ||
// to kind | ||
Set<String> namespaceSubresources = Set.of("status", "finalize"); | ||
if (Objects.equals(currentParts[0], "namespaces")) { | ||
if (currentParts.length > 1) { | ||
requestInfo.namespace = currentParts[1]; | ||
|
||
// if there is another step after the namespace name and it is not a known | ||
// namespace subresource | ||
// move currentParts to include it as a resource in its own right | ||
if (currentParts.length > 2 && !namespaceSubresources.contains(currentParts[2])) { | ||
currentParts = Arrays.copyOfRange(currentParts, 2, currentParts.length); | ||
} | ||
} | ||
} else if ("userspaces".equals(currentParts[0])) { | ||
if (currentParts.length > 1) { | ||
requestInfo.userspace = currentParts[1]; | ||
|
||
// if there is another step after the userspace name | ||
// move currentParts to include it as a resource in its own right | ||
if (currentParts.length > 2) { | ||
currentParts = Arrays.copyOfRange(currentParts, 2, currentParts.length); | ||
} | ||
} | ||
} else { | ||
requestInfo.userspace = ""; | ||
requestInfo.namespace = ""; | ||
} | ||
|
||
// parsing successful, so we now know the proper value for .Parts | ||
requestInfo.parts = currentParts; | ||
// special verbs no subresources | ||
// parts look like: resource/resourceName/subresource/other/stuff/we/don't/interpret | ||
if (requestInfo.parts.length >= 3 && !specialVerbs.contains( | ||
requestInfo.verb)) { | ||
requestInfo.subresource = requestInfo.parts[2]; | ||
// if there is another step after the subresource name, and it is not a known | ||
if (requestInfo.parts.length >= 4) { | ||
requestInfo.subName = requestInfo.parts[3]; | ||
} | ||
} | ||
|
||
if (requestInfo.parts.length >= 2) { | ||
requestInfo.name = requestInfo.parts[1]; | ||
} | ||
|
||
if (requestInfo.parts.length >= 1) { | ||
requestInfo.resource = requestInfo.parts[0]; | ||
} | ||
|
||
// has name and no subresource but verb=create, then this is a non-resource request | ||
if (StringUtils.isNotBlank(requestInfo.name) && StringUtils.isBlank(requestInfo.subresource) | ||
&& "create".equals(requestInfo.verb)) { | ||
requestInfo.isResourceRequest = false; | ||
} | ||
|
||
// if there's no name on the request, and we thought it was a get before, then the actual | ||
// verb is a list or a watch | ||
if (requestInfo.name.isEmpty() && "get".equals(requestInfo.verb)) { | ||
requestInfo.verb = "list"; | ||
} | ||
return requestInfo; | ||
} | ||
|
||
private String[] splitPath(String path) { | ||
path = StringUtils.strip(path, "/"); | ||
if (StringUtils.isEmpty(path)) { | ||
return new String[] {}; | ||
} | ||
return StringUtils.split(path, "/"); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
package run.halo.gradle.role; | ||
|
||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties; | ||
import java.util.ArrayList; | ||
import java.util.HashMap; | ||
import java.util.List; | ||
import java.util.Map; | ||
import lombok.Builder; | ||
import lombok.Data; | ||
import lombok.NoArgsConstructor; | ||
|
||
@Data | ||
public class Role { | ||
private final String kind = "Role"; | ||
|
||
private final String apiVersion = "v1alpha1"; | ||
|
||
private final Metadata metadata = new Metadata(); | ||
|
||
private final List<PolicyRule> rules = new ArrayList<>(); | ||
|
||
@Data | ||
@NoArgsConstructor | ||
public static class PolicyRule { | ||
private String[] apiGroups; | ||
|
||
private String[] resources; | ||
|
||
private String[] resourceNames; | ||
|
||
private String[] nonResourceURLs; | ||
|
||
private String[] verbs; | ||
|
||
@Builder | ||
public PolicyRule(String[] apiGroups, String[] resources, String[] resourceNames, | ||
String[] nonResourceURLs, String[] verbs) { | ||
this.apiGroups = apiGroups; | ||
this.resources = resources; | ||
this.resourceNames = resourceNames; | ||
this.nonResourceURLs = nonResourceURLs; | ||
this.verbs = verbs; | ||
} | ||
} | ||
|
||
@Data | ||
@JsonIgnoreProperties(ignoreUnknown = true) | ||
public static class Metadata { | ||
private String name; | ||
private Map<String, String> labels = new HashMap<>(); | ||
private Map<String, String> annotations = new HashMap<>(); | ||
} | ||
} |
Oops, something went wrong.