|
| 1 | +package main |
| 2 | + |
| 3 | +import ( |
| 4 | + "encoding/json" |
| 5 | + "flag" |
| 6 | + "fmt" |
| 7 | + "net/http" |
| 8 | + "os" |
| 9 | + "strings" |
| 10 | + |
| 11 | + v2 "windshift/internal/restapi/v2" |
| 12 | +) |
| 13 | + |
| 14 | +type document struct { |
| 15 | + OpenAPI string `json:"openapi"` |
| 16 | + Paths map[string]map[string]operation `json:"paths"` |
| 17 | +} |
| 18 | + |
| 19 | +type operation struct { |
| 20 | + Security []map[string][]string `json:"security"` |
| 21 | +} |
| 22 | + |
| 23 | +func main() { |
| 24 | + specPath := flag.String("spec", "api/openapi-v2.json", "v2 OpenAPI JSON path") |
| 25 | + flag.Parse() |
| 26 | + |
| 27 | + data, err := os.ReadFile(*specPath) |
| 28 | + if err != nil { |
| 29 | + fail("read spec: %v", err) |
| 30 | + } |
| 31 | + var spec document |
| 32 | + if err := json.Unmarshal(data, &spec); err != nil { |
| 33 | + fail("decode spec: %v", err) |
| 34 | + } |
| 35 | + if !strings.HasPrefix(spec.OpenAPI, "3.") { |
| 36 | + fail("openapi version %q is not 3.x", spec.OpenAPI) |
| 37 | + } |
| 38 | + |
| 39 | + want := make(map[string]v2.Route) |
| 40 | + for _, route := range v2.Inventory() { |
| 41 | + key := strings.ToLower(route.Method) + " " + route.Path |
| 42 | + want[key] = route |
| 43 | + item, ok := spec.Paths[route.Path] |
| 44 | + if !ok { |
| 45 | + fail("route %s is missing", key) |
| 46 | + } |
| 47 | + operation, ok := item[strings.ToLower(route.Method)] |
| 48 | + if !ok { |
| 49 | + fail("route %s is missing", key) |
| 50 | + } |
| 51 | + validateSecurity(route, operation) |
| 52 | + } |
| 53 | + |
| 54 | + for path, item := range spec.Paths { |
| 55 | + for method := range item { |
| 56 | + if !isHTTPMethod(method) { |
| 57 | + continue |
| 58 | + } |
| 59 | + key := method + " " + path |
| 60 | + if _, ok := want[key]; !ok { |
| 61 | + fail("OpenAPI operation %s is not in the v2 inventory", key) |
| 62 | + } |
| 63 | + } |
| 64 | + } |
| 65 | + fmt.Printf("API v2 OpenAPI parity is valid (%d operations).\n", len(want)) |
| 66 | +} |
| 67 | + |
| 68 | +func validateSecurity(route v2.Route, operation operation) { |
| 69 | + if route.Auth == v2.AuthPublic { |
| 70 | + if len(operation.Security) != 0 { |
| 71 | + fail("public route %s %s declares security", route.Method, route.Path) |
| 72 | + } |
| 73 | + return |
| 74 | + } |
| 75 | + if len(operation.Security) != 1 { |
| 76 | + fail("authenticated route %s %s must declare one security requirement", route.Method, route.Path) |
| 77 | + } |
| 78 | + scopes, ok := operation.Security[0]["BearerAuth"] |
| 79 | + if !ok || strings.Join(scopes, "\x00") != strings.Join(route.Scopes, "\x00") { |
| 80 | + fail("route %s %s scopes do not match its inventory", route.Method, route.Path) |
| 81 | + } |
| 82 | +} |
| 83 | + |
| 84 | +func isHTTPMethod(method string) bool { |
| 85 | + switch strings.ToUpper(method) { |
| 86 | + case http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete, http.MethodHead, http.MethodOptions: |
| 87 | + return true |
| 88 | + default: |
| 89 | + return false |
| 90 | + } |
| 91 | +} |
| 92 | + |
| 93 | +func fail(format string, args ...any) { |
| 94 | + fmt.Fprintf(os.Stderr, "openapi-v2-check: "+format+"\n", args...) |
| 95 | + os.Exit(1) |
| 96 | +} |
0 commit comments