Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 6 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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 := \
Expand Down
43 changes: 43 additions & 0 deletions scripts/check-scopes/README.md
Original file line number Diff line number Diff line change
@@ -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).
117 changes: 117 additions & 0 deletions scripts/check-scopes/main.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading