From 1bf007875b100c8614629d998bd9c56cf76930f4 Mon Sep 17 00:00:00 2001
From: nameoffnv <553619+nameoffnv@users.noreply.github.com>
Date: Tue, 18 Jul 2023 10:03:41 +0300
Subject: [PATCH] feat: global security (#1620)

* global security

* improve test
---
 operation.go                           |   5 ++
 parser.go                              |  31 ++++++++
 parser_test.go                         |  15 ++++
 testdata/global_security/api/api.go    |  34 ++++++++
 testdata/global_security/expected.json | 105 +++++++++++++++++++++++++
 testdata/global_security/main.go       |  18 +++++
 6 files changed, 208 insertions(+)
 create mode 100644 testdata/global_security/api/api.go
 create mode 100644 testdata/global_security/expected.json
 create mode 100644 testdata/global_security/main.go

diff --git a/operation.go b/operation.go
index 8583f5f97..edf251002 100644
--- a/operation.go
+++ b/operation.go
@@ -721,6 +721,11 @@ func (operation *Operation) ParseRouterComment(commentLine string) error {
 
 // ParseSecurityComment parses comment for given `security` comment string.
 func (operation *Operation) ParseSecurityComment(commentLine string) error {
+	if len(commentLine) == 0 {
+		operation.Security = []map[string][]string{}
+		return nil
+	}
+
 	var (
 		securityMap    = make(map[string][]string)
 		securitySource = commentLine[strings.Index(commentLine, "@Security")+1:]
diff --git a/parser.go b/parser.go
index 8afbb5f48..4db4a96f5 100644
--- a/parser.go
+++ b/parser.go
@@ -569,6 +569,9 @@ func parseGeneralAPIInfo(parser *Parser, comments []string) error {
 
 			parser.swagger.SecurityDefinitions[value] = scheme
 
+		case securityAttr:
+			parser.swagger.Security = append(parser.swagger.Security, parseSecurity(value))
+
 		case "@query.collection.format":
 			parser.collectionFormatInQuery = TransToValidCollectionFormat(value)
 
@@ -768,6 +771,34 @@ func parseSecAttributes(context string, lines []string, index *int) (*spec.Secur
 	return scheme, nil
 }
 
+func parseSecurity(commentLine string) map[string][]string {
+	securityMap := make(map[string][]string)
+
+	for _, securityOption := range strings.Split(commentLine, "||") {
+		securityOption = strings.TrimSpace(securityOption)
+
+		left, right := strings.Index(securityOption, "["), strings.Index(securityOption, "]")
+
+		if !(left == -1 && right == -1) {
+			scopes := securityOption[left+1 : right]
+
+			var options []string
+
+			for _, scope := range strings.Split(scopes, ",") {
+				options = append(options, strings.TrimSpace(scope))
+			}
+
+			securityKey := securityOption[0:left]
+			securityMap[securityKey] = append(securityMap[securityKey], options...)
+		} else {
+			securityKey := strings.TrimSpace(securityOption)
+			securityMap[securityKey] = []string{}
+		}
+	}
+
+	return securityMap
+}
+
 func initIfEmpty(license *spec.License) *spec.License {
 	if license == nil {
 		return new(spec.License)
diff --git a/parser_test.go b/parser_test.go
index 55b287d6f..71f360129 100644
--- a/parser_test.go
+++ b/parser_test.go
@@ -2157,6 +2157,21 @@ func TestParseTypeOverrides(t *testing.T) {
 	assert.Equal(t, string(expected), string(b))
 }
 
+func TestGlobalSecurity(t *testing.T) {
+	t.Parallel()
+
+	searchDir := "testdata/global_security"
+	p := New()
+	err := p.ParseAPI(searchDir, mainAPIFile, defaultParseDepth)
+	assert.NoError(t, err)
+
+	expected, err := os.ReadFile(filepath.Join(searchDir, "expected.json"))
+	assert.NoError(t, err)
+
+	b, _ := json.MarshalIndent(p.swagger, "", "  ")
+	assert.Equal(t, string(expected), string(b))
+}
+
 func TestParseNested(t *testing.T) {
 	t.Parallel()
 
diff --git a/testdata/global_security/api/api.go b/testdata/global_security/api/api.go
new file mode 100644
index 000000000..8b8741c57
--- /dev/null
+++ b/testdata/global_security/api/api.go
@@ -0,0 +1,34 @@
+package api
+
+import (
+	"net/http"
+)
+
+// @Summary default security
+// @Success 200
+// @Router /testapi/application [get]
+func GetApplication(w http.ResponseWriter, r *http.Request) {}
+
+// @Summary no security
+// @Security
+// @Success 200
+// @Router /testapi/nosec [get]
+func GetNoSec(w http.ResponseWriter, r *http.Request) {}
+
+// @Summary basic security
+// @Security BasicAuth
+// @Success 200
+// @Router /testapi/basic [get]
+func GetBasic(w http.ResponseWriter, r *http.Request) {}
+
+// @Summary oauth2 write
+// @Security OAuth2Application[write]
+// @Success 200
+// @Router /testapi/oauth/write [get]
+func GetOAuthWrite(w http.ResponseWriter, r *http.Request) {}
+
+// @Summary oauth2 admin
+// @Security OAuth2Application[admin]
+// @Success 200
+// @Router /testapi/oauth/admin [get]
+func GetOAuthAdmin(w http.ResponseWriter, r *http.Request) {}
diff --git a/testdata/global_security/expected.json b/testdata/global_security/expected.json
new file mode 100644
index 000000000..7505df733
--- /dev/null
+++ b/testdata/global_security/expected.json
@@ -0,0 +1,105 @@
+{
+  "swagger": "2.0",
+  "info": {
+    "title": "Swagger Example API",
+    "contact": {},
+    "version": "1.0"
+  },
+  "paths": {
+    "/testapi/application": {
+      "get": {
+        "summary": "default security",
+        "responses": {
+          "200": {
+            "description": "OK"
+          }
+        }
+      }
+    },
+    "/testapi/basic": {
+      "get": {
+        "security": [
+          {
+            "BasicAuth": []
+          }
+        ],
+        "summary": "basic security",
+        "responses": {
+          "200": {
+            "description": "OK"
+          }
+        }
+      }
+    },
+    "/testapi/nosec": {
+      "get": {
+        "security": [],
+        "summary": "no security",
+        "responses": {
+          "200": {
+            "description": "OK"
+          }
+        }
+      }
+    },
+    "/testapi/oauth/admin": {
+      "get": {
+        "security": [
+          {
+            "OAuth2Application": [
+              "admin"
+            ]
+          }
+        ],
+        "summary": "oauth2 admin",
+        "responses": {
+          "200": {
+            "description": "OK"
+          }
+        }
+      }
+    },
+    "/testapi/oauth/write": {
+      "get": {
+        "security": [
+          {
+            "OAuth2Application": [
+              "write"
+            ]
+          }
+        ],
+        "summary": "oauth2 write",
+        "responses": {
+          "200": {
+            "description": "OK"
+          }
+        }
+      }
+    }
+  },
+  "securityDefinitions": {
+    "APIKeyAuth": {
+      "type": "apiKey",
+      "name": "Authorization",
+      "in": "header"
+    },
+    "BasicAuth": {
+      "type": "basic"
+    },
+    "OAuth2Application": {
+      "type": "oauth2",
+      "flow": "application",
+      "tokenUrl": "https://example.com/oauth/token",
+      "scopes": {
+        "admin": " Grants read and write access to administrative information",
+        "write": " Grants write access"
+      }
+    }
+  },
+  "security": [
+    {
+      "APIKeyAuth": [],
+      "OAuth2Application": []
+    }
+  ]
+}
\ No newline at end of file
diff --git a/testdata/global_security/main.go b/testdata/global_security/main.go
new file mode 100644
index 000000000..83484bac8
--- /dev/null
+++ b/testdata/global_security/main.go
@@ -0,0 +1,18 @@
+package global_security
+
+// @title Swagger Example API
+// @version 1.0
+
+// @securityDefinitions.apikey APIKeyAuth
+// @in header
+// @name Authorization
+
+// @securityDefinitions.basic  BasicAuth
+
+// @securityDefinitions.oauth2.application OAuth2Application
+// @tokenUrl https://example.com/oauth/token
+// @scope.write Grants write access
+// @scope.admin Grants read and write access to administrative information
+
+// @security APIKeyAuth || OAuth2Application
+func main() {}