198 lines
5.6 KiB
Go
198 lines
5.6 KiB
Go
package test
|
|
|
|
import (
|
|
"go/ast"
|
|
"go/parser"
|
|
"go/token"
|
|
"io/fs"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
"RenewCMS/internal/domain/role"
|
|
)
|
|
|
|
var legacyRoleSpots = map[string]bool{
|
|
"internal/infrastructure/useCases/GetMyAccess.go": true,
|
|
"internal/infrastructure/useCases/CreateUser.go": true,
|
|
"internal/infrastructure/useCases/AcceptInvite.go": true,
|
|
}
|
|
|
|
var legacyRoleComparison = regexp.MustCompile(`user\.Role(Owner|Admin)|\.Role\s*(==|!=)`)
|
|
|
|
func productionGoFiles(t *testing.T, roots ...string) map[string]string {
|
|
t.Helper()
|
|
files := map[string]string{}
|
|
for _, root := range roots {
|
|
err := filepath.WalkDir(filepath.Join("..", root), func(path string, entry fs.DirEntry, walkErr error) error {
|
|
if walkErr != nil {
|
|
return walkErr
|
|
}
|
|
if entry.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
|
|
return nil
|
|
}
|
|
content, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
files[strings.TrimPrefix(filepath.ToSlash(path), "../")] = string(content)
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("walk %s: %v", root, err)
|
|
}
|
|
}
|
|
return files
|
|
}
|
|
|
|
func TestRolesPermissionsEnforcementHygiene(t *testing.T) {
|
|
t.Run("The owner and admin gates are gone from the code", func(t *testing.T) {
|
|
for name, content := range productionGoFiles(t, "api") {
|
|
if strings.Contains(content, "RequireOwner") {
|
|
t.Errorf("%s still mentions RequireOwner", name)
|
|
}
|
|
if strings.Contains(content, "OwnerRequiredErrorMessage") || strings.Contains(content, "OwnerOrAdminRequiredErrorMessage") {
|
|
t.Errorf("%s still mentions a removed owner message", name)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("No authorization decision reads the legacy user role", func(t *testing.T) {
|
|
for name, content := range productionGoFiles(t, "api", "internal/infrastructure/useCases") {
|
|
if legacyRoleSpots[name] {
|
|
continue
|
|
}
|
|
if match := legacyRoleComparison.FindString(content); match != "" {
|
|
t.Errorf("%s compares the legacy role: %q", name, match)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("The legacy role is read for resolution only in the allowed spots", func(t *testing.T) {
|
|
for name, content := range productionGoFiles(t, "internal/infrastructure/useCases") {
|
|
if strings.Contains(content, "CallerRole") || strings.Contains(content, "isAdminRole") {
|
|
t.Errorf("%s still carries a role-based caller", name)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
var permissionGateNames = map[string]bool{"require": true, "requireAll": true}
|
|
|
|
func roleKeyConstants(t *testing.T) map[string]string {
|
|
t.Helper()
|
|
file, err := parser.ParseFile(token.NewFileSet(), filepath.Join("..", "internal", "domain", "role", "roleKeys.go"), nil, 0)
|
|
if err != nil {
|
|
t.Fatalf("parse roleKeys.go: %v", err)
|
|
}
|
|
constants := map[string]string{}
|
|
ast.Inspect(file, func(node ast.Node) bool {
|
|
spec, ok := node.(*ast.ValueSpec)
|
|
if !ok {
|
|
return true
|
|
}
|
|
for i, name := range spec.Names {
|
|
if i >= len(spec.Values) {
|
|
continue
|
|
}
|
|
if literal, ok := spec.Values[i].(*ast.BasicLit); ok && literal.Kind == token.STRING {
|
|
value, _ := strconv.Unquote(literal.Value)
|
|
constants[name.Name] = value
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
return constants
|
|
}
|
|
|
|
func routerGateKeys(t *testing.T) []string {
|
|
t.Helper()
|
|
file, err := parser.ParseFile(token.NewFileSet(), filepath.Join("..", "api", "router.go"), nil, 0)
|
|
if err != nil {
|
|
t.Fatalf("parse router.go: %v", err)
|
|
}
|
|
constants := roleKeyConstants(t)
|
|
found := map[string]bool{}
|
|
ast.Inspect(file, func(node ast.Node) bool {
|
|
call, ok := node.(*ast.CallExpr)
|
|
if !ok || len(call.Args) != 1 {
|
|
return true
|
|
}
|
|
selector, ok := call.Fun.(*ast.SelectorExpr)
|
|
if !ok || !permissionGateNames[selector.Sel.Name] {
|
|
return true
|
|
}
|
|
switch arg := call.Args[0].(type) {
|
|
case *ast.BasicLit:
|
|
value, _ := strconv.Unquote(arg.Value)
|
|
found[value] = true
|
|
case *ast.SelectorExpr:
|
|
found[constants[arg.Sel.Name]] = true
|
|
default:
|
|
t.Errorf("gate %s called with an argument the test cannot resolve", selector.Sel.Name)
|
|
}
|
|
return true
|
|
})
|
|
keys := make([]string, 0, len(found))
|
|
for key := range found {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
return keys
|
|
}
|
|
|
|
func unknownCatalogueKeys(keys []string) []string {
|
|
var unknown []string
|
|
for _, key := range keys {
|
|
if _, err := role.Lookup(key); err != nil {
|
|
unknown = append(unknown, key)
|
|
}
|
|
}
|
|
return unknown
|
|
}
|
|
|
|
func TestRouterPermissionGateKeysAreInTheCatalogue(t *testing.T) {
|
|
t.Run("Every key passed to a router permission gate is in the catalogue", func(t *testing.T) {
|
|
keys := routerGateKeys(t)
|
|
if len(keys) == 0 {
|
|
t.Fatal("no permission gate keys found in api/router.go")
|
|
}
|
|
if unknown := unknownCatalogueKeys(keys); len(unknown) > 0 {
|
|
t.Errorf("router gates use keys missing from the catalogue: %v", unknown)
|
|
}
|
|
})
|
|
|
|
t.Run("Chained gates contribute both keys", func(t *testing.T) {
|
|
keys := routerGateKeys(t)
|
|
for _, want := range []string{"activity.export", "activity.view"} {
|
|
if sort.SearchStrings(keys, want) == len(keys) || keys[sort.SearchStrings(keys, want)] != want {
|
|
t.Errorf("expected gate key %q to be found", want)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("The checking function catches a misspelled key", func(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
keys []string
|
|
want int
|
|
}{
|
|
{"valid keys", []string{"activity.view", "activity.export"}, 0},
|
|
{"misspelled key", []string{"activity.veiw"}, 1},
|
|
{"one bad among good", []string{"activity.view", "apikeys.revok"}, 1},
|
|
{"empty key", []string{""}, 1},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if got := len(unknownCatalogueKeys(tc.keys)); got != tc.want {
|
|
t.Errorf("unknown = %d, want %d", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|