Skip to content

Commit

Permalink
feat(openapi): add OpenAPI spec generation (#7)
Browse files Browse the repository at this point in the history
* Add openapi package

* Update usage string

* Add OpenAPI spec function for blocking endpoint

* Add OpenAPI subcommand

* Add OpenAPI spec for check endpoint

* Update check HTTP method OpenAPI spec

* Add security scheme

* Add 401 responses to OpenAPI specs

* Add error handling to fix SAST
  • Loading branch information
SafeEval authored Jul 24, 2024
1 parent e6e9711 commit cc6ac0c
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 14 deletions.
42 changes: 42 additions & 0 deletions cmd/jwt-block/openapi.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package cmd

import (
"fmt"

"github.com/spf13/cobra"
"github.com/spf13/viper"

"github.com/divergentcodes/jwt-block/internal/core"
"github.com/divergentcodes/jwt-block/web"
)

var openapiCmd = &cobra.Command{
Use: "openapi",
Short: "Generate OpenAPI specs for jwt-block",
Long: "Generate OpenAPI specs for jwt-block",
Run: openapi,
}

func init() {
logger := core.GetLogger()

openapiCmd.PersistentFlags().String("format", "yaml", "format for OpenAPI specs (yaml or json)")
err := viper.BindPFlag("format", openapiCmd.PersistentFlags().Lookup("format"))
if err != nil {
logger.Fatalw(err.Error())
}

rootCmd.AddCommand(openapiCmd)
}

func openapi(cmd *cobra.Command, args []string) {
logger := core.GetLogger()

format := viper.GetString("format")
schema, err := web.GenerateOpenAPI(format)
if err != nil {
logger.Fatalw(err.Error())
}

fmt.Print(schema)
}
2 changes: 1 addition & 1 deletion cmd/jwt-block/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ func initRootFlags() {

// json
defaultOutJSON := viper.GetBool(core.OptStr_OutJSON)
rootCmd.PersistentFlags().Bool("json", defaultOutJSON, "Use JSON output")
rootCmd.PersistentFlags().Bool("json", defaultOutJSON, "Use JSON log output")
err = viper.BindPFlag(core.OptStr_OutJSON, rootCmd.PersistentFlags().Lookup("json"))
if err != nil {
panic(err)
Expand Down
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require (
github.com/redis/go-redis/v9 v9.6.0
github.com/spf13/cobra v1.8.1
github.com/spf13/viper v1.19.0
github.com/swaggest/openapi-go v0.2.53
go.uber.org/zap v1.27.0
)

Expand Down Expand Up @@ -37,12 +38,15 @@ require (
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/swaggest/jsonschema-go v0.3.72 // indirect
github.com/swaggest/refl v1.3.0 // indirect
github.com/yuin/gopher-lua v1.1.1 // indirect
go.uber.org/multierr v1.10.0 // indirect
golang.org/x/crypto v0.24.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/sys v0.21.0 // indirect
golang.org/x/text v0.16.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
15 changes: 15 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZp
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
github.com/alicebob/miniredis/v2 v2.33.0 h1:uvTF0EDeu9RLnUEG27Db5I68ESoIxTiXbNUiji6lZrA=
github.com/alicebob/miniredis/v2 v2.33.0/go.mod h1:MhP4a3EU7aENRi9aO+tHfTBZicLqQevyi/DJpoj6mi0=
github.com/bool64/dev v0.2.35 h1:M17TLsO/pV2J7PYI/gpe3Ua26ETkzZGb+dC06eoMqlk=
github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
Expand All @@ -24,6 +26,7 @@ github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PU
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
Expand Down Expand Up @@ -61,6 +64,7 @@ github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6g
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
Expand All @@ -85,6 +89,15 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/swaggest/assertjson v1.9.0 h1:dKu0BfJkIxv/xe//mkCrK5yZbs79jL7OVf9Ija7o2xQ=
github.com/swaggest/jsonschema-go v0.3.72 h1:IHaGlR1bdBUBPfhe4tfacN2TGAPKENEGiNyNzvnVHv4=
github.com/swaggest/jsonschema-go v0.3.72/go.mod h1:OrGyEoVqpfSFJ4Am4V/FQcQ3mlEC1vVeleA+5ggbVW4=
github.com/swaggest/openapi-go v0.2.53 h1:lWHKgC9IN48nBYxvuBrmAVJgki/1xsrGZWaWJnOLenE=
github.com/swaggest/openapi-go v0.2.53/go.mod h1:2Q7NpuG9NgpGeTaNOo852GSR6cCzSP4IznA9DNdUTQw=
github.com/swaggest/refl v1.3.0 h1:PEUWIku+ZznYfsoyheF97ypSduvMApYyGkYF3nabS0I=
github.com/swaggest/refl v1.3.0/go.mod h1:3Ujvbmh1pfSbDYjC6JGG7nMgPvpG0ehQL4iNonnLNbg=
github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
Expand All @@ -106,6 +119,8 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogR
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
33 changes: 20 additions & 13 deletions web/block.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
package web

import (
"encoding/json"
"net/http"
"strings"

"github.com/divergentcodes/jwt-block/internal/blocklist"
"github.com/divergentcodes/jwt-block/internal/cache"
"github.com/divergentcodes/jwt-block/internal/core"
"github.com/swaggest/openapi-go"
"github.com/swaggest/openapi-go/openapi3"
)

// Handler for /blocklist/block
Expand Down Expand Up @@ -64,19 +65,25 @@ func jwtBlock(w http.ResponseWriter, r *http.Request) {
}

// Response.
allowed, allowedOrigin := isCorsRequestAllowed(r)
if allowed {
addCorsResponseHeaders(w, allowedOrigin)
WriteSuccessResponse(r, w, result.Message, 200)
}

// OpenAPI documentation generation.
func blockGenerateOpenAPI(reflector *openapi3.Reflector) {
logger := core.GetLogger()

blockOp, err := reflector.NewOperationContext(http.MethodPost, "/blocklist/block")
if err != nil {
logger.Fatalw(err.Error())
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
err = json.NewEncoder(w).Encode(result)

statusCodes := []int{http.StatusOK, http.StatusUnauthorized}
for _, status := range statusCodes {
blockOp.AddRespStructure(new(blocklist.BlockResult), func(cu *openapi.ContentUnit) { cu.HTTPStatus = status })
}

err = reflector.AddOperation(blockOp)
if err != nil {
logger.Errorw(
"failed to JSON encode response data",
"func", "web.jwtBlock",
"data", result,
"error", err,
)
logger.Fatalw(err.Error())
}
}
22 changes: 22 additions & 0 deletions web/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"strings"

"github.com/spf13/viper"
"github.com/swaggest/openapi-go"
"github.com/swaggest/openapi-go/openapi3"

"github.com/divergentcodes/jwt-block/internal/blocklist"
"github.com/divergentcodes/jwt-block/internal/cache"
Expand Down Expand Up @@ -123,3 +125,23 @@ func jwtCheck(w http.ResponseWriter, r *http.Request) {
)
}
}

// OpenAPI documentation generation.
func checkGenerateOpenAPI(reflector *openapi3.Reflector) {
logger := core.GetLogger()

checkOp, err := reflector.NewOperationContext(http.MethodGet, "/blocklist/check")
if err != nil {
logger.Fatalw(err.Error())
}

statusCodes := []int{http.StatusOK, http.StatusUnauthorized}
for _, status := range statusCodes {
checkOp.AddRespStructure(new(blocklist.CheckResult), func(cu *openapi.ContentUnit) { cu.HTTPStatus = status })
}

err = reflector.AddOperation(checkOp)
if err != nil {
logger.Fatalw(err.Error())
}
}
52 changes: 52 additions & 0 deletions web/openapi.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package web

import (
"fmt"

"github.com/divergentcodes/jwt-block/internal/core"
"github.com/swaggest/openapi-go/openapi3"
)

// Generate the OpenAPI spec for the service.
func GenerateOpenAPI(format string) (string, error) {
reflector := openapi3.Reflector{}

// Basic info.
reflector.Spec = &openapi3.Spec{
Openapi: "3.0.3",
}
reflector.Spec.Info.
WithTitle("JWT Block").
WithVersion(core.Version).
WithDescription("API of the JWT Block service")

// Base URL.
server := openapi3.Server{
URL: "http://jwtblock.localhost",
}
reflector.Spec.Servers = append(reflector.Spec.Servers, server)

// Declare security scheme.
securityName := "bearerToken"
reflector.Spec.SetHTTPBearerTokenSecurity(securityName, "JWT", "Access token")
reflector.Spec.WithSecurity(map[string][]string{securityName: {}})

// Endpoints.
blockGenerateOpenAPI(&reflector)
checkGenerateOpenAPI(&reflector)

// Dump the schema.
var schema []byte
var err error
if format == "json" {
schema, err = reflector.Spec.MarshalJSON()
} else if format == "yaml" {
schema, err = reflector.Spec.MarshalYAML()
} else {
return "", fmt.Errorf("invalid OpenAPI output format: %s", format)
}
if err != nil {
return "", err
}
return string(schema), nil
}

0 comments on commit cc6ac0c

Please sign in to comment.