generated from coulomb/repo-seed
Add CLI and service skeleton
This commit is contained in:
@@ -1,30 +1,500 @@
|
|||||||
// Command flex-auth is the CLI entry point for the flex-auth authorization
|
// Command flex-auth is the CLI entry point for the flex-auth authorization
|
||||||
// registry and control plane.
|
// registry and control plane.
|
||||||
//
|
|
||||||
// At skeleton stage this binary only reports its version. Subcommands
|
|
||||||
// (validate, load, test, check, batch-check, explain) are added in
|
|
||||||
// FLEX-WP-0002.
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
|
"github.com/netkingdom/flex-auth/internal/audit"
|
||||||
|
decisioncore "github.com/netkingdom/flex-auth/internal/decision"
|
||||||
|
"github.com/netkingdom/flex-auth/internal/policy"
|
||||||
|
"github.com/netkingdom/flex-auth/internal/registry"
|
||||||
|
"github.com/netkingdom/flex-auth/pkg/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
// version is set at build time via -ldflags "-X main.version=…".
|
// version is set at build time via -ldflags "-X main.version=...".
|
||||||
var version = "0.0.0-dev"
|
var version = "0.0.0-dev"
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
showVersion := flag.Bool("version", false, "print version and exit")
|
os.Exit(run(os.Args[1:], os.Stdout, os.Stderr))
|
||||||
flag.Parse()
|
}
|
||||||
|
|
||||||
if *showVersion || (flag.NArg() > 0 && flag.Arg(0) == "version") {
|
func run(args []string, stdout, stderr io.Writer) int {
|
||||||
fmt.Println(version)
|
if len(args) == 0 {
|
||||||
return
|
printUsage(stderr)
|
||||||
|
return 64
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintln(os.Stderr, "flex-auth: no subcommand yet (skeleton stage — see workplans/FLEX-WP-0002).")
|
switch args[0] {
|
||||||
fmt.Fprintln(os.Stderr, "Try: flex-auth --version")
|
case "version", "--version", "-version":
|
||||||
os.Exit(64) // EX_USAGE
|
fmt.Fprintln(stdout, version)
|
||||||
|
return 0
|
||||||
|
case "validate":
|
||||||
|
return runValidate(args[1:], stdout, stderr)
|
||||||
|
case "load-registry":
|
||||||
|
return runLoadRegistry(args[1:], stdout, stderr)
|
||||||
|
case "test-policy":
|
||||||
|
return runTestPolicy(args[1:], stdout, stderr)
|
||||||
|
case "check":
|
||||||
|
return runCheck(args[1:], stdout, stderr)
|
||||||
|
case "batch-check":
|
||||||
|
return runBatchCheck(args[1:], stdout, stderr)
|
||||||
|
case "list-allowed":
|
||||||
|
return runListAllowed(args[1:], stdout, stderr)
|
||||||
|
case "explain":
|
||||||
|
return runExplain(args[1:], stdout, stderr)
|
||||||
|
case "serve":
|
||||||
|
return runServe(args[1:], stdout, stderr)
|
||||||
|
case "help", "-h", "--help":
|
||||||
|
printUsage(stdout)
|
||||||
|
return 0
|
||||||
|
default:
|
||||||
|
fmt.Fprintf(stderr, "flex-auth: unknown subcommand %q\n", args[0])
|
||||||
|
printUsage(stderr)
|
||||||
|
return 64
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runValidate(args []string, stdout, stderr io.Writer) int {
|
||||||
|
fs := newFlagSet("validate", stderr)
|
||||||
|
kind := fs.String("kind", "", "resource-manifest, subject-manifest, protected-system, relationship, access-descriptor, or policy")
|
||||||
|
file := fs.String("file", "", "YAML/JSON file to validate")
|
||||||
|
if err := fs.Parse(args); err != nil {
|
||||||
|
return 64
|
||||||
|
}
|
||||||
|
if *kind == "" || *file == "" {
|
||||||
|
fmt.Fprintln(stderr, "validate requires --kind and --file")
|
||||||
|
return 64
|
||||||
|
}
|
||||||
|
|
||||||
|
switch *kind {
|
||||||
|
case "resource-manifest":
|
||||||
|
var manifest api.ResourceManifest
|
||||||
|
if err := readYAML(*file, &manifest); err != nil {
|
||||||
|
return fail(stderr, err)
|
||||||
|
}
|
||||||
|
if err := registry.NewStore().ImportResourceManifest(manifest); err != nil {
|
||||||
|
return fail(stderr, err)
|
||||||
|
}
|
||||||
|
case "subject-manifest":
|
||||||
|
var manifest api.SubjectManifest
|
||||||
|
if err := readYAML(*file, &manifest); err != nil {
|
||||||
|
return fail(stderr, err)
|
||||||
|
}
|
||||||
|
if err := registry.NewStore().ImportSubjectManifest(manifest); err != nil {
|
||||||
|
return fail(stderr, err)
|
||||||
|
}
|
||||||
|
case "protected-system":
|
||||||
|
var manifest api.ProtectedSystemManifest
|
||||||
|
if err := readYAML(*file, &manifest); err != nil {
|
||||||
|
return fail(stderr, err)
|
||||||
|
}
|
||||||
|
if err := registry.NewStore().PutProtectedSystem(manifest); err != nil {
|
||||||
|
return fail(stderr, err)
|
||||||
|
}
|
||||||
|
case "relationship":
|
||||||
|
var fact api.RelationshipFact
|
||||||
|
if err := readYAML(*file, &fact); err != nil {
|
||||||
|
return fail(stderr, err)
|
||||||
|
}
|
||||||
|
if err := registry.NewStore().PutRelationship(fact); err != nil {
|
||||||
|
return fail(stderr, err)
|
||||||
|
}
|
||||||
|
case "access-descriptor":
|
||||||
|
var descriptor api.CaringAccessDescriptor
|
||||||
|
if err := readYAML(*file, &descriptor); err != nil {
|
||||||
|
return fail(stderr, err)
|
||||||
|
}
|
||||||
|
if err := validateDescriptor(descriptor); err != nil {
|
||||||
|
return fail(stderr, err)
|
||||||
|
}
|
||||||
|
case "policy", "policy-package":
|
||||||
|
pkg, err := policy.LoadAndValidateFile(context.Background(), *file)
|
||||||
|
if err != nil {
|
||||||
|
return fail(stderr, err)
|
||||||
|
}
|
||||||
|
if err := writeJSON(stdout, pkg.Validation); err != nil {
|
||||||
|
return fail(stderr, err)
|
||||||
|
}
|
||||||
|
if !pkg.Valid {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
default:
|
||||||
|
fmt.Fprintf(stderr, "unsupported validate kind %q\n", *kind)
|
||||||
|
return 64
|
||||||
|
}
|
||||||
|
|
||||||
|
return writeStatus(stdout, "valid", map[string]any{"kind": *kind, "file": *file})
|
||||||
|
}
|
||||||
|
|
||||||
|
func runLoadRegistry(args []string, stdout, stderr io.Writer) int {
|
||||||
|
fs := newFlagSet("load-registry", stderr)
|
||||||
|
file := fs.String("file", "", "registry snapshot JSON file")
|
||||||
|
if err := fs.Parse(args); err != nil {
|
||||||
|
return 64
|
||||||
|
}
|
||||||
|
if *file == "" {
|
||||||
|
fmt.Fprintln(stderr, "load-registry requires --file")
|
||||||
|
return 64
|
||||||
|
}
|
||||||
|
|
||||||
|
store, err := registry.LoadFile(*file)
|
||||||
|
if err != nil {
|
||||||
|
return fail(stderr, err)
|
||||||
|
}
|
||||||
|
snapshot := store.Snapshot()
|
||||||
|
return writeStatus(stdout, "loaded", map[string]any{
|
||||||
|
"file": *file,
|
||||||
|
"systems": len(snapshot.Systems),
|
||||||
|
"resource_manifests": len(snapshot.ResourceManifests),
|
||||||
|
"subjects": len(snapshot.Subjects),
|
||||||
|
"groups": len(snapshot.Groups),
|
||||||
|
"teams": len(snapshot.Teams),
|
||||||
|
"tenants": len(snapshot.Tenants),
|
||||||
|
"relationships": len(snapshot.Relationships),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func runTestPolicy(args []string, stdout, stderr io.Writer) int {
|
||||||
|
fs := newFlagSet("test-policy", stderr)
|
||||||
|
file := fs.String("file", "", "policy package Markdown file")
|
||||||
|
if err := fs.Parse(args); err != nil {
|
||||||
|
return 64
|
||||||
|
}
|
||||||
|
if *file == "" {
|
||||||
|
fmt.Fprintln(stderr, "test-policy requires --file")
|
||||||
|
return 64
|
||||||
|
}
|
||||||
|
|
||||||
|
pkg, err := policy.LoadAndValidateFile(context.Background(), *file)
|
||||||
|
if err != nil {
|
||||||
|
return fail(stderr, err)
|
||||||
|
}
|
||||||
|
if err := writeJSON(stdout, pkg.Validation); err != nil {
|
||||||
|
return fail(stderr, err)
|
||||||
|
}
|
||||||
|
if !pkg.Valid {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func runCheck(args []string, stdout, stderr io.Writer) int {
|
||||||
|
fs := newFlagSet("check", stderr)
|
||||||
|
registryPath := fs.String("registry", "", "registry snapshot JSON file")
|
||||||
|
policyPath := fs.String("policy", "", "policy package Markdown file")
|
||||||
|
requestPath := fs.String("request", "", "check request YAML/JSON file")
|
||||||
|
logPath := fs.String("log", "", "optional JSONL decision log path")
|
||||||
|
if err := fs.Parse(args); err != nil {
|
||||||
|
return 64
|
||||||
|
}
|
||||||
|
if *registryPath == "" || *policyPath == "" || *requestPath == "" {
|
||||||
|
fmt.Fprintln(stderr, "check requires --registry, --policy, and --request")
|
||||||
|
return 64
|
||||||
|
}
|
||||||
|
|
||||||
|
engine, err := buildEngine(context.Background(), *registryPath, *policyPath, *logPath)
|
||||||
|
if err != nil {
|
||||||
|
return fail(stderr, err)
|
||||||
|
}
|
||||||
|
var request api.CheckRequest
|
||||||
|
if err := readYAML(*requestPath, &request); err != nil {
|
||||||
|
return fail(stderr, err)
|
||||||
|
}
|
||||||
|
decision, err := engine.Check(context.Background(), request)
|
||||||
|
if err != nil {
|
||||||
|
return fail(stderr, err)
|
||||||
|
}
|
||||||
|
if err := writeJSON(stdout, decision); err != nil {
|
||||||
|
return fail(stderr, err)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func runBatchCheck(args []string, stdout, stderr io.Writer) int {
|
||||||
|
fs := newFlagSet("batch-check", stderr)
|
||||||
|
registryPath := fs.String("registry", "", "registry snapshot JSON file")
|
||||||
|
policyPath := fs.String("policy", "", "policy package Markdown file")
|
||||||
|
requestPath := fs.String("request", "", "batch check request YAML/JSON file")
|
||||||
|
logPath := fs.String("log", "", "optional JSONL decision log path")
|
||||||
|
if err := fs.Parse(args); err != nil {
|
||||||
|
return 64
|
||||||
|
}
|
||||||
|
if *registryPath == "" || *policyPath == "" || *requestPath == "" {
|
||||||
|
fmt.Fprintln(stderr, "batch-check requires --registry, --policy, and --request")
|
||||||
|
return 64
|
||||||
|
}
|
||||||
|
|
||||||
|
engine, err := buildEngine(context.Background(), *registryPath, *policyPath, *logPath)
|
||||||
|
if err != nil {
|
||||||
|
return fail(stderr, err)
|
||||||
|
}
|
||||||
|
var request api.BatchCheckRequest
|
||||||
|
if err := readYAML(*requestPath, &request); err != nil {
|
||||||
|
return fail(stderr, err)
|
||||||
|
}
|
||||||
|
decisions, err := engine.BatchCheck(context.Background(), request)
|
||||||
|
if err != nil {
|
||||||
|
return fail(stderr, err)
|
||||||
|
}
|
||||||
|
if err := writeJSON(stdout, decisions); err != nil {
|
||||||
|
return fail(stderr, err)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func runListAllowed(args []string, stdout, stderr io.Writer) int {
|
||||||
|
fs := newFlagSet("list-allowed", stderr)
|
||||||
|
registryPath := fs.String("registry", "", "registry snapshot JSON file")
|
||||||
|
policyPath := fs.String("policy", "", "policy package Markdown file")
|
||||||
|
subject := fs.String("subject", "", "subject id")
|
||||||
|
action := fs.String("action", "", "action name")
|
||||||
|
system := fs.String("system", "", "protected system id")
|
||||||
|
resourceType := fs.String("resource-type", "", "resource type")
|
||||||
|
logPath := fs.String("log", "", "optional JSONL decision log path")
|
||||||
|
var filters keyValueFlags
|
||||||
|
fs.Var(&filters, "filter", "resource filter as key=value; may be repeated")
|
||||||
|
if err := fs.Parse(args); err != nil {
|
||||||
|
return 64
|
||||||
|
}
|
||||||
|
if *registryPath == "" || *policyPath == "" || *subject == "" || *action == "" {
|
||||||
|
fmt.Fprintln(stderr, "list-allowed requires --registry, --policy, --subject, and --action")
|
||||||
|
return 64
|
||||||
|
}
|
||||||
|
|
||||||
|
engine, err := buildEngine(context.Background(), *registryPath, *policyPath, *logPath)
|
||||||
|
if err != nil {
|
||||||
|
return fail(stderr, err)
|
||||||
|
}
|
||||||
|
decisions, err := engine.ListAllowed(context.Background(), decisioncore.ListAllowedRequest{
|
||||||
|
Subject: api.SubjectRef{ID: *subject},
|
||||||
|
Action: *action,
|
||||||
|
System: *system,
|
||||||
|
ResourceType: *resourceType,
|
||||||
|
Filters: filters.Map(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fail(stderr, err)
|
||||||
|
}
|
||||||
|
if err := writeJSON(stdout, decisions); err != nil {
|
||||||
|
return fail(stderr, err)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func runExplain(args []string, stdout, stderr io.Writer) int {
|
||||||
|
fs := newFlagSet("explain", stderr)
|
||||||
|
logPath := fs.String("decision-log", "", "JSONL decision log path")
|
||||||
|
decisionID := fs.String("decision-id", "", "decision id to explain")
|
||||||
|
if err := fs.Parse(args); err != nil {
|
||||||
|
return 64
|
||||||
|
}
|
||||||
|
if *logPath == "" || *decisionID == "" {
|
||||||
|
fmt.Fprintln(stderr, "explain requires --decision-log and --decision-id")
|
||||||
|
return 64
|
||||||
|
}
|
||||||
|
|
||||||
|
log := audit.NewJSONLDecisionLog(*logPath)
|
||||||
|
envelope, ok, err := log.Find(*decisionID)
|
||||||
|
if err != nil {
|
||||||
|
return fail(stderr, err)
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return fail(stderr, fmt.Errorf("decision %q not found", *decisionID))
|
||||||
|
}
|
||||||
|
if err := writeJSON(stdout, decisioncore.ExplainEnvelope(envelope)); err != nil {
|
||||||
|
return fail(stderr, err)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func runServe(args []string, stdout, stderr io.Writer) int {
|
||||||
|
fs := newFlagSet("serve", stderr)
|
||||||
|
addr := fs.String("addr", "127.0.0.1:8080", "HTTP listen address")
|
||||||
|
registryPath := fs.String("registry", "", "registry snapshot JSON file")
|
||||||
|
policyPath := fs.String("policy", "", "policy package Markdown file")
|
||||||
|
logPath := fs.String("log", "", "optional JSONL decision log path")
|
||||||
|
if err := fs.Parse(args); err != nil {
|
||||||
|
return 64
|
||||||
|
}
|
||||||
|
if *registryPath == "" || *policyPath == "" {
|
||||||
|
fmt.Fprintln(stderr, "serve requires --registry and --policy")
|
||||||
|
return 64
|
||||||
|
}
|
||||||
|
|
||||||
|
engine, err := buildEngine(context.Background(), *registryPath, *policyPath, *logPath)
|
||||||
|
if err != nil {
|
||||||
|
return fail(stderr, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.Header().Set("content-type", "application/json")
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
|
||||||
|
})
|
||||||
|
mux.HandleFunc("/v1/check", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var request api.CheckRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
decision, err := engine.Check(r.Context(), request)
|
||||||
|
writeHTTP(w, decision, err)
|
||||||
|
})
|
||||||
|
mux.HandleFunc("/v1/batch_check", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var request api.BatchCheckRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
decisions, err := engine.BatchCheck(r.Context(), request)
|
||||||
|
writeHTTP(w, decisions, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
fmt.Fprintf(stderr, "flex-auth serving on http://%s\n", *addr)
|
||||||
|
if err := http.ListenAndServe(*addr, mux); err != nil {
|
||||||
|
return fail(stderr, err)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildEngine(ctx context.Context, registryPath, policyPath, logPath string) (*decisioncore.Engine, error) {
|
||||||
|
store, err := registry.LoadFile(registryPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pkg, err := policy.LoadAndValidateFile(ctx, policyPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
engine, err := decisioncore.NewEngine(store, pkg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if logPath != "" {
|
||||||
|
engine.SetDecisionLog(audit.NewJSONLDecisionLog(logPath))
|
||||||
|
}
|
||||||
|
return engine, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateDescriptor(descriptor api.CaringAccessDescriptor) error {
|
||||||
|
if descriptor.Profile != api.CaringProfileCaring040RC2 {
|
||||||
|
return fmt.Errorf("unsupported CARING profile %q", descriptor.Profile)
|
||||||
|
}
|
||||||
|
if descriptor.SubjectType == "" {
|
||||||
|
return fmt.Errorf("subject_type is required")
|
||||||
|
}
|
||||||
|
if descriptor.OrganizationRelation == "" {
|
||||||
|
return fmt.Errorf("organization_relation is required")
|
||||||
|
}
|
||||||
|
if descriptor.CanonicalRole == "" {
|
||||||
|
return fmt.Errorf("canonical_role is required")
|
||||||
|
}
|
||||||
|
if descriptor.Scope.Level == "" || descriptor.Scope.ID == "" {
|
||||||
|
return fmt.Errorf("scope.level and scope.id are required")
|
||||||
|
}
|
||||||
|
if len(descriptor.Planes) == 0 {
|
||||||
|
return fmt.Errorf("at least one plane is required")
|
||||||
|
}
|
||||||
|
if len(descriptor.Capabilities) == 0 {
|
||||||
|
return fmt.Errorf("at least one capability is required")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readYAML(path string, out any) error {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := yaml.Unmarshal(data, out); err != nil {
|
||||||
|
return fmt.Errorf("unmarshal %s: %w", path, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeJSON(w io.Writer, value any) error {
|
||||||
|
encoder := json.NewEncoder(w)
|
||||||
|
encoder.SetIndent("", " ")
|
||||||
|
if err := encoder.Encode(value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeStatus(w io.Writer, status string, extra map[string]any) int {
|
||||||
|
out := map[string]any{"status": status}
|
||||||
|
for key, value := range extra {
|
||||||
|
out[key] = value
|
||||||
|
}
|
||||||
|
if err := writeJSON(w, out); err != nil {
|
||||||
|
return fail(w, err)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeHTTP(w http.ResponseWriter, value any, err error) {
|
||||||
|
w.Header().Set("content-type", "application/json")
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = json.NewEncoder(w).Encode(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFlagSet(name string, stderr io.Writer) *flag.FlagSet {
|
||||||
|
fs := flag.NewFlagSet(name, flag.ContinueOnError)
|
||||||
|
fs.SetOutput(stderr)
|
||||||
|
return fs
|
||||||
|
}
|
||||||
|
|
||||||
|
func fail(stderr io.Writer, err error) int {
|
||||||
|
fmt.Fprintln(stderr, "flex-auth:", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func printUsage(w io.Writer) {
|
||||||
|
fmt.Fprintln(w, "usage: flex-auth <command> [options]")
|
||||||
|
fmt.Fprintln(w, "commands: version, validate, load-registry, test-policy, check, batch-check, list-allowed, explain, serve")
|
||||||
|
}
|
||||||
|
|
||||||
|
type keyValueFlags []string
|
||||||
|
|
||||||
|
func (f *keyValueFlags) String() string {
|
||||||
|
return strings.Join(*f, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *keyValueFlags) Set(value string) error {
|
||||||
|
if !strings.Contains(value, "=") {
|
||||||
|
return fmt.Errorf("filter must be key=value")
|
||||||
|
}
|
||||||
|
*f = append(*f, value)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f keyValueFlags) Map() map[string]any {
|
||||||
|
out := make(map[string]any, len(f))
|
||||||
|
for _, item := range f {
|
||||||
|
key, value, _ := strings.Cut(item, "=")
|
||||||
|
out[key] = value
|
||||||
|
}
|
||||||
|
return out
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,90 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
func TestVersionDefault(t *testing.T) {
|
"github.com/netkingdom/flex-auth/pkg/api"
|
||||||
if version == "" {
|
)
|
||||||
t.Fatal("version must not be empty")
|
|
||||||
|
func TestRunVersion(t *testing.T) {
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
code := run([]string{"version"}, &stdout, &stderr)
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("code = %d, stderr = %s", code, stderr.String())
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(stdout.String()) == "" {
|
||||||
|
t.Fatal("version output is empty")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRunTestPolicy(t *testing.T) {
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
code := run([]string{"test-policy", "--file", examplePath("policy_package.md")}, &stdout, &stderr)
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("code = %d, stderr = %s", code, stderr.String())
|
||||||
|
}
|
||||||
|
if !strings.Contains(stdout.String(), `"valid": true`) {
|
||||||
|
t.Fatalf("stdout = %s; want valid policy result", stdout.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunCheck(t *testing.T) {
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
code := run([]string{
|
||||||
|
"check",
|
||||||
|
"--registry", examplePath("registry_snapshot.json"),
|
||||||
|
"--policy", examplePath("policy_package.md"),
|
||||||
|
"--request", examplePath("check_request.yaml"),
|
||||||
|
}, &stdout, &stderr)
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("code = %d, stderr = %s", code, stderr.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
var decision api.DecisionEnvelope
|
||||||
|
if err := json.Unmarshal(stdout.Bytes(), &decision); err != nil {
|
||||||
|
t.Fatalf("unmarshal decision: %v\n%s", err, stdout.String())
|
||||||
|
}
|
||||||
|
if decision.Effect != api.DecisionEffectAllow {
|
||||||
|
t.Fatalf("decision.Effect = %q; want allow", decision.Effect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunBatchCheck(t *testing.T) {
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
code := run([]string{
|
||||||
|
"batch-check",
|
||||||
|
"--registry", examplePath("registry_snapshot.json"),
|
||||||
|
"--policy", examplePath("policy_package.md"),
|
||||||
|
"--request", examplePath("batch_check_request.yaml"),
|
||||||
|
}, &stdout, &stderr)
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("code = %d, stderr = %s", code, stderr.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
var decisions []api.DecisionEnvelope
|
||||||
|
if err := json.Unmarshal(stdout.Bytes(), &decisions); err != nil {
|
||||||
|
t.Fatalf("unmarshal decisions: %v\n%s", err, stdout.String())
|
||||||
|
}
|
||||||
|
if len(decisions) != 2 || decisions[0].Effect != api.DecisionEffectAllow || decisions[1].Effect != api.DecisionEffectDeny {
|
||||||
|
t.Fatalf("decisions = %+v; want allow then deny", decisions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunValidateAccessDescriptor(t *testing.T) {
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
code := run([]string{"validate", "--kind", "access-descriptor", "--file", examplePath("access_descriptor.yaml")}, &stdout, &stderr)
|
||||||
|
if code != 0 {
|
||||||
|
t.Fatalf("code = %d, stderr = %s", code, stderr.String())
|
||||||
|
}
|
||||||
|
if !strings.Contains(stdout.String(), `"status": "valid"`) {
|
||||||
|
t.Fatalf("stdout = %s; want valid status", stdout.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func examplePath(name string) string {
|
||||||
|
return filepath.Join("..", "..", "examples", "caring", name)
|
||||||
|
}
|
||||||
|
|||||||
12
examples/caring/batch_check_request.yaml
Normal file
12
examples/caring/batch_check_request.yaml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
id: batch:tenant-alpha-documents
|
||||||
|
subject:
|
||||||
|
id: user:alice
|
||||||
|
type: Human
|
||||||
|
tenant: tenant:alpha
|
||||||
|
action: read
|
||||||
|
resources:
|
||||||
|
- id: document:internal-note
|
||||||
|
system: markitect-tool
|
||||||
|
- id: document:missing
|
||||||
|
type: document
|
||||||
|
system: markitect-tool
|
||||||
@@ -154,6 +154,12 @@ func (e *Engine) Explain(decisionID string) (Explanation, error) {
|
|||||||
return Explanation{}, fmt.Errorf("decision %q not found", decisionID)
|
return Explanation{}, fmt.Errorf("decision %q not found", decisionID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return ExplainEnvelope(decision), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExplainEnvelope returns the same explanation shape for an already-loaded
|
||||||
|
// decision envelope.
|
||||||
|
func ExplainEnvelope(decision api.DecisionEnvelope) Explanation {
|
||||||
return Explanation{
|
return Explanation{
|
||||||
DecisionID: decision.ID,
|
DecisionID: decision.ID,
|
||||||
Effect: decision.Effect,
|
Effect: decision.Effect,
|
||||||
@@ -166,7 +172,7 @@ func (e *Engine) Explain(decisionID string) (Explanation, error) {
|
|||||||
MatchedRule: decision.MatchedRule,
|
MatchedRule: decision.MatchedRule,
|
||||||
Diagnostics: decision.Diagnostics,
|
Diagnostics: decision.Diagnostics,
|
||||||
Caring: decision.Caring,
|
Caring: decision.Caring,
|
||||||
}, nil
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type registryFacts struct {
|
type registryFacts struct {
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ induced access, and privilege-escalation findings.
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: FLEX-WP-0002-T007
|
id: FLEX-WP-0002-T007
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "ee9ae6dd-c31f-4d4e-b238-533a2b8040d4"
|
state_hub_task_id: "ee9ae6dd-c31f-4d4e-b238-533a2b8040d4"
|
||||||
```
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user