feat: more role backend and admin/writer roles additions and adjustments
This commit is contained in:
1 parent
8c6c6456da
commit
de142f6c5f
131 files changed
+2663
-435
No files matched your search
@@ -0,0 +1,197 @@
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user