Repository: getkin/kin-openapi
Pull request #905 (2024-02-08 23:25:19)

openapi3filter: add `Unwrap()` method to SecurityRequirementsError
Allows users to perform checks like `errors.Is` which may be returned from the given AuthenticationFunc.

fenollp: > Allows users to perform checks like errors.Is which may be returned from the given AuthenticationFunc.

Please add a test showing this behavior (an Example would be best)
nickajacks1: I've added an Example. Please let me know if it wasn't what you had in mind!

Patch:
diff --git a/.github/docs/openapi3filter.txt b/.github/docs/openapi3filter.txt
index f0ba156..b05679e 100644
--- a/.github/docs/openapi3filter.txt
+++ b/.github/docs/openapi3filter.txt
@@ -331,6 +331,8 @@ type SecurityRequirementsError struct {
 
 func (err *SecurityRequirementsError) Error() string
 
+func (err SecurityRequirementsError) Unwrap() []error
+
 type StatusCoder interface {
 	StatusCode() int
 }
diff --git a/openapi3filter/errors.go b/openapi3filter/errors.go
index e78e982..ea7c7c3 100644
--- a/openapi3filter/errors.go
+++ b/openapi3filter/errors.go
@@ -89,3 +89,9 @@ func (err *SecurityRequirementsError) Error() string {
 
 	return buff.String()
 }
+
+var _ interface{ Unwrap() []error } = SecurityRequirementsError{}
+
+func (err SecurityRequirementsError) Unwrap() []error {
+	return err.Errors
+}


Test patch:
diff --git a/openapi3filter/validate_request_example_test.go b/openapi3filter/validate_request_example_test.go
new file mode 100644
index 0000000..653db30
--- /dev/null
+++ b/openapi3filter/validate_request_example_test.go
@@ -0,0 +1,117 @@
+package openapi3filter
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"log"
+	"net/http"
+
+	"github.com/getkin/kin-openapi/openapi3"
+	"github.com/getkin/kin-openapi/routers/gorillamux"
+)
+
+func ExampleAuthenticationFunc() {
+	const spec = `
+openapi: 3.0.0
+info:
+  title: 'Validator'
+  version: 0.0.1
+components:
+  securitySchemes:
+    OAuth2:
+      type: oauth2
+      flows:
+        clientCredentials:
+          tokenUrl: /oauth2/token
+          scopes:
+            secrets.read: Ability to read secrets
+            secrets.write: Ability to write secrets
+paths:
+  /secret:
+    post:
+      security:
+        - OAuth2:
+          - secrets.write
+      responses:
+        '200':
+          description: Ok
+        '401':
+          description: Unauthorized
+`
+	var (
+		errUnauthenticated = errors.New("login required")
+		errForbidden       = errors.New("permission denied")
+	)
+
+	userScopes := map[string][]string{
+		"Alice": {"secrets.read"},
+		"Bob":   {"secrets.read", "secrets.write"},
+	}
+
+	authenticationFunc := func(_ context.Context, ai *AuthenticationInput) error {
+		user := ai.RequestValidationInput.Request.Header.Get("X-User")
+		if user == "" {
+			return errUnauthenticated
+		}
+
+		for _, requiredScope := range ai.Scopes {
+			var allowed bool
+			for _, scope := range userScopes[user] {
+				if scope == requiredScope {
+					allowed = true
+					break
+				}
+			}
+			if !allowed {
+				return errForbidden
+			}
+		}
+
+		return nil
+	}
+
+	loader := openapi3.NewLoader()
+	doc, _ := loader.LoadFromData([]byte(spec))
+	router, _ := gorillamux.NewRouter(doc)
+
+	validateRequest := func(req *http.Request) {
+		route, pathParams, _ := router.FindRoute(req)
+
+		validationInput := &RequestValidationInput{
+			Request:    req,
+			PathParams: pathParams,
+			Route:      route,
+			Options: &Options{
+				AuthenticationFunc: authenticationFunc,
+			},
+		}
+		err := ValidateRequest(context.TODO(), validationInput)
+		switch {
+		case errors.Is(err, errUnauthenticated):
+			fmt.Println("username is required")
+		case errors.Is(err, errForbidden):
+			fmt.Println("user is not allowed to perform this action")
+		case err == nil:
+			fmt.Println("ok")
+		default:
+			log.Fatal(err)
+		}
+	}
+
+	req1, _ := http.NewRequest(http.MethodPost, "/secret", nil)
+	req1.Header.Set("X-User", "Alice")
+
+	req2, _ := http.NewRequest(http.MethodPost, "/secret", nil)
+	req2.Header.Set("X-User", "Bob")
+
+	req3, _ := http.NewRequest(http.MethodPost, "/secret", nil)
+
+	validateRequest(req1)
+	validateRequest(req2)
+	validateRequest(req3)
+	// output:
+	// user is not allowed to perform this action
+	// ok
+	// username is required
+}