From dbca33c1fcc488be2a2a173601c03a689f42f9f6 Mon Sep 17 00:00:00 2001 From: Karen <31778860+nerdyslacker@users.noreply.github.com> Date: Thu, 14 Mar 2024 15:31:54 +0400 Subject: [PATCH] Added echo middleware with tests (#16) --- README.md | 44 +++++++++- echo_middleware.go | 76 +++++++++++++++++ echo_middleware_test.go | 180 ++++++++++++++++++++++++++++++++++++++++ go.mod | 13 +-- go.sum | 28 ++++--- 5 files changed, 324 insertions(+), 17 deletions(-) create mode 100644 echo_middleware.go create mode 100644 echo_middleware_test.go diff --git a/README.md b/README.md index e0b3ca7..07812b9 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Open Policy Agent Middleware -This middleware integrates Open Policy Agent (OPA) to your http/gin/fiber app. +This middleware integrates Open Policy Agent (OPA) to your http/gin/fiber/echo app. You can use it to enforce policies on endpoints. You can use OPA as local policy engine, or as a remote policy engine. @@ -200,4 +200,46 @@ func main() { }) app.Listen(":8080") } +``` + +## Usage with Echo +```go +package main + +import ( + "github.com/Joffref/opa-middleware" + "github.com/Joffref/opa-middleware/config" + "github.com/labstack/echo/v4" +) + +func main() { + e := echo.New() + middleware, err := opamiddleware.NewEchoMiddleware( + &config.Config{ + URL: "http://localhost:8181/", + Query: "data.policy.allow", + ExceptedResult: true, + DeniedStatusCode: 403, + DeniedMessage: "Forbidden", + }, + func(c echo.Context) (map[string]interface{}, error) { + return map[string]interface{}{ + "path": c.Request().URL.Path, + "method": c.Request().Method, + }, nil + }, + ) + if err != nil { + return + } + e.Use(middleware.Use()) + e.GET("/ping", func(c echo.Context) error { + err := c.JSON(200, "pong") + if err != nil { + return err + } + return nil + }) + e.Start(":8080") +} ``` \ No newline at end of file diff --git a/echo_middleware.go b/echo_middleware.go new file mode 100644 index 0000000..cce338f --- /dev/null +++ b/echo_middleware.go @@ -0,0 +1,76 @@ +package opamiddleware + +import ( + "errors" + "github.com/Joffref/opa-middleware/config" + "github.com/Joffref/opa-middleware/internal" + "github.com/labstack/echo/v4" + "net/http" +) + +type EchoInputCreationMethod func(c echo.Context) (map[string]interface{}, error) + +type EchoMiddleware struct { + Config *config.Config + InputCreationMethod EchoInputCreationMethod `json:"binding_method,omitempty"` +} + +func NewEchoMiddleware(cfg *config.Config, input EchoInputCreationMethod) (*EchoMiddleware, error) { + err := cfg.Validate() + if err != nil { + return nil, err + } + if input == nil { + if cfg.InputCreationMethod == nil { + return nil, errors.New("[opa-middleware-echo] InputCreationMethod must be provided") + } + input = func(c echo.Context) (map[string]interface{}, error) { + bind, err := cfg.InputCreationMethod(c.Request()) + if err != nil { + return nil, err + } + return bind, nil + } + } + return &EchoMiddleware{ + Config: cfg, + InputCreationMethod: input, + }, nil +} + +func (e *EchoMiddleware) Use() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + if e.Config.Debug { + e.Config.Logger.Printf("[opa-middleware-echo] Request received") + } + result, err := e.query(c) + if err != nil { + if e.Config.Debug { + e.Config.Logger.Printf("[opa-middleware-echo] Error: %s", err.Error()) + } + return c.JSON(http.StatusInternalServerError, map[string]interface{}{"error": err.Error()}) + } + if e.Config.Debug { + e.Config.Logger.Printf("[opa-middleware-echo] Result: %t", result) + } + if result != e.Config.ExceptedResult { + return c.JSON(e.Config.DeniedStatusCode, map[string]interface{}{"error": e.Config.DeniedMessage}) + } + return next(c) + } + } +} + +func (e *EchoMiddleware) query(c echo.Context) (bool, error) { + bind, err := e.InputCreationMethod(c) + if err != nil { + return !e.Config.ExceptedResult, err + } + if e.Config.URL != "" { + input := make(map[string]interface{}) + input["input"] = bind + return internal.QueryURL(c.Request(), e.Config, input) + } + return internal.QueryPolicy(c.Request(), e.Config, bind) +} \ No newline at end of file diff --git a/echo_middleware_test.go b/echo_middleware_test.go new file mode 100644 index 0000000..1db40dc --- /dev/null +++ b/echo_middleware_test.go @@ -0,0 +1,180 @@ +package opamiddleware + +import ( + "github.com/Joffref/opa-middleware/config" + "github.com/labstack/echo/v4" + "net/http" + "net/http/httptest" + "net/url" + "testing" +) + +var Test_Policy = ` +package policy + +default allow = false + +allow { + input.path = "/api/v1/users" + input.method = "GET" +}` + +func TestEchoMiddleware_Query(t *testing.T) { + type fields struct { + Config *config.Config + InputCreationMethod EchoInputCreationMethod + } + type args struct { + req *http.Request + } + tests := []struct { + name string + fields fields + args args + want bool + wantErr bool + }{ + { + name: "Test EchoMiddleware_Query", + fields: fields{ + Config: &config.Config{ + Policy: Test_Policy, + Query: "data.policy.allow", + ExceptedResult: true, + DeniedStatusCode: 403, + DeniedMessage: "Forbidden", + }, + InputCreationMethod: func(c echo.Context) (map[string]interface{}, error) { + return map[string]interface{}{ + "path": c.Request().URL.Path, + "method": c.Request().Method, + }, nil + }, + }, + args: args{ + req: &http.Request{ + URL: &url.URL{ + Path: "/api/v1/users", + }, + Method: "GET", + }, + }, + want: true, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := echo.New() + h := &EchoMiddleware{ + Config: tt.fields.Config, + InputCreationMethod: tt.fields.InputCreationMethod, + } + c := e.NewContext(tt.args.req, httptest.NewRecorder()) + got, err := h.query(c) + if (err != nil) != tt.wantErr { + t.Errorf("Query() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("Query() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestEchoMiddleware_Use(t *testing.T) { + type fields struct { + Config *config.Config + InputCreationMethod EchoInputCreationMethod + } + tests := []struct { + name string + fields fields + }{ + { + name: "Test EchoMiddleware_Use", + fields: fields{ + Config: &config.Config{ + Policy: Test_Policy, + Query: "data.policy.allow", + ExceptedResult: true, + DeniedStatusCode: 403, + DeniedMessage: "Forbidden", + }, + InputCreationMethod: func(c echo.Context) (map[string]interface{}, error) { + return map[string]interface{}{ + "path": c.Request().URL.Path, + "method": c.Request().Method, + }, nil + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + h := &EchoMiddleware{ + Config: tt.fields.Config, + InputCreationMethod: tt.fields.InputCreationMethod, + } + h.Use() + }) + } +} + +func TestNewEchoMiddleware(t *testing.T) { + type args struct { + cfg *config.Config + inputCreationMethod EchoInputCreationMethod + } + tests := []struct { + name string + args args + want *EchoMiddleware + wantErr bool + }{ + { + name: "Test NewEchoMiddleware", + args: args{ + cfg: &config.Config{ + Policy: "policy", + Query: "data.query", + ExceptedResult: true, + DeniedStatusCode: 403, + DeniedMessage: "Forbidden", + }, + inputCreationMethod: func(c echo.Context) (map[string]interface{}, error) { + return map[string]interface{}{ + "path": c.Request().URL.Path, + "method": c.Request().Method, + }, nil + }, + }, + want: &EchoMiddleware{ + Config: &config.Config{ + Policy: "policy", + Query: "data.query", + ExceptedResult: true, + DeniedStatusCode: 403, + DeniedMessage: "Forbidden", + }, + InputCreationMethod: func(c echo.Context) (map[string]interface{}, error) { + return map[string]interface{}{ + "path": c.Request().URL.Path, + "method": c.Request().Method, + }, nil + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := NewEchoMiddleware(tt.args.cfg, tt.args.inputCreationMethod) + if (err != nil) != tt.wantErr { + t.Errorf("NewEchoMiddleware() error = %v, wantErr %v", err, tt.wantErr) + return + } + }) + } +} diff --git a/go.mod b/go.mod index 08f596f..3bc7b50 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.18 require ( github.com/gin-gonic/gin v1.9.1 github.com/gofiber/fiber/v2 v2.49.2 + github.com/labstack/echo/v4 v4.11.4 github.com/open-policy-agent/opa v0.43.1 github.com/valyala/fasthttp v1.49.0 ) @@ -27,9 +28,10 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.16.7 // indirect github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/labstack/gommon v0.4.2 // indirect github.com/leodido/go-urn v1.2.4 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -40,16 +42,17 @@ require ( github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.11 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect github.com/valyala/tcplisten v1.0.0 // indirect github.com/vektah/gqlparser/v2 v2.4.6 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/yashtewari/glob-intersection v0.1.0 // indirect golang.org/x/arch v0.3.0 // indirect - golang.org/x/crypto v0.14.0 // indirect - golang.org/x/net v0.17.0 // indirect - golang.org/x/sys v0.13.0 // indirect - golang.org/x/text v0.13.0 // indirect + golang.org/x/crypto v0.17.0 // indirect + golang.org/x/net v0.19.0 // indirect + golang.org/x/sys v0.15.0 // indirect + golang.org/x/text v0.14.0 // indirect google.golang.org/protobuf v1.30.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 78e273f..e1c344b 100644 --- a/go.sum +++ b/go.sum @@ -651,6 +651,10 @@ github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8= +github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo= @@ -668,8 +672,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= @@ -923,8 +927,8 @@ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1F github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= @@ -948,6 +952,8 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fasthttp v1.49.0 h1:9FdvCpmxB74LH4dPb7IJ1cOSsluR07XG3I1txXWwJpE= github.com/valyala/fasthttp v1.49.0/go.mod h1:k2zXd82h/7UZc3VOdJ2WaUqt1uZ/XpXAfE9i+HBC3lA= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/vektah/gqlparser/v2 v2.4.6 h1:Yjzp66g6oVq93Jihbi0qhGnf/6zIWjcm8H6gA27zstE= @@ -1066,8 +1072,8 @@ golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1164,8 +1170,8 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= +golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1299,8 +1305,8 @@ golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= +golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -1315,8 +1321,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=