github.com/w3security/vervet/v5@v5.3.1-0.20230618081846-5bd9b5d799dc/document.go (about) 1 package vervet 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "log" 7 "net/url" 8 "os" 9 "path/filepath" 10 "strings" 11 12 "github.com/getkin/kin-openapi/openapi3" 13 "github.com/ghodss/yaml" 14 "github.com/google/uuid" 15 ) 16 17 func init() { 18 // Necessary for `format: uuid` to validate. 19 openapi3.DefineStringFormatCallback("uuid", func(v string) error { 20 _, err := uuid.Parse(v) 21 return err 22 }) 23 openapi3.DefineStringFormatCallback("url", func(v string) error { 24 _, err := url.Parse(v) 25 return err 26 }) 27 } 28 29 // Document is an OpenAPI 3 document object model. 30 type Document struct { 31 *openapi3.T 32 path string 33 url *url.URL 34 } 35 36 // NewDocumentFile loads an OpenAPI spec file from the given file path, 37 // returning a document object. 38 func NewDocumentFile(specFile string) (_ *Document, returnErr error) { 39 // Restore current working directory upon returning 40 cwd, err := os.Getwd() 41 if err != nil { 42 return nil, err 43 } 44 defer func() { 45 err := os.Chdir(cwd) 46 if err != nil { 47 log.Println("warning: failed to restore working directory: %w", err) 48 if returnErr == nil { 49 returnErr = err 50 } 51 } 52 }() 53 54 specFile, err = filepath.Abs(specFile) 55 if err != nil { 56 return nil, fmt.Errorf("failed to get absolute path: %w", err) 57 } 58 59 // `cd` to the path containing the spec file, so that relative paths 60 // resolve. 61 specBase, specDir := filepath.Base(specFile), filepath.Dir(specFile) 62 err = os.Chdir(specDir) 63 if err != nil { 64 return nil, fmt.Errorf("failed to chdir %q: %w", specDir, err) 65 } 66 67 specURL, err := url.Parse(specFile) 68 if err != nil { 69 return nil, err 70 } 71 72 var t openapi3.T 73 contents, err := os.ReadFile(specFile) 74 if err != nil { 75 return nil, err 76 } 77 err = yaml.Unmarshal(contents, &t) 78 if err != nil { 79 return nil, err 80 } 81 err = newRefAliasResolver(&t).resolve() 82 if err != nil { 83 return nil, err 84 } 85 86 l := openapi3.NewLoader() 87 l.IsExternalRefsAllowed = true 88 err = l.ResolveRefsIn(&t, specURL) 89 if err != nil { 90 return nil, fmt.Errorf("failed to load %q: %w", specBase, err) 91 } 92 93 if t.Components == nil { 94 t.Components = &openapi3.Components{} 95 } 96 97 return &Document{ 98 T: &t, 99 path: specFile, 100 url: specURL, 101 }, nil 102 } 103 104 // NewResolvedDocument returns a Document that has already been loaded and 105 // references resolved from the given URL. The URL is provided to indicate the 106 // document's origin in logging and error messages. 107 func NewResolvedDocument(t *openapi3.T, url *url.URL) *Document { 108 return &Document{ 109 T: t, 110 path: url.String(), 111 url: url, 112 } 113 } 114 115 // MarshalJSON implements json.Marshaler. 116 func (d *Document) MarshalJSON() ([]byte, error) { 117 return d.T.MarshalJSON() 118 } 119 120 // RelativePath returns the relative path for resolving references from the 121 // file path location of the top-level document: the directory which contains 122 // the file from which the top-level document was loaded. 123 func (d *Document) RelativePath() string { 124 return filepath.Dir(d.path) 125 } 126 127 // Location returns the URL from where the document was loaded. 128 func (d *Document) Location() *url.URL { 129 return d.url 130 } 131 132 // ResolveRefs resolves all Ref types in the document, causing the Value field 133 // of each Ref to be loaded and populated from its referenced location. 134 func (d *Document) ResolveRefs() error { 135 l := openapi3.NewLoader() 136 l.IsExternalRefsAllowed = true 137 return l.ResolveRefsIn(d.T, d.url) 138 } 139 140 // LoadReference loads a reference from refPath, relative to relPath, into 141 // target. The relative path of the reference is returned, so that references 142 // may be chain-loaded with successive calls. 143 func (d *Document) LoadReference(relPath, refPath string, target interface{}) (_ string, returnErr error) { 144 refUrl, err := url.Parse(refPath) 145 if err != nil { 146 return "", err 147 } 148 if refUrl.Scheme == "" || refUrl.Scheme == "file" { 149 refPath, err = filepath.Abs(filepath.Join(relPath, refUrl.Path)) 150 if err != nil { 151 return "", err 152 } 153 refUrl.Path = refPath 154 } 155 156 // Parse and load the contents of the referenced document. 157 l := openapi3.NewLoader() 158 l.IsExternalRefsAllowed = true 159 contents, err := openapi3.DefaultReadFromURI(l, refUrl) 160 if err != nil { 161 return "", fmt.Errorf("failed to read %q: %w", refUrl, err) 162 } 163 // If the reference is to an element in the referenced document, further resolve that. 164 if refUrl.Fragment != "" { 165 parts := strings.Split(refUrl.Fragment, "/") 166 // TODO: support actual jsonpaths if/when needed. For now only 167 // top-level properties are supported. 168 if parts[0] != "" || len(parts) > 2 { 169 return "", fmt.Errorf("URL %q not supported", refUrl) 170 } 171 elements := map[string]interface{}{} 172 err := yaml.Unmarshal(contents, &elements) 173 if err != nil { 174 return "", err 175 } 176 elementDoc, ok := elements[parts[1]] 177 if !ok { 178 return "", fmt.Errorf("element %q not found in %q", parts[1], refUrl.Path) 179 } 180 contents, err = json.Marshal(elementDoc) 181 if err != nil { 182 return "", err 183 } 184 } 185 186 // Unmarshal the resolved reference into target object. 187 err = yaml.Unmarshal(contents, target) 188 if err != nil { 189 return "", err 190 } 191 192 return filepath.Abs(filepath.Dir(refUrl.Path)) 193 } 194 195 // Version returns the version of the document. 196 func (d *Document) Version() (Version, error) { 197 vs, err := ExtensionString(d.Extensions, ExtW3SecurityApiVersion) 198 if err != nil { 199 return Version{}, err 200 } 201 return ParseVersion(vs) 202 } 203 204 // Lifecycle returns the lifecycle of the document. 205 func (d *Document) Lifecycle() (Lifecycle, error) { 206 ls, err := ExtensionString(d.Extensions, ExtW3SecurityApiLifecycle) 207 if err != nil { 208 if IsExtensionNotFound(err) { 209 // If it's not marked as deprecated or sunset, assume it's released. 210 return LifecycleReleased, err 211 } 212 return lifecycleUndefined, err 213 } 214 return ParseLifecycle(ls) 215 }