mirror of
https://github.com/larksuite/cli.git
synced 2026-07-06 00:06:28 +08:00
385 lines
9.9 KiB
Go
385 lines
9.9 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package registry
|
|
|
|
import (
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// IdentityToAccessToken maps the --identity flag value to the corresponding
|
|
// accessTokens value used in from_meta JSON files. Bot identity uses
|
|
// tenant_access_token, so "bot" maps to "tenant".
|
|
func IdentityToAccessToken(identity string) string {
|
|
if identity == "bot" {
|
|
return "tenant"
|
|
}
|
|
return identity
|
|
}
|
|
|
|
// FilterScopes filters scopes by domain and permission level.
|
|
func FilterScopes(allScopes []string, domains []string, permissions []string) []string {
|
|
var result []string
|
|
for _, scope := range allScopes {
|
|
parts := strings.Split(scope, ":")
|
|
|
|
if len(domains) > 0 {
|
|
if len(parts) == 0 {
|
|
continue
|
|
}
|
|
found := false
|
|
for _, d := range domains {
|
|
if parts[0] == d {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
continue
|
|
}
|
|
}
|
|
|
|
if len(permissions) > 0 {
|
|
if len(parts) < 3 {
|
|
continue
|
|
}
|
|
perm := parts[2]
|
|
matched := false
|
|
for _, p := range permissions {
|
|
switch p {
|
|
case "read":
|
|
if strings.Contains(perm, "read") {
|
|
matched = true
|
|
}
|
|
case "write":
|
|
if strings.Contains(perm, "write") {
|
|
matched = true
|
|
}
|
|
case "readonly":
|
|
if perm == "readonly" {
|
|
matched = true
|
|
}
|
|
case "writeonly":
|
|
if perm == "writeonly" || perm == "write_only" {
|
|
matched = true
|
|
}
|
|
}
|
|
}
|
|
if !matched {
|
|
continue
|
|
}
|
|
}
|
|
|
|
result = append(result, scope)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// CollectScopesForProjects collects the recommended scope for each API method
|
|
// in the specified from_meta projects. For each method, only the scope with
|
|
// the highest priority score is selected.
|
|
func CollectScopesForProjects(projects []string, identity string) []string {
|
|
priorities := LoadScopePriorities()
|
|
scopeSet := make(map[string]bool)
|
|
for _, project := range projects {
|
|
spec := LoadFromMeta(project)
|
|
if spec == nil {
|
|
continue
|
|
}
|
|
resources, ok := spec["resources"].(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
for _, resSpec := range resources {
|
|
resMap, ok := resSpec.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
methods, ok := resMap["methods"].(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
for _, methodSpec := range methods {
|
|
methodMap, ok := methodSpec.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
if tokens, ok := methodMap["accessTokens"].([]interface{}); ok {
|
|
supported := false
|
|
for _, t := range tokens {
|
|
if ts, ok := t.(string); ok && ts == IdentityToAccessToken(identity) {
|
|
supported = true
|
|
break
|
|
}
|
|
}
|
|
if !supported {
|
|
continue
|
|
}
|
|
}
|
|
scopes, ok := methodMap["scopes"].([]interface{})
|
|
if !ok || len(scopes) == 0 {
|
|
continue
|
|
}
|
|
bestScope := ""
|
|
bestScore := -1
|
|
for _, s := range scopes {
|
|
str, ok := s.(string)
|
|
if !ok {
|
|
continue
|
|
}
|
|
score := DefaultScopeScore
|
|
if v, exists := priorities[str]; exists {
|
|
score = v
|
|
}
|
|
if score > bestScore {
|
|
bestScore = score
|
|
bestScope = str
|
|
}
|
|
}
|
|
if bestScope != "" {
|
|
scopeSet[bestScope] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
result := make([]string, 0, len(scopeSet))
|
|
for s := range scopeSet {
|
|
result = append(result, s)
|
|
}
|
|
sort.Strings(result)
|
|
return result
|
|
}
|
|
|
|
// ScopeSource tracks which APIs and shortcuts contributed a scope.
|
|
type ScopeSource struct {
|
|
APIs []string // e.g. "POST calendar.event.create"
|
|
Shortcuts []string // e.g. "+send", "+reply"
|
|
}
|
|
|
|
// CollectScopesWithSources is like CollectScopesForProjects but also records
|
|
// which API method contributed each scope. Used by scope-audit.
|
|
func CollectScopesWithSources(projects []string, identity string) ([]string, map[string]*ScopeSource) {
|
|
priorities := LoadScopePriorities()
|
|
scopeSet := make(map[string]bool)
|
|
sources := make(map[string]*ScopeSource)
|
|
|
|
for _, project := range projects {
|
|
spec := LoadFromMeta(project)
|
|
if spec == nil {
|
|
continue
|
|
}
|
|
resources, ok := spec["resources"].(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
for resName, resSpec := range resources {
|
|
resMap, ok := resSpec.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
methods, ok := resMap["methods"].(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
for methodName, methodSpec := range methods {
|
|
methodMap, ok := methodSpec.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
if tokens, ok := methodMap["accessTokens"].([]interface{}); ok {
|
|
supported := false
|
|
for _, t := range tokens {
|
|
if ts, ok := t.(string); ok && ts == IdentityToAccessToken(identity) {
|
|
supported = true
|
|
break
|
|
}
|
|
}
|
|
if !supported {
|
|
continue
|
|
}
|
|
}
|
|
scopes, ok := methodMap["scopes"].([]interface{})
|
|
if !ok || len(scopes) == 0 {
|
|
continue
|
|
}
|
|
bestScope := ""
|
|
bestScore := -1
|
|
for _, s := range scopes {
|
|
str, ok := s.(string)
|
|
if !ok {
|
|
continue
|
|
}
|
|
score := DefaultScopeScore
|
|
if v, exists := priorities[str]; exists {
|
|
score = v
|
|
}
|
|
if score > bestScore {
|
|
bestScore = score
|
|
bestScope = str
|
|
}
|
|
}
|
|
if bestScope != "" {
|
|
scopeSet[bestScope] = true
|
|
if sources[bestScope] == nil {
|
|
sources[bestScope] = &ScopeSource{}
|
|
}
|
|
methodID := GetStrFromMap(methodMap, "id")
|
|
if methodID == "" {
|
|
methodID = project + "." + resName + "." + methodName
|
|
}
|
|
httpMethod := GetStrFromMap(methodMap, "httpMethod")
|
|
if httpMethod == "" {
|
|
httpMethod = "?"
|
|
}
|
|
sources[bestScope].APIs = append(sources[bestScope].APIs, httpMethod+" "+methodID)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sort API lists for stable output
|
|
for _, src := range sources {
|
|
sort.Strings(src.APIs)
|
|
}
|
|
|
|
result := make([]string, 0, len(scopeSet))
|
|
for s := range scopeSet {
|
|
result = append(result, s)
|
|
}
|
|
sort.Strings(result)
|
|
return result, sources
|
|
}
|
|
|
|
// CommandEntry represents a CLI command (API method or shortcut) and its scopes.
|
|
type CommandEntry struct {
|
|
Command string // CLI label, e.g. "calendars create" or "+agenda"
|
|
Type string // "api" or "shortcut"
|
|
Scopes []string // effective scopes (requiredScopes if present, else [bestScope])
|
|
HTTPMethod string // e.g. "POST" (API only)
|
|
}
|
|
|
|
// CollectCommandScopes walks from_meta methods for the given projects and
|
|
// returns one CommandEntry per API method, sorted by command label.
|
|
//
|
|
// Scope selection per method:
|
|
// - If the method has a "requiredScopes" field, all of those scopes are needed (conjunction).
|
|
// - Otherwise, only the highest-priority scope from "scopes" is shown (minimum privilege).
|
|
func CollectCommandScopes(projects []string, identity string) []CommandEntry {
|
|
priorities := LoadScopePriorities()
|
|
var entries []CommandEntry
|
|
|
|
for _, project := range projects {
|
|
spec := LoadFromMeta(project)
|
|
if spec == nil {
|
|
continue
|
|
}
|
|
resources, ok := spec["resources"].(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
for resName, resSpec := range resources {
|
|
resMap, ok := resSpec.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
methods, ok := resMap["methods"].(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
for methodName, methodSpec := range methods {
|
|
methodMap, ok := methodSpec.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
if tokens, ok := methodMap["accessTokens"].([]interface{}); ok {
|
|
supported := false
|
|
for _, t := range tokens {
|
|
if ts, ok := t.(string); ok && ts == IdentityToAccessToken(identity) {
|
|
supported = true
|
|
break
|
|
}
|
|
}
|
|
if !supported {
|
|
continue
|
|
}
|
|
}
|
|
rawScopes, ok := methodMap["scopes"].([]interface{})
|
|
if !ok || len(rawScopes) == 0 {
|
|
continue
|
|
}
|
|
|
|
// Check for requiredScopes (conjunction — all needed)
|
|
var effectiveScopes []string
|
|
if reqRaw, ok := methodMap["requiredScopes"].([]interface{}); ok && len(reqRaw) > 0 {
|
|
for _, s := range reqRaw {
|
|
if str, ok := s.(string); ok {
|
|
effectiveScopes = append(effectiveScopes, str)
|
|
}
|
|
}
|
|
} else {
|
|
// Pick the single best scope (minimum privilege)
|
|
bestScope := ""
|
|
bestScore := -1
|
|
for _, s := range rawScopes {
|
|
str, ok := s.(string)
|
|
if !ok {
|
|
continue
|
|
}
|
|
score := DefaultScopeScore
|
|
if v, exists := priorities[str]; exists {
|
|
score = v
|
|
}
|
|
if score > bestScore {
|
|
bestScore = score
|
|
bestScope = str
|
|
}
|
|
}
|
|
if bestScope != "" {
|
|
effectiveScopes = []string{bestScope}
|
|
}
|
|
}
|
|
if len(effectiveScopes) == 0 {
|
|
continue
|
|
}
|
|
|
|
httpMethod := GetStrFromMap(methodMap, "httpMethod")
|
|
entries = append(entries, CommandEntry{
|
|
Command: resName + " " + methodName,
|
|
Type: "api",
|
|
Scopes: effectiveScopes,
|
|
HTTPMethod: httpMethod,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
sort.Slice(entries, func(i, j int) bool {
|
|
return entries[i].Command < entries[j].Command
|
|
})
|
|
return entries
|
|
}
|
|
|
|
// GetScopesForDomains returns scopes for specific projects (by project name).
|
|
func GetScopesForDomains(projects []string, identity string) []string {
|
|
return CollectScopesForProjects(projects, identity)
|
|
}
|
|
|
|
// GetReadOnlyScopes returns read-only scopes from the recommended (best-per-method) scope set.
|
|
func GetReadOnlyScopes(identity string) []string {
|
|
allProjects := ListFromMetaProjects()
|
|
return FilterScopes(CollectScopesForProjects(allProjects, identity), nil, []string{"read", "readonly"})
|
|
}
|
|
|
|
// ResolveScopesFromFilters resolves scopes from project and permission filters.
|
|
func ResolveScopesFromFilters(projects []string, permissions []string, identity string) []string {
|
|
return FilterScopes(CollectScopesForProjects(projects, identity), nil, permissions)
|
|
}
|
|
|
|
// ComputeMinimumScopeSet computes the minimum set of scopes that covers all
|
|
// from_meta API methods. Equivalent to CollectScopesForProjects with all projects.
|
|
func ComputeMinimumScopeSet(identity string) []string {
|
|
return CollectScopesForProjects(ListFromMetaProjects(), identity)
|
|
}
|