mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat: add lint check for API key scope enum completeness (#19862)
Added a script/linter to ensure all `policy.RBACPermissions` entries are part of the `api_key_scope` enumerated in the `coderd/database/dump.sql` file. Fixes #19846
This commit is contained in:
@@ -561,7 +561,7 @@ endif
|
||||
|
||||
# Note: we don't run zizmor in the lint target because it takes a while. CI
|
||||
# runs it explicitly.
|
||||
lint: lint/shellcheck lint/go lint/ts lint/examples lint/helm lint/site-icons lint/markdown lint/actions/actionlint
|
||||
lint: lint/shellcheck lint/go lint/ts lint/examples lint/helm lint/site-icons lint/markdown lint/actions/actionlint lint/check-scopes
|
||||
.PHONY: lint
|
||||
|
||||
lint/site-icons:
|
||||
@@ -614,6 +614,11 @@ lint/actions/zizmor:
|
||||
.
|
||||
.PHONY: lint/actions/zizmor
|
||||
|
||||
# Verify api_key_scope enum contains all RBAC <resource>:<action> values.
|
||||
lint/check-scopes: coderd/database/dump.sql
|
||||
go run ./scripts/check-scopes
|
||||
.PHONY: lint/check-scopes
|
||||
|
||||
# All files generated by the database should be added here, and this can be used
|
||||
# as a target for jobs that need to run after the database is generated.
|
||||
DB_GEN_FILES := \
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# check-scopes
|
||||
|
||||
Validates that the DB enum `api_key_scope` contains every `<resource>:<action>` derived from `coderd/rbac/policy/RBACPermissions`.
|
||||
|
||||
- Exits 0 when all scopes are present in `coderd/database/dump.sql`.
|
||||
- Exits 1 and prints missing values with suggested `ALTER TYPE` statements otherwise.
|
||||
|
||||
## Usage
|
||||
|
||||
Ensure the schema dump is up-to-date, then run the check:
|
||||
|
||||
```sh
|
||||
make -B gen/db # forces DB dump regeneration
|
||||
make lint/check-scopes
|
||||
```
|
||||
|
||||
Or directly:
|
||||
|
||||
```sh
|
||||
go run ./tools/check-scopes
|
||||
```
|
||||
|
||||
Optional flags:
|
||||
|
||||
- `-dump path` — override path to `dump.sql` (default `coderd/database/dump.sql`).
|
||||
|
||||
## Remediation
|
||||
|
||||
When the tool reports missing values:
|
||||
|
||||
1. Create a DB migration extending the enum, e.g.:
|
||||
|
||||
```sql
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'template:view_insights';
|
||||
```
|
||||
|
||||
2. Regenerate and re-run:
|
||||
|
||||
```sh
|
||||
make -B gen/db && make lint/check-scopes
|
||||
```
|
||||
|
||||
3. Decide whether each new scope is public (exposed in the catalog) or internal-only (handled by the catalog task).
|
||||
@@ -0,0 +1,117 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/rbac/policy"
|
||||
)
|
||||
|
||||
// defaultDumpPath is the repo-relative path to the generated schema dump.
|
||||
const defaultDumpPath = "coderd/database/dump.sql"
|
||||
|
||||
var dumpPathFlag = flag.String("dump", defaultDumpPath, "path to dump.sql (defaults to coderd/database/dump.sql)")
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
want := expectedFromRBAC()
|
||||
have, err := enumValuesFromDump(*dumpPathFlag)
|
||||
if err != nil {
|
||||
_, _ = fmt.Fprintf(os.Stderr, "check-scopes: error reading dump: %v\n", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
// Compute missing: want - have
|
||||
var missing []string
|
||||
for k := range want {
|
||||
if _, ok := have[k]; !ok {
|
||||
missing = append(missing, k)
|
||||
}
|
||||
}
|
||||
sort.Strings(missing)
|
||||
|
||||
if len(missing) == 0 {
|
||||
_, _ = fmt.Println("check-scopes: OK — all RBAC <resource>:<action> values exist in api_key_scope enum")
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = fmt.Fprintln(os.Stderr, "check-scopes: missing enum values:")
|
||||
for _, m := range missing {
|
||||
_, _ = fmt.Fprintf(os.Stderr, " - %s\n", m)
|
||||
}
|
||||
_, _ = fmt.Fprintln(os.Stderr)
|
||||
_, _ = fmt.Fprintln(os.Stderr, "To fix: add a DB migration extending the enum, e.g.:")
|
||||
for _, m := range missing {
|
||||
_, _ = fmt.Fprintf(os.Stderr, " ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS '%s';\n", m)
|
||||
}
|
||||
_, _ = fmt.Fprintln(os.Stderr)
|
||||
_, _ = fmt.Fprintln(os.Stderr, "Also decide if each new scope is public (exposed in the catalog) or internal-only (catalog task).")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// expectedFromRBAC returns the set of <resource>:<action> pairs derived from RBACPermissions.
|
||||
func expectedFromRBAC() map[string]struct{} {
|
||||
want := make(map[string]struct{})
|
||||
for resource, def := range policy.RBACPermissions {
|
||||
if resource == policy.WildcardSymbol {
|
||||
// Ignore wildcard entry; it has no concrete <resource>:<action> pairs.
|
||||
continue
|
||||
}
|
||||
for action := range def.Actions {
|
||||
key := resource + ":" + string(action)
|
||||
want[key] = struct{}{}
|
||||
}
|
||||
}
|
||||
return want
|
||||
}
|
||||
|
||||
// enumValuesFromDump parses dump.sql and extracts all literals from the
|
||||
// `CREATE TYPE api_key_scope AS ENUM (...)` block.
|
||||
func enumValuesFromDump(path string) (map[string]struct{}, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
const enumHead = "CREATE TYPE api_key_scope AS ENUM ("
|
||||
litRe := regexp.MustCompile(`'([^']+)'`)
|
||||
|
||||
values := make(map[string]struct{})
|
||||
inEnum := false
|
||||
s := bufio.NewScanner(f)
|
||||
for s.Scan() {
|
||||
line := strings.TrimSpace(s.Text())
|
||||
if !inEnum {
|
||||
if strings.Contains(line, enumHead) {
|
||||
inEnum = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(line, ");") {
|
||||
// End of enum block
|
||||
return values, nil
|
||||
}
|
||||
// Collect single-quoted literals on this line.
|
||||
for _, m := range litRe.FindAllStringSubmatch(line, -1) {
|
||||
if len(m) > 1 {
|
||||
values[m[1]] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := s.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !inEnum {
|
||||
return nil, xerrors.New("api_key_scope enum block not found in dump")
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
Reference in New Issue
Block a user