From b649a18a0c8edabd4c6591990cc965fc71200d53 Mon Sep 17 00:00:00 2001 From: blobcode Date: Mon, 9 Feb 2026 19:44:39 -0500 Subject: [PATCH 1/4] basic oauth --- src/api.go | 108 +++++--------- src/go.mod | 27 +++- src/go.sum | 259 ++++++++++++++++++++++++++++++++++ src/int_test.go | 4 +- src/job_scheduling_test.go | 2 +- src/oauth.go | 74 ++++++++++ src/oauth_integration_test.go | 114 +++++++++++++++ src/scheduler.go | 126 ++++++++--------- src/status.go | 2 +- src/supervisor.go | 18 ++- 10 files changed, 579 insertions(+), 155 deletions(-) create mode 100644 src/oauth.go create mode 100644 src/oauth_integration_test.go diff --git a/src/api.go b/src/api.go index 566c74b..1f00b4e 100644 --- a/src/api.go +++ b/src/api.go @@ -15,7 +15,7 @@ import ( "syscall" "time" - "github.com/redis/go-redis/v9" + "github.com/go-redis/redis/v8" ) type App struct { @@ -26,6 +26,7 @@ type App struct { wg sync.WaitGroup log *slog.Logger statusRegistry *StatusRegistry + oauthServer *OAuthServer } func NewApp(redisAddr, gpuType string, log *slog.Logger) *App { @@ -36,6 +37,13 @@ func NewApp(redisAddr, gpuType string, log *slog.Logger) *App { consumerID := fmt.Sprintf("worker_%d", os.Getpid()) supervisor := NewSupervisor(redisAddr, consumerID, gpuType, log) + // Initialize OAuth2 server + oauthServer, err := NewOAuthServer(redisAddr, client, log) + if err != nil { + log.Error("failed to initialize oauth server", "err", err) + // For now, we don't exit, but we should probably handle this better + } + mux := http.NewServeMux() a := &App{ redisClient: client, @@ -44,6 +52,7 @@ func NewApp(redisAddr, gpuType string, log *slog.Logger) *App { httpServer: &http.Server{Addr: ":3000", Handler: mux}, log: log, statusRegistry: statusRegistry, + oauthServer: oauthServer, } mux.HandleFunc("/auth/login", a.login) @@ -54,6 +63,10 @@ func NewApp(redisAddr, gpuType string, log *slog.Logger) *App { mux.HandleFunc("/supervisors/status/", a.getSupervisorStatusByID) mux.HandleFunc("/supervisors", a.getAllSupervisors) + // OAuth2 routes + mux.HandleFunc("/oauth/authorize", a.handleAuthorize) + mux.HandleFunc("/oauth/token", a.handleToken) + a.log.Info("new app initialized", "redis_address", redisAddr, "gpu_type", gpuType, "http_address", a.httpServer.Addr) @@ -167,6 +180,22 @@ func (a *App) refresh(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello, world!\n") } +func (a *App) handleAuthorize(w http.ResponseWriter, r *http.Request) { + err := a.oauthServer.Server.HandleAuthorizeRequest(w, r) + if err != nil { + a.log.Error("authorization request failed", "error", err) + http.Error(w, err.Error(), http.StatusBadRequest) + } +} + +func (a *App) handleToken(w http.ResponseWriter, r *http.Request) { + err := a.oauthServer.Server.HandleTokenRequest(w, r) + if err != nil { + a.log.Error("token request failed", "error", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + type CreateJobRequest struct { Type string `json:"type"` Payload map[string]interface{} `json:"payload"` @@ -200,8 +229,9 @@ func (a *App) createJob(w http.ResponseWriter, r *http.Request) { http.Error(w, "Job type is required", http.StatusBadRequest) return } - if err := a.scheduler.Enqueue("jobType", "gpuType", payload); err != nil { - a.log.Error("enqueue failed", "err", err, "payload", payload) + jobID, err := a.scheduler.Enqueue(req.Type, req.RequiredGPU, req.Payload) + if err != nil { + a.log.Error("enqueue failed", "err", err, "payload", req.Payload) http.Error(w, "enqueue failed", http.StatusInternalServerError) return } @@ -325,75 +355,3 @@ func (a *App) getAllSupervisors(w http.ResponseWriter, r *http.Request) { return } } - -func (a *App) getSupervisorStatus(w http.ResponseWriter, r *http.Request) { - supervisors, err := a.statusRegistry.GetAllSupervisors() - if err != nil { - a.log.Error("failed to get supervisor status", "error", err) - http.Error(w, "failed to get supervisor status", http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(map[string]interface{}{ - "supervisors": supervisors, - "count": len(supervisors), - }); err != nil { - a.log.Error("failed to encode supervisor status response", "error", err) - http.Error(w, "failed to encode response", http.StatusInternalServerError) - return - } -} - -func (a *App) getSupervisorStatusByID(w http.ResponseWriter, r *http.Request) { - // extract consumer ID from URL path - path := strings.TrimPrefix(r.URL.Path, "/supervisors/status/") - if path == "" { - http.Error(w, "consumer ID required", http.StatusBadRequest) - return - } - - supervisor, err := a.statusRegistry.GetSupervisor(path) - if err != nil { - a.log.Error("failed to get supervisor status", "consumer_id", path, "error", err) - http.Error(w, "supervisor not found", http.StatusNotFound) - return - } - - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(supervisor); err != nil { - a.log.Error("failed to encode supervisor status response", "error", err) - http.Error(w, "failed to encode response", http.StatusInternalServerError) - return - } -} - -func (a *App) getAllSupervisors(w http.ResponseWriter, r *http.Request) { - activeOnly := r.URL.Query().Get("active") == "true" - - var supervisors []SupervisorStatus - var err error - - if activeOnly { - supervisors, err = a.statusRegistry.GetActiveSupervisors() - } else { - supervisors, err = a.statusRegistry.GetAllSupervisors() - } - - if err != nil { - a.log.Error("failed to get supervisors", "active_only", activeOnly, "error", err) - http.Error(w, "failed to get supervisors", http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "application/json") - if err := json.NewEncoder(w).Encode(map[string]interface{}{ - "supervisors": supervisors, - "count": len(supervisors), - "active_only": activeOnly, - }); err != nil { - a.log.Error("failed to encode supervisors response", "error", err) - http.Error(w, "failed to encode response", http.StatusInternalServerError) - return - } -} diff --git a/src/go.mod b/src/go.mod index 097afa1..4d1f7e1 100644 --- a/src/go.mod +++ b/src/go.mod @@ -2,11 +2,32 @@ module mist go 1.24.3 -require github.com/redis/go-redis/v9 v9.10.0 +require ( + github.com/go-oauth2/oauth2/v4 v4.5.4 + github.com/go-oauth2/redis/v4 v4.1.1 + github.com/go-redis/redis/v8 v8.0.0-beta.5 + github.com/redis/go-redis/v9 v9.10.0 + gopkg.in/natefinch/lumberjack.v2 v2.2.1 + gopkg.in/yaml.v3 v3.0.1 +) require ( + github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect + github.com/google/uuid v1.1.1 // indirect + github.com/json-iterator/go v1.1.10 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.1 // indirect + github.com/tidwall/btree v0.0.0-20191029221954-400434d76274 // indirect + github.com/tidwall/buntdb v1.1.2 // indirect + github.com/tidwall/gjson v1.12.1 // indirect + github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect + github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e // indirect + github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563 // indirect + go.opentelemetry.io/otel v0.6.0 // indirect + google.golang.org/grpc v1.29.1 // indirect ) diff --git a/src/go.sum b/src/go.sum index 0cdd507..3617ea8 100644 --- a/src/go.sum +++ b/src/go.sum @@ -1,15 +1,274 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/DataDog/sketches-go v0.0.0-20190923095040-43f19ad77ff7 h1:qELHH0AWCvf98Yf+CNIJx9vOZOfHFDDzgDRYsnNk/vs= +github.com/DataDog/sketches-go v0.0.0-20190923095040-43f19ad77ff7/go.mod h1:Q5DbzQ+3AkgGwymQO7aZFNP7ns2lZKGtvRBzRXfdi60= +github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= +github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= +github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= +github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/benbjohnson/clock v1.0.0/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-rendezvous v0.0.0-20200609043717-5ab96a526299/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= +github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/gavv/httpexpect v2.0.0+incompatible h1:1X9kcRshkSKEjNJJxX9Y9mQ5BRfbxU5kORdjhlA1yX8= +github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= +github.com/go-oauth2/oauth2/v4 v4.1.0/go.mod h1:+rsyi0o/ZbSfhL/3Xr/sAtL4brS+IdGj86PHVlPjE+4= +github.com/go-oauth2/oauth2/v4 v4.5.4 h1:YjI0tmGW8oxVhn9QSBIxlr641QugWrJY5UWa6XmLcW0= +github.com/go-oauth2/oauth2/v4 v4.5.4/go.mod h1:BXiOY+QZtZy2ewbsGk2B5P8TWmtz/Rf7ES5ZttQFxfQ= +github.com/go-oauth2/redis/v4 v4.1.1 h1:uYLGPbAEZ3tb2Qg+BHzrtMHbJ7NeX6S9Ol0+iYyBF5E= +github.com/go-oauth2/redis/v4 v4.1.1/go.mod h1:cYNT5bLEwCnrFXqSbWDvxXzfTaF/fKMf1XoRVFwBPrc= +github.com/go-redis/redis/v8 v8.0.0-beta.5 h1:i4Rhw1v2H9HTWO05wsKdpGpFYFU9OW+foa2GuDIjbBA= +github.com/go-redis/redis/v8 v8.0.0-beta.5/go.mod h1:Mm9EH/5UMRx680UIryN6rd5XFn/L7zORPqLV+1D5thQ= +github.com/go-session/session v3.1.2+incompatible/go.mod h1:8B3iivBQjrz/JtC68Np2T1yBBLxTan3mn/3OM0CyRt0= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0= +github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= +github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= +github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= +github.com/klauspost/compress v1.10.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.15.0 h1:xqfchp4whNFxn5A4XFyyYtitiWI8Hy5EW59jEwcyL6U= +github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/moul/http2curl v1.0.0 h1:dRMWoAtb+ePxMlLkrCbAqh4TlPHXvoGUSQ323/9Zahs= +github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= +github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.13.0 h1:M76yO2HkZASFjXL0HSoZJ1AYEmQxNJmY41Jx1zNUq1Y= +github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/opentracing/opentracing-go v1.1.1-0.20190913142402-a7454ce5950e/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/redis/go-redis/v9 v9.10.0 h1:FxwK3eV8p/CQa0Ch276C7u2d0eNC9kCmAYQ7mCXCzVs= github.com/redis/go-redis/v9 v9.10.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= +github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/assertions v1.1.0 h1:MkTeG1DMwsrdH7QtLXy5W+fUxWq+vmb6cLmyJ7aRtF0= +github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/tidwall/btree v0.0.0-20191029221954-400434d76274 h1:G6Z6HvJuPjG6XfNGi/feOATzeJrfgTNJY+rGrHbA04E= +github.com/tidwall/btree v0.0.0-20191029221954-400434d76274/go.mod h1:huei1BkDWJ3/sLXmO+bsCNELL+Bp2Kks9OLyQFkzvA8= +github.com/tidwall/buntdb v1.1.2 h1:noCrqQXL9EKMtcdwJcmuVKSEjqu1ua99RHHgbLTEHRo= +github.com/tidwall/buntdb v1.1.2/go.mod h1:xAzi36Hir4FarpSHyfuZ6JzPJdjRZ8QlLZSntE2mqlI= +github.com/tidwall/gjson v1.3.4/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls= +github.com/tidwall/gjson v1.6.0/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls= +github.com/tidwall/gjson v1.12.1 h1:ikuZsLdhr8Ws0IdROXUS1Gi4v9Z4pGqpX/CvJkxvfpo= +github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb h1:5NSYaAdrnblKByzd7XByQEJVT8+9v0W/tIY0Oo4OwrE= +github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb/go.mod h1:lKYYLFIr9OIgdgrtgkZ9zgRxRdvPYsExnYBsEAd8W5M= +github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tidwall/pretty v1.0.1/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e h1:+NL1GDIUOKxVfbp2KoJQD9cTQ6dyP2co9q4yzmT9FZo= +github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e/go.mod h1:/h+UnNGt0IhNNJLkGikcdcJqm66zGD/uJGMRxK/9+Ao= +github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563 h1:Otn9S136ELckZ3KKDyCkxapfufrqDqwmGjcHfAyXRrE= +github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563/go.mod h1:mLqSmt7Dv/CNneF2wfcChfN1rvapyQr01LGKnKex0DQ= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.14.0/go.mod h1:ol1PCaL0dX20wC0htZ7sYCsvCYmrouYra0zHzaclZhE= +github.com/valyala/fasthttp v1.34.0 h1:d3AAQJ2DRcxJYHm7OXNXtXt2as1vMDfxeIcFvhmGGm4= +github.com/valyala/fasthttp v1.34.0/go.mod h1:epZA5N+7pY6ZaEKRmstzOuYJx9HI8DI1oaCGZpdH4h0= +github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= +github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= +github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= +github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= +github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= +go.opentelemetry.io/otel v0.6.0 h1:+vkHm/XwJ7ekpISV2Ixew93gCrxTbuwTF5rSewnLLgw= +go.opentelemetry.io/otel v0.6.0/go.mod h1:jzBIgIzK43Iu1BpDAXwqOd6UPsSAk+ewVZ5ofSXw4Ek= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae h1:Ih9Yo4hSPImZOpfGuA4bR/ORKTAbhZo2AbWNRCnevdo= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20191009194640-548a555dbc03 h1:4HYDjxeNXAOTv3o1N2tjo8UUSlhQgAD52FVkwxnWgM8= +google.golang.org/genproto v0.0.0-20191009194640-548a555dbc03/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.29.1 h1:EC2SB8S04d2r73uptxphDSUG+kTKVgjRPF+N3xpxRB4= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/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= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/src/int_test.go b/src/int_test.go index 851a24f..1d9e20f 100644 --- a/src/int_test.go +++ b/src/int_test.go @@ -12,7 +12,7 @@ import ( "testing" "time" - "github.com/redis/go-redis/v9" + "github.com/go-redis/redis/v8" ) func addDummySupervisors(statusRegistry *StatusRegistry, log *slog.Logger) { @@ -100,7 +100,7 @@ func TestIntegration(t *testing.T) { "data": fmt.Sprintf("test_data_%d", i), } - if err := scheduler.Enqueue(jobType, "TT", payload); err != nil { + if _, err := scheduler.Enqueue(jobType, "TT", payload); err != nil { t.Errorf("Failed to enqueue job: %v", err) } } diff --git a/src/job_scheduling_test.go b/src/job_scheduling_test.go index 62c165e..b7271f1 100644 --- a/src/job_scheduling_test.go +++ b/src/job_scheduling_test.go @@ -36,7 +36,7 @@ func TestJobEnqueueAndSupervisor(t *testing.T) { // Enqueue jobs for i := 0; i < 3; i++ { payload := map[string]interface{}{"task": i} - if err := scheduler.Enqueue("test_job_type", "AMD", payload); err != nil { + if _, err := scheduler.Enqueue("test_job_type", "AMD", payload); err != nil { t.Errorf("Failed to enqueue job %d: %v", i, err) } } diff --git a/src/oauth.go b/src/oauth.go new file mode 100644 index 0000000..f23d377 --- /dev/null +++ b/src/oauth.go @@ -0,0 +1,74 @@ +package main + +import ( + "log/slog" + "net/http" + "time" + + "github.com/go-oauth2/oauth2/v4" + "github.com/go-oauth2/oauth2/v4/errors" + "github.com/go-oauth2/oauth2/v4/generates" + "github.com/go-oauth2/oauth2/v4/manage" + "github.com/go-oauth2/oauth2/v4/models" + "github.com/go-oauth2/oauth2/v4/server" + "github.com/go-oauth2/oauth2/v4/store" + oredis "github.com/go-oauth2/redis/v4" + "github.com/go-redis/redis/v8" +) + +type OAuthServer struct { + Server *server.Server + Manager *manage.Manager +} + +func NewOAuthServer(redisAddr string, client *redis.Client, log *slog.Logger) (*OAuthServer, error) { + var tokenStore oauth2.TokenStore + if redisAddr == "memory" { + tokenStore, _ = store.NewMemoryTokenStore() + } else { + tokenStore = oredis.NewRedisStore(&redis.Options{Addr: redisAddr}) + } + + clientStore := store.NewClientStore() + err := clientStore.Set("cli", &models.Client{ + ID: "cli", + Secret: "demo-client-secret", + Domain: "http://localhost:3000", + }) + if err != nil { + return nil, err + } + + manager := manage.NewDefaultManager() + manager.MapTokenStorage(tokenStore) + manager.MapClientStorage(clientStore) + + // Enable PKCE + manager.SetValidateURIHandler(manage.DefaultValidateURI) + manager.SetAuthorizeCodeExp(time.Minute * 10) + + manager.MapAccessGenerate(generates.NewAccessGenerate()) + + srv := server.NewDefaultServer(manager) + + srv.SetAllowGetAccessRequest(true) + srv.SetClientInfoHandler(server.ClientFormHandler) + + srv.SetUserAuthorizationHandler(func(w http.ResponseWriter, r *http.Request) (userID string, err error) { + return "test-user", nil + }) + + srv.SetInternalErrorHandler(func(err error) (re *errors.Response) { + log.Error("Internal OAuth2 error", "error", err) + return + }) + + srv.SetResponseErrorHandler(func(re *errors.Response) { + log.Error("OAuth2 response error", "error", re.Error) + }) + + return &OAuthServer{ + Server: srv, + Manager: manager, + }, nil +} diff --git a/src/oauth_integration_test.go b/src/oauth_integration_test.go new file mode 100644 index 0000000..ff1fa51 --- /dev/null +++ b/src/oauth_integration_test.go @@ -0,0 +1,114 @@ +package main + +import ( + "crypto/sha256" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "testing" + "time" + + log2 "mist/multilogger" +) + +// Helper for PKCE +func generateCodeChallenge(verifier string) string { + s := sha256.New() + s.Write([]byte(verifier)) + return base64.RawURLEncoding.EncodeToString(s.Sum(nil)) +} + +func TestOAuthFlow(t *testing.T) { + // Setup app + cfg, err := log2.GetLogConfig() + if err != nil { + t.Fatalf("Failed to get log config: %v", err) + } + log, err := log2.CreateLogger("test", &cfg) + if err != nil { + t.Fatalf("Failed to create logger: %v", err) + } + + app := NewApp("memory", "TestGPU", log) + + // Create test server + ts := httptest.NewServer(app.httpServer.Handler) + defer ts.Close() + + // 1. Authorize Request + clientID := "demo-client-id" + verifier := "some-random-secret-verifier-string-1234567890" // High entropy string + challenge := generateCodeChallenge(verifier) + redirectURI := "http://localhost:3000" + + authURL := fmt.Sprintf("%s/oauth/authorize?client_id=%s&response_type=code&redirect_uri=%s&code_challenge=%s&code_challenge_method=S256", + ts.URL, clientID, url.QueryEscape(redirectURI), challenge) + + t.Logf("Requesting authorization: %s", authURL) + + client := ts.Client() + client.Timeout = 5 * time.Second + // Disable redirect following to inspect the location + client.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } + + resp, err := client.Get(authURL) + if err != nil { + t.Fatalf("Failed to GET authorize: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusFound { + body, _ := io.ReadAll(resp.Body) + t.Fatalf("Expected 302 Found, got %d. Body: %s", resp.StatusCode, body) + } + + loc, err := resp.Location() + if err != nil { + t.Fatalf("Failed to get location: %v", err) + } + + code := loc.Query().Get("code") + if code == "" { + t.Fatalf("No code in redirect location: %s", loc.String()) + } + t.Logf("Got auth code: %s", code) + + // 2. Token Exchange + tokenURL := fmt.Sprintf("%s/oauth/token", ts.URL) + data := url.Values{} + data.Set("grant_type", "authorization_code") + data.Set("client_id", clientID) + data.Set("client_secret", "demo-client-secret") + data.Set("code", code) + data.Set("redirect_uri", redirectURI) + data.Set("code_verifier", verifier) + + t.Logf("Exchanging token at: %s", tokenURL) + + resp, err = client.PostForm(tokenURL, data) + if err != nil { + t.Fatalf("Failed to POST token: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + t.Fatalf("Expected 200 OK, got %d. Body: %s", resp.StatusCode, body) + } + + var tokenResp map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil { + t.Fatalf("Failed to decode token response: %v", err) + } + + if _, ok := tokenResp["access_token"]; !ok { + t.Errorf("No access_token in response: %v", tokenResp) + } + t.Logf("Token response: %v", tokenResp) +} diff --git a/src/scheduler.go b/src/scheduler.go index a9d8c7a..ba4d471 100644 --- a/src/scheduler.go +++ b/src/scheduler.go @@ -3,12 +3,12 @@ package main import ( "context" "encoding/json" + "errors" "fmt" "log/slog" "time" - "errors" - "github.com/redis/go-redis/v9" + "github.com/go-redis/redis/v8" ) type Scheduler struct { @@ -29,7 +29,7 @@ func NewScheduler(redisAddr string, log *slog.Logger) *Scheduler { } } -func (s *Scheduler) Enqueue(jobType string, requiredGPU string, payload map[string]interface{}) error { +func (s *Scheduler) Enqueue(jobType string, requiredGPU string, payload map[string]interface{}) (string, error) { // create a new job job := Job{ ID: generateJobID(), @@ -42,17 +42,17 @@ func (s *Scheduler) Enqueue(jobType string, requiredGPU string, payload map[stri } if ok, err := s.JobExists(job.ID); err != nil { - return err + return "", err } else if ok { s.log.Warn("duplicate job skipped", "job_id", job.ID) - return nil + return job.ID, nil } // marshal the payload payloadJSON, err := json.Marshal(job.Payload) if err != nil { s.log.Error("failed to marshal job payload", "error", err) - return err + return "", err } // start redis pipeline @@ -62,8 +62,8 @@ func (s *Scheduler) Enqueue(jobType string, requiredGPU string, payload map[stri pipe.XAdd(s.ctx, &redis.XAddArgs{ Stream: StreamName, Values: map[string]interface{}{ - "job_id": job.ID, - "payload": string(payloadJSON), + "job_id": job.ID, + "payload": string(payloadJSON), "job_state": string(job.JobState), }, }) @@ -73,7 +73,7 @@ func (s *Scheduler) Enqueue(jobType string, requiredGPU string, payload map[stri pipe.HSet(s.ctx, metadataKey, map[string]interface{}{ "type": job.Type, "retries": job.Retries, - "created": job.Created.Format(time.RFC3339), + "created": job.Created.Format(time.RFC3339), "required_gpu": job.RequiredGPU, "job_state": string(job.JobState), }) @@ -81,7 +81,7 @@ func (s *Scheduler) Enqueue(jobType string, requiredGPU string, payload map[stri // execute pipeline if _, err := pipe.Exec(s.ctx); err != nil { s.log.Error("failed to enqueue job", "error", err) - return err + return "", err } s.log.Info("enqueued job", "job_id", job.ID, "job_type", job.Type, "gpu", requiredGPU) @@ -93,64 +93,64 @@ func (s *Scheduler) Close() error { } func (s *Scheduler) JobExists(jobID string) (bool, error) { - exists, err := s.client.Exists(s.ctx, "job:"+jobID).Result() - if err != nil { - return false, err - } - return exists > 0, nil + exists, err := s.client.Exists(s.ctx, "job:"+jobID).Result() + if err != nil { + return false, err + } + return exists > 0, nil } func (s *Scheduler) ListenForEvents() { - s.log.Info("listening for job events...", "stream", JobEventStream) - - lastID := "$" - - for { - result, err := s.client.XRead(s.ctx, &redis.XReadArgs{ - Streams: []string{JobEventStream, lastID}, - Count: 10, - Block: 5 * time.Second, - }).Result() - - if err != nil { - if errors.Is(err, redis.Nil) { - continue // no new messages - } - s.log.Error("error reading from event stream", "error", err) + s.log.Info("listening for job events...", "stream", JobEventStream) + + lastID := "$" + + for { + result, err := s.client.XRead(s.ctx, &redis.XReadArgs{ + Streams: []string{JobEventStream, lastID}, + Count: 10, + Block: 5 * time.Second, + }).Result() + + if err != nil { + if errors.Is(err, redis.Nil) { + continue // no new messages + } + s.log.Error("error reading from event stream", "error", err) time.Sleep(time.Second) - continue - } - - for _, stream := range result { - for _, msg := range stream.Messages { - s.handleEventMessage(msg) - lastID = msg.ID - } - } - } + continue + } + + for _, stream := range result { + for _, msg := range stream.Messages { + s.handleEventMessage(msg) + lastID = msg.ID + } + } + } } func (s *Scheduler) handleEventMessage(msg redis.XMessage) { - jobID, _ := msg.Values["job_id"].(string) - state, _ := msg.Values["state"].(string) - timestamp, _ := msg.Values["timestamp"].(string) - supervisor, _ := msg.Values["supervisor"].(string) - - if jobID == "" { - s.log.Warn("received event with missing job_id", "message_id", msg.ID) - return - } - - metadataKey := fmt.Sprintf("job:%s", jobID) - - // Update job state in Redis - if err := s.client.HSet(s.ctx, metadataKey, "job_state", state, "updated_at", timestamp).Err(); err != nil { - s.log.Error("failed to update job metadata", "job_id", jobID, "error", err) - return - } - - s.log.Info("job state updated", - "job_id", jobID, - "state", state, - "supervisor", supervisor) + jobID, _ := msg.Values["job_id"].(string) + state, _ := msg.Values["state"].(string) + timestamp, _ := msg.Values["timestamp"].(string) + supervisor, _ := msg.Values["supervisor"].(string) + + if jobID == "" { + s.log.Warn("received event with missing job_id", "message_id", msg.ID) + return + } + + metadataKey := fmt.Sprintf("job:%s", jobID) + + // Update job state in Redis + if err := s.client.HSet(s.ctx, metadataKey, "job_state", state, "updated_at", timestamp).Err(); err != nil { + s.log.Error("failed to update job metadata", "job_id", jobID, "error", err) + return + } + + s.log.Info("job state updated", + "job_id", jobID, + "state", state, + "supervisor", supervisor) } diff --git a/src/status.go b/src/status.go index bbb1292..ebd74ca 100644 --- a/src/status.go +++ b/src/status.go @@ -7,7 +7,7 @@ import ( "fmt" "log/slog" - "github.com/redis/go-redis/v9" + "github.com/go-redis/redis/v8" ) type StatusRegistry struct { diff --git a/src/supervisor.go b/src/supervisor.go index aa93e82..e8e2153 100644 --- a/src/supervisor.go +++ b/src/supervisor.go @@ -6,11 +6,11 @@ import ( "errors" "fmt" "log/slog" + "strconv" "sync" "time" - "strconv" - "github.com/redis/go-redis/v9" + "github.com/go-redis/redis/v8" ) type Supervisor struct { @@ -132,9 +132,9 @@ func (s *Supervisor) handleMessage(message redis.XMessage) { } if len(metadata) == 0 { - s.log.Error("job metadata not found", "job_id", jobID) - s.ackMessage(message.ID) - return + s.log.Error("job metadata not found", "job_id", jobID) + s.ackMessage(message.ID) + return } jobType := metadata["type"] @@ -143,7 +143,7 @@ func (s *Supervisor) handleMessage(message redis.XMessage) { createdTime, _ := time.Parse(time.RFC3339, metadata["created"]) retries, _ := strconv.Atoi(metadata["retries"]) - + job := Job{ ID: jobID, Type: jobType, @@ -192,11 +192,10 @@ func (s *Supervisor) processJob(job Job) bool { return true } - func (s *Supervisor) emitJobEvent(jobID string, state JobState) { event := map[string]interface{}{ - "job_id": jobID, - "state": string(state), + "job_id": jobID, + "state": string(state), "timestamp": time.Now().Format(time.RFC3339), "supervisor": s.consumerID, "gpu_type": s.gpuType, @@ -212,7 +211,6 @@ func (s *Supervisor) emitJobEvent(jobID string, state JobState) { } } - func (s *Supervisor) ackMessage(messageID string) { result := s.redisClient.XAck(s.ctx, StreamName, ConsumerGroup, messageID) if result.Err() != nil { From cde0d28262add2c34cea9cc3e91cb14e45a0d39b Mon Sep 17 00:00:00 2001 From: blobcode Date: Mon, 9 Feb 2026 20:48:04 -0500 Subject: [PATCH 2/4] implement cli portion --- .gitignore | 1 + cli/cmd/auth_login.go | 120 ++++++++++++++--------- go.sum | 0 src/api.go | 177 +++++++++++++++++++++++++++++----- src/go.mod | 8 +- src/go.sum | 79 ++------------- src/oauth.go | 27 +++++- src/oauth_integration_test.go | 114 ---------------------- src/redis_store.go | 81 ++++++++++++++++ src/user.go | 77 +++++++++++++++ 10 files changed, 424 insertions(+), 260 deletions(-) create mode 100644 go.sum delete mode 100644 src/oauth_integration_test.go create mode 100644 src/redis_store.go create mode 100644 src/user.go diff --git a/.gitignore b/.gitignore index e660fd9..5f2766f 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ bin/ +logs/app.log diff --git a/cli/cmd/auth_login.go b/cli/cmd/auth_login.go index 9209865..628a67d 100644 --- a/cli/cmd/auth_login.go +++ b/cli/cmd/auth_login.go @@ -2,8 +2,14 @@ package cmd import ( "bufio" + "crypto/rand" + "crypto/sha256" + "encoding/base64" "encoding/json" "fmt" + "io" + "net/http" + "net/url" "os" "os/exec" "path/filepath" @@ -11,25 +17,42 @@ import ( "strings" ) -// TODO: Update with real auth URL -const authUrl = "https://example.com/login" +const ( + serverBaseUrl = "http://localhost:3000" + authUrl = serverBaseUrl + "/oauth/authorize" + tokenUrl = serverBaseUrl + "/oauth/token" + clientID = "cli" + redirectURI = serverBaseUrl + "/oauth/callback" +) type LoginCmd struct { } -func openUrl() error { +func generateRandomString(n int) (string, error) { + b := make([]byte, n) + _, err := rand.Read(b) + if err != nil { + return "", err + } + return base64.RawURLEncoding.EncodeToString(b), nil +} + +func generateCodeChallenge(verifier string) string { + s := sha256.New() + s.Write([]byte(verifier)) + return base64.RawURLEncoding.EncodeToString(s.Sum(nil)) +} +func openUrl(url string) error { var cmd *exec.Cmd - url := authUrl switch runtime.GOOS { case "darwin": cmd = exec.Command("open", url) case "windows": cmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", url) - default: // Linux, BSD, etc. + default: // Linux cmd = exec.Command("xdg-open", url) } - return cmd.Start() } @@ -51,59 +74,68 @@ func saveTokenToConfig(ctx *AppContext, token string) error { if err := os.WriteFile(configPath, data, 0o600); err != nil { return fmt.Errorf("failed to write config file: %w", err) } - fmt.Println("Token saved:", token) + fmt.Println("Token saved") return nil } -func getLongLivedToken(shortLivedToken string) (string, error) { - // Placeholder for actual implementation to exchange short-lived token for long-lived token - // In a real scenario, this would involve making an HTTP request to the auth server - return shortLivedToken + "_long_lived", nil -} - func (l *LoginCmd) Run(ctx *AppContext) error { - // mist auth login - if ctx.Config != nil && ctx.Config.AccessToken != "" { - - // Already logged in, ask if they want to re-login - fmt.Println("Already logged in with token:", ctx.Config.AccessToken) - fmt.Print("Re-enter token? (y/N): ") - reader := bufio.NewReader(os.Stdin) - answer, _ := reader.ReadString('\n') - answer = strings.TrimSpace(strings.ToLower(answer)) - if answer != "y" && answer != "yes" { - fmt.Println("Aborting login.") - return nil - } + verifier, err := generateRandomString(32) + if err != nil { + return fmt.Errorf("failed to generate verifier: %w", err) } + challenge := generateCodeChallenge(verifier) - fmt.Println("Opening browser for authentication...") - fmt.Printf("If your browser didn't open, click here: \033]8;;%s\033\\%s\033]8;;\033\\\n", authUrl, authUrl) + u, _ := url.Parse(authUrl) + q := u.Query() + q.Set("client_id", clientID) + q.Set("response_type", "code") + q.Set("redirect_uri", redirectURI) + q.Set("code_challenge", challenge) + q.Set("code_challenge_method", "S256") + u.RawQuery = q.Encode() - err := openUrl() - if err != nil { - fmt.Println("Error opening browser:", err) - return err + fmt.Printf("If your browser doesn't open, visit: %s\n", u.String()) + + if err := openUrl(u.String()); err != nil { + fmt.Printf("Failed to open browser: %v\n", err) } - fmt.Print("token: ") + fmt.Print("Enter the authorization code: ") reader := bufio.NewReader(os.Stdin) - token, _ := reader.ReadString('\n') - token = strings.TrimSpace(token) + code, _ := reader.ReadString('\n') + code = strings.TrimSpace(code) - token, err = getLongLivedToken(token) - if err != nil { - fmt.Println("Error obtaining long-lived token:", err) - return err + if code == "" { + return fmt.Errorf("authorization code is required") } - err = saveTokenToConfig(ctx, token) + data := url.Values{} + data.Set("grant_type", "authorization_code") + data.Set("client_id", clientID) + data.Set("code", code) + data.Set("redirect_uri", redirectURI) + data.Set("code_verifier", verifier) + + resp, err := http.PostForm(tokenUrl, data) if err != nil { - fmt.Println("Error during token saving") - return err + return fmt.Errorf("failed to exchange token: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("token exchange failed: %s", body) } - fmt.Println("Saved token to config") + var tokenResp map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil { + return fmt.Errorf("failed to decode token response: %w", err) + } - return nil + accessToken, ok := tokenResp["access_token"].(string) + if !ok { + return fmt.Errorf("invalid token response: missing access_token") + } + + return saveTokenToConfig(ctx, accessToken) } diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e69de29 diff --git a/src/api.go b/src/api.go index 1f00b4e..bf84f77 100644 --- a/src/api.go +++ b/src/api.go @@ -3,7 +3,6 @@ package main import ( "context" "encoding/json" - "errors" "fmt" "log/slog" log2 "mist/multilogger" @@ -15,7 +14,12 @@ import ( "syscall" "time" + "crypto/rand" + "encoding/hex" + "html/template" + "github.com/go-redis/redis/v8" + "golang.org/x/crypto/bcrypt" ) type App struct { @@ -27,18 +31,38 @@ type App struct { log *slog.Logger statusRegistry *StatusRegistry oauthServer *OAuthServer + userStore UserStore } func NewApp(redisAddr, gpuType string, log *slog.Logger) *App { client := redis.NewClient(&redis.Options{Addr: redisAddr}) scheduler := NewScheduler(redisAddr, log) statusRegistry := NewStatusRegistry(client, log) + userStore := NewRedisUserStore(client) + + ctx := context.Background() + _, err := userStore.GetByUsername(ctx, "admin") + if err != nil { + // Create admin user if not exists + // TODO: REMOVE THIS + hash, _ := bcrypt.GenerateFromPassword([]byte("admin"), bcrypt.DefaultCost) + admin := &User{ + ID: "admin", + Username: "admin", + PasswordHash: string(hash), + Role: "admin", + } + if err := userStore.Create(ctx, admin); err != nil { + log.Error("failed to seed admin user", "err", err) + } else { + log.Info("seeded admin user") + } + } consumerID := fmt.Sprintf("worker_%d", os.Getpid()) supervisor := NewSupervisor(redisAddr, consumerID, gpuType, log) - // Initialize OAuth2 server - oauthServer, err := NewOAuthServer(redisAddr, client, log) + oauthServer, err := NewOAuthServer(redisAddr, client, log, userStore) if err != nil { log.Error("failed to initialize oauth server", "err", err) // For now, we don't exit, but we should probably handle this better @@ -53,19 +77,21 @@ func NewApp(redisAddr, gpuType string, log *slog.Logger) *App { log: log, statusRegistry: statusRegistry, oauthServer: oauthServer, + userStore: userStore, } - mux.HandleFunc("/auth/login", a.login) + mux.HandleFunc("/auth/login", a.handleLogin) mux.HandleFunc("/auth/refresh", a.refresh) - mux.HandleFunc("/jobs", a.handleJobs) - mux.HandleFunc("/jobs/status", a.getJobStatus) - mux.HandleFunc("/supervisors/status", a.getSupervisorStatus) - mux.HandleFunc("/supervisors/status/", a.getSupervisorStatusByID) - mux.HandleFunc("/supervisors", a.getAllSupervisors) + mux.HandleFunc("/jobs", a.requireAuth(a.handleJobs)) + mux.HandleFunc("/jobs/status", a.requireAuth(a.getJobStatus)) + mux.HandleFunc("/supervisors/status", a.requireAuth(a.getSupervisorStatus)) + mux.HandleFunc("/supervisors/status/", a.requireAuth(a.getSupervisorStatusByID)) + mux.HandleFunc("/supervisors", a.requireAuth(a.getAllSupervisors)) - // OAuth2 routes + // OAuth mux.HandleFunc("/oauth/authorize", a.handleAuthorize) mux.HandleFunc("/oauth/token", a.handleToken) + mux.HandleFunc("/oauth/callback", a.handleOAuthCallback) a.log.Info("new app initialized", "redis_address", redisAddr, "gpu_type", gpuType, "http_address", a.httpServer.Addr) @@ -73,6 +99,72 @@ func NewApp(redisAddr, gpuType string, log *slog.Logger) *App { return a } +func (a *App) handleOAuthCallback(w http.ResponseWriter, r *http.Request) { + code := r.URL.Query().Get("code") + if code == "" { + http.Error(w, "Code not found", http.StatusBadRequest) + return + } + + tmpl := ` + + + + Copy this code into the cli: {{ .Code }} + + + ` + t, _ := template.New("callback").Parse(tmpl) + t.Execute(w, map[string]string{"Code": code}) +} + +func (a *App) requireAuth(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + cookie, err := r.Cookie("session_id") + if err != nil { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + _, err = a.redisClient.Get(r.Context(), "session:"+cookie.Value).Result() + if err != nil { + http.Error(w, "Unauthorized session", http.StatusUnauthorized) + return + } + + next(w, r) + } +} + +func (a *App) requireAdmin(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + cookie, err := r.Cookie("session_id") + if err != nil { + http.Error(w, "Unauthorized", http.StatusUnauthorized) + return + } + + username, err := a.redisClient.Get(r.Context(), "session:"+cookie.Value).Result() + if err != nil { + http.Error(w, "Unauthorized session", http.StatusUnauthorized) + return + } + + user, err := a.userStore.GetByUsername(r.Context(), username) + if err != nil { + http.Error(w, "User not found", http.StatusUnauthorized) + return + } + + if user.Role != "admin" { + http.Error(w, "Forbidden", http.StatusForbidden) + return + } + + next(w, r) + } +} + func (a *App) Start() error { // Connect to redis if err := a.redisClient.Ping(context.Background()).Err(); err != nil { @@ -158,22 +250,63 @@ func main() { log.Info("all services stopped cleanly") } -func (a *App) login(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - a.log.Info("login handler accessed", "remote_address", r.RemoteAddr) - val, err := a.redisClient.Get(ctx, "some:key").Result() - if errors.Is(err, redis.Nil) { - a.log.Info("redis key not found") - http.Error(w, "redis key not found", http.StatusNotFound) +func (a *App) handleLogin(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + // Render login form + tmpl := ` + + + Login + +

Login

+
+ +
+
+ +
+ + + ` + t, _ := template.New("login").Parse(tmpl) + returnURL := r.URL.Query().Get("return_url") + t.Execute(w, map[string]string{"ReturnURL": returnURL}) return } - if err != nil { - a.log.Error("redis error on login", "err", err) - http.Error(w, "redis error", http.StatusInternalServerError) + + if r.Method == http.MethodPost { + username := r.FormValue("username") + password := r.FormValue("password") + returnURL := r.FormValue("return_url") + + user, err := a.userStore.GetByUsername(r.Context(), username) + if err != nil || !a.userStore.VerifyPassword(user, password) { + http.Error(w, "Invalid credentials", http.StatusUnauthorized) + return + } + + // Generate session + b := make([]byte, 32) + rand.Read(b) + sessionID := hex.EncodeToString(b) + + // Store session + a.redisClient.Set(r.Context(), "session:"+sessionID, user.ID, 24*time.Hour) + + http.SetCookie(w, &http.Cookie{ + Name: "session_id", + Value: sessionID, + Path: "/", + HttpOnly: true, + Expires: time.Now().Add(24 * time.Hour), + }) + + if returnURL == "" { + returnURL = "/" + } + http.Redirect(w, r, returnURL, http.StatusFound) return } - a.log.Info("login success", "remote_address", r.RemoteAddr) - fmt.Fprintf(w, "login page; redis says: %q\n", val) } func (a *App) refresh(w http.ResponseWriter, r *http.Request) { diff --git a/src/go.mod b/src/go.mod index 4d1f7e1..18a3c09 100644 --- a/src/go.mod +++ b/src/go.mod @@ -4,9 +4,9 @@ go 1.24.3 require ( github.com/go-oauth2/oauth2/v4 v4.5.4 - github.com/go-oauth2/redis/v4 v4.1.1 github.com/go-redis/redis/v8 v8.0.0-beta.5 github.com/redis/go-redis/v9 v9.10.0 + golang.org/x/crypto v0.48.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 gopkg.in/yaml.v3 v3.0.1 ) @@ -15,11 +15,10 @@ require ( github.com/cespare/xxhash v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/fsnotify/fsnotify v1.4.9 // indirect github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/google/uuid v1.1.1 // indirect - github.com/json-iterator/go v1.1.10 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.1 // indirect + github.com/nxadm/tail v1.4.4 // indirect github.com/tidwall/btree v0.0.0-20191029221954-400434d76274 // indirect github.com/tidwall/buntdb v1.1.2 // indirect github.com/tidwall/gjson v1.12.1 // indirect @@ -30,4 +29,5 @@ require ( github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563 // indirect go.opentelemetry.io/otel v0.6.0 // indirect google.golang.org/grpc v1.29.1 // indirect + gopkg.in/yaml.v2 v2.3.0 // indirect ) diff --git a/src/go.sum b/src/go.sum index 3617ea8..9f945ae 100644 --- a/src/go.sum +++ b/src/go.sum @@ -1,5 +1,4 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/DataDog/sketches-go v0.0.0-20190923095040-43f19ad77ff7 h1:qELHH0AWCvf98Yf+CNIJx9vOZOfHFDDzgDRYsnNk/vs= github.com/DataDog/sketches-go v0.0.0-20190923095040-43f19ad77ff7/go.mod h1:Q5DbzQ+3AkgGwymQO7aZFNP7ns2lZKGtvRBzRXfdi60= @@ -7,7 +6,6 @@ github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= -github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/benbjohnson/clock v1.0.0/go.mod h1:bGMdMPoPVvcYyt1gHDf4J2KE153Yf9BuiUKYMaxlTDM= @@ -25,7 +23,6 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-rendezvous v0.0.0-20200609043717-5ab96a526299/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= @@ -33,7 +30,6 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -41,32 +37,20 @@ github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/gavv/httpexpect v2.0.0+incompatible h1:1X9kcRshkSKEjNJJxX9Y9mQ5BRfbxU5kORdjhlA1yX8= github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= -github.com/go-oauth2/oauth2/v4 v4.1.0/go.mod h1:+rsyi0o/ZbSfhL/3Xr/sAtL4brS+IdGj86PHVlPjE+4= github.com/go-oauth2/oauth2/v4 v4.5.4 h1:YjI0tmGW8oxVhn9QSBIxlr641QugWrJY5UWa6XmLcW0= github.com/go-oauth2/oauth2/v4 v4.5.4/go.mod h1:BXiOY+QZtZy2ewbsGk2B5P8TWmtz/Rf7ES5ZttQFxfQ= -github.com/go-oauth2/redis/v4 v4.1.1 h1:uYLGPbAEZ3tb2Qg+BHzrtMHbJ7NeX6S9Ol0+iYyBF5E= -github.com/go-oauth2/redis/v4 v4.1.1/go.mod h1:cYNT5bLEwCnrFXqSbWDvxXzfTaF/fKMf1XoRVFwBPrc= github.com/go-redis/redis/v8 v8.0.0-beta.5 h1:i4Rhw1v2H9HTWO05wsKdpGpFYFU9OW+foa2GuDIjbBA= github.com/go-redis/redis/v8 v8.0.0-beta.5/go.mod h1:Mm9EH/5UMRx680UIryN6rd5XFn/L7zORPqLV+1D5thQ= -github.com/go-session/session v3.1.2+incompatible/go.mod h1:8B3iivBQjrz/JtC68Np2T1yBBLxTan3mn/3OM0CyRt0= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= @@ -74,7 +58,6 @@ github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0= github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= @@ -82,13 +65,8 @@ github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= -github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k= -github.com/klauspost/compress v1.10.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.15.0 h1:xqfchp4whNFxn5A4XFyyYtitiWI8Hy5EW59jEwcyL6U= github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= @@ -96,25 +74,15 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/moul/http2curl v1.0.0 h1:dRMWoAtb+ePxMlLkrCbAqh4TlPHXvoGUSQ323/9Zahs= github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.13.0 h1:M76yO2HkZASFjXL0HSoZJ1AYEmQxNJmY41Jx1zNUq1Y= github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1lskyM0= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1 h1:o0+MgICZLuZ7xjH7Vx6zS/zcu93/BEp1VwkIW1mEXCE= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/opentracing/opentracing-go v1.1.1-0.20190913142402-a7454ce5950e/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= @@ -123,10 +91,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/redis/go-redis/v9 v9.10.0 h1:FxwK3eV8p/CQa0Ch276C7u2d0eNC9kCmAYQ7mCXCzVs= github.com/redis/go-redis/v9 v9.10.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= -github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v1.1.0 h1:MkTeG1DMwsrdH7QtLXy5W+fUxWq+vmb6cLmyJ7aRtF0= github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= @@ -134,9 +100,7 @@ github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9 github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/tidwall/btree v0.0.0-20191029221954-400434d76274 h1:G6Z6HvJuPjG6XfNGi/feOATzeJrfgTNJY+rGrHbA04E= @@ -144,7 +108,6 @@ github.com/tidwall/btree v0.0.0-20191029221954-400434d76274/go.mod h1:huei1BkDWJ github.com/tidwall/buntdb v1.1.2 h1:noCrqQXL9EKMtcdwJcmuVKSEjqu1ua99RHHgbLTEHRo= github.com/tidwall/buntdb v1.1.2/go.mod h1:xAzi36Hir4FarpSHyfuZ6JzPJdjRZ8QlLZSntE2mqlI= github.com/tidwall/gjson v1.3.4/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls= -github.com/tidwall/gjson v1.6.0/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls= github.com/tidwall/gjson v1.12.1 h1:ikuZsLdhr8Ws0IdROXUS1Gi4v9Z4pGqpX/CvJkxvfpo= github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb h1:5NSYaAdrnblKByzd7XByQEJVT8+9v0W/tIY0Oo4OwrE= @@ -153,7 +116,6 @@ github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0 github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= -github.com/tidwall/pretty v1.0.1/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e h1:+NL1GDIUOKxVfbp2KoJQD9cTQ6dyP2co9q4yzmT9FZo= @@ -162,10 +124,8 @@ github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563 h1:Otn9S136ELckZ github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563/go.mod h1:mLqSmt7Dv/CNneF2wfcChfN1rvapyQr01LGKnKex0DQ= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.14.0/go.mod h1:ol1PCaL0dX20wC0htZ7sYCsvCYmrouYra0zHzaclZhE= github.com/valyala/fasthttp v1.34.0 h1:d3AAQJ2DRcxJYHm7OXNXtXt2as1vMDfxeIcFvhmGGm4= github.com/valyala/fasthttp v1.34.0/go.mod h1:epZA5N+7pY6ZaEKRmstzOuYJx9HI8DI1oaCGZpdH4h0= -github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= @@ -178,11 +138,11 @@ github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCO github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= -github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= go.opentelemetry.io/otel v0.6.0 h1:+vkHm/XwJ7ekpISV2Ixew93gCrxTbuwTF5rSewnLLgw= go.opentelemetry.io/otel v0.6.0/go.mod h1:jzBIgIzK43Iu1BpDAXwqOd6UPsSAk+ewVZ5ofSXw4Ek= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -190,51 +150,36 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc= -golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae h1:Ih9Yo4hSPImZOpfGuA4bR/ORKTAbhZo2AbWNRCnevdo= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20191009194640-548a555dbc03 h1:4HYDjxeNXAOTv3o1N2tjo8UUSlhQgAD52FVkwxnWgM8= @@ -245,11 +190,6 @@ google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQ google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.29.1 h1:EC2SB8S04d2r73uptxphDSUG+kTKVgjRPF+N3xpxRB4= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -262,12 +202,9 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkep gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/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= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/src/oauth.go b/src/oauth.go index f23d377..492a42e 100644 --- a/src/oauth.go +++ b/src/oauth.go @@ -1,8 +1,11 @@ package main import ( + "context" + "fmt" "log/slog" "net/http" + "net/url" "time" "github.com/go-oauth2/oauth2/v4" @@ -12,7 +15,6 @@ import ( "github.com/go-oauth2/oauth2/v4/models" "github.com/go-oauth2/oauth2/v4/server" "github.com/go-oauth2/oauth2/v4/store" - oredis "github.com/go-oauth2/redis/v4" "github.com/go-redis/redis/v8" ) @@ -21,18 +23,17 @@ type OAuthServer struct { Manager *manage.Manager } -func NewOAuthServer(redisAddr string, client *redis.Client, log *slog.Logger) (*OAuthServer, error) { +func NewOAuthServer(redisAddr string, client *redis.Client, log *slog.Logger, userStore UserStore) (*OAuthServer, error) { var tokenStore oauth2.TokenStore if redisAddr == "memory" { tokenStore, _ = store.NewMemoryTokenStore() } else { - tokenStore = oredis.NewRedisStore(&redis.Options{Addr: redisAddr}) + tokenStore = NewRedisTokenStore(client) } clientStore := store.NewClientStore() err := clientStore.Set("cli", &models.Client{ ID: "cli", - Secret: "demo-client-secret", Domain: "http://localhost:3000", }) if err != nil { @@ -55,7 +56,23 @@ func NewOAuthServer(redisAddr string, client *redis.Client, log *slog.Logger) (* srv.SetClientInfoHandler(server.ClientFormHandler) srv.SetUserAuthorizationHandler(func(w http.ResponseWriter, r *http.Request) (userID string, err error) { - return "test-user", nil + cookie, err := r.Cookie("session_id") + if err != nil { + redirectURL := "/auth/login?return_url=" + url.QueryEscape(r.URL.String()) + http.Redirect(w, r, redirectURL, http.StatusFound) + return "", nil + } + + key := fmt.Sprintf("session:%s", cookie.Value) + userID, err = client.Get(context.Background(), key).Result() + if err != nil { + // Invalid session + redirectURL := "/auth/login?return_url=" + url.QueryEscape(r.URL.String()) + http.Redirect(w, r, redirectURL, http.StatusFound) + return "", nil + } + + return userID, nil }) srv.SetInternalErrorHandler(func(err error) (re *errors.Response) { diff --git a/src/oauth_integration_test.go b/src/oauth_integration_test.go deleted file mode 100644 index ff1fa51..0000000 --- a/src/oauth_integration_test.go +++ /dev/null @@ -1,114 +0,0 @@ -package main - -import ( - "crypto/sha256" - "encoding/base64" - "encoding/json" - "fmt" - "io" - "net/http" - "net/http/httptest" - "net/url" - "testing" - "time" - - log2 "mist/multilogger" -) - -// Helper for PKCE -func generateCodeChallenge(verifier string) string { - s := sha256.New() - s.Write([]byte(verifier)) - return base64.RawURLEncoding.EncodeToString(s.Sum(nil)) -} - -func TestOAuthFlow(t *testing.T) { - // Setup app - cfg, err := log2.GetLogConfig() - if err != nil { - t.Fatalf("Failed to get log config: %v", err) - } - log, err := log2.CreateLogger("test", &cfg) - if err != nil { - t.Fatalf("Failed to create logger: %v", err) - } - - app := NewApp("memory", "TestGPU", log) - - // Create test server - ts := httptest.NewServer(app.httpServer.Handler) - defer ts.Close() - - // 1. Authorize Request - clientID := "demo-client-id" - verifier := "some-random-secret-verifier-string-1234567890" // High entropy string - challenge := generateCodeChallenge(verifier) - redirectURI := "http://localhost:3000" - - authURL := fmt.Sprintf("%s/oauth/authorize?client_id=%s&response_type=code&redirect_uri=%s&code_challenge=%s&code_challenge_method=S256", - ts.URL, clientID, url.QueryEscape(redirectURI), challenge) - - t.Logf("Requesting authorization: %s", authURL) - - client := ts.Client() - client.Timeout = 5 * time.Second - // Disable redirect following to inspect the location - client.CheckRedirect = func(req *http.Request, via []*http.Request) error { - return http.ErrUseLastResponse - } - - resp, err := client.Get(authURL) - if err != nil { - t.Fatalf("Failed to GET authorize: %v", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusFound { - body, _ := io.ReadAll(resp.Body) - t.Fatalf("Expected 302 Found, got %d. Body: %s", resp.StatusCode, body) - } - - loc, err := resp.Location() - if err != nil { - t.Fatalf("Failed to get location: %v", err) - } - - code := loc.Query().Get("code") - if code == "" { - t.Fatalf("No code in redirect location: %s", loc.String()) - } - t.Logf("Got auth code: %s", code) - - // 2. Token Exchange - tokenURL := fmt.Sprintf("%s/oauth/token", ts.URL) - data := url.Values{} - data.Set("grant_type", "authorization_code") - data.Set("client_id", clientID) - data.Set("client_secret", "demo-client-secret") - data.Set("code", code) - data.Set("redirect_uri", redirectURI) - data.Set("code_verifier", verifier) - - t.Logf("Exchanging token at: %s", tokenURL) - - resp, err = client.PostForm(tokenURL, data) - if err != nil { - t.Fatalf("Failed to POST token: %v", err) - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - body, _ := io.ReadAll(resp.Body) - t.Fatalf("Expected 200 OK, got %d. Body: %s", resp.StatusCode, body) - } - - var tokenResp map[string]interface{} - if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil { - t.Fatalf("Failed to decode token response: %v", err) - } - - if _, ok := tokenResp["access_token"]; !ok { - t.Errorf("No access_token in response: %v", tokenResp) - } - t.Logf("Token response: %v", tokenResp) -} diff --git a/src/redis_store.go b/src/redis_store.go new file mode 100644 index 0000000..112b75c --- /dev/null +++ b/src/redis_store.go @@ -0,0 +1,81 @@ +package main + +import ( + "context" + "encoding/json" + + "github.com/go-oauth2/oauth2/v4" + "github.com/go-oauth2/oauth2/v4/models" + "github.com/go-redis/redis/v8" +) + +type RedisTokenStore struct { + client *redis.Client +} + +func NewRedisTokenStore(client *redis.Client) *RedisTokenStore { + return &RedisTokenStore{ + client: client, + } +} + +func (s *RedisTokenStore) Create(ctx context.Context, info oauth2.TokenInfo) error { + data, err := json.Marshal(info) + if err != nil { + return err + } + + pipe := s.client.Pipeline() + if code := info.GetCode(); code != "" { + pipe.Set(ctx, "oauth:code:"+code, data, info.GetCodeExpiresIn()) + } + if access := info.GetAccess(); access != "" { + pipe.Set(ctx, "oauth:access:"+access, data, info.GetAccessExpiresIn()) + } + if refresh := info.GetRefresh(); refresh != "" { + pipe.Set(ctx, "oauth:refresh:"+refresh, data, info.GetRefreshExpiresIn()) + } + + _, err = pipe.Exec(ctx) + return err +} + +func (s *RedisTokenStore) RemoveByCode(ctx context.Context, code string) error { + return s.client.Del(ctx, "oauth:code:"+code).Err() +} + +func (s *RedisTokenStore) RemoveByAccess(ctx context.Context, access string) error { + return s.client.Del(ctx, "oauth:access:"+access).Err() +} + +func (s *RedisTokenStore) RemoveByRefresh(ctx context.Context, refresh string) error { + return s.client.Del(ctx, "oauth:refresh:"+refresh).Err() +} + +func (s *RedisTokenStore) GetByCode(ctx context.Context, code string) (oauth2.TokenInfo, error) { + return s.get(ctx, "oauth:code:"+code) +} + +func (s *RedisTokenStore) GetByAccess(ctx context.Context, access string) (oauth2.TokenInfo, error) { + return s.get(ctx, "oauth:access:"+access) +} + +func (s *RedisTokenStore) GetByRefresh(ctx context.Context, refresh string) (oauth2.TokenInfo, error) { + return s.get(ctx, "oauth:refresh:"+refresh) +} + +func (s *RedisTokenStore) get(ctx context.Context, key string) (oauth2.TokenInfo, error) { + result, err := s.client.Get(ctx, key).Result() + if err == redis.Nil { + return nil, nil + } + if err != nil { + return nil, err + } + + var tm models.Token + if err := json.Unmarshal([]byte(result), &tm); err != nil { + return nil, err + } + return &tm, nil +} diff --git a/src/user.go b/src/user.go new file mode 100644 index 0000000..5910763 --- /dev/null +++ b/src/user.go @@ -0,0 +1,77 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + + "golang.org/x/crypto/bcrypt" + + "github.com/go-redis/redis/v8" +) + +type User struct { + ID string `json:"id"` + Username string `json:"username"` + PasswordHash string `json:"password_hash"` + Role string `json:"role"` +} + +type UserStore interface { + Create(ctx context.Context, user *User) error + GetByUsername(ctx context.Context, username string) (*User, error) + VerifyPassword(user *User, password string) bool +} + +type RedisUserStore struct { + client *redis.Client +} + +func NewRedisUserStore(client *redis.Client) *RedisUserStore { + return &RedisUserStore{client: client} +} + +func (s *RedisUserStore) Create(ctx context.Context, user *User) error { + key := fmt.Sprintf("user:%s", user.Username) + exists, err := s.client.Exists(ctx, key).Result() + if err != nil { + return err + } + if exists > 0 { + return fmt.Errorf("user already exists") + } + + data, err := json.Marshal(user) + if err != nil { + return err + } + + return s.client.Set(ctx, key, data, 0).Err() +} + +func (s *RedisUserStore) GetByUsername(ctx context.Context, username string) (*User, error) { + key := fmt.Sprintf("user:%s", username) + val, err := s.client.Get(ctx, key).Result() + if err != nil { + return nil, err + } + + var user User + if err := json.Unmarshal([]byte(val), &user); err != nil { + return nil, err + } + return &user, nil +} + +func (s *RedisUserStore) VerifyPassword(user *User, password string) bool { + err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password)) + return err == nil +} + +func (u *User) ToSafe() map[string]interface{} { + return map[string]interface{}{ + "id": u.ID, + "username": u.Username, + "role": u.Role, + } +} From a4c32c1f08aa348194d2f822e74bf56399ce5638 Mon Sep 17 00:00:00 2001 From: Oliver Kwun-Morfitt Date: Mon, 16 Feb 2026 17:34:52 -0500 Subject: [PATCH 3/4] Parsed refresh_token, expires, and token type from response and updated accesstoken struct --- cli/cmd/auth_login.go | 9 ++++++++- cli/cmd/root.go | 4 +++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/cli/cmd/auth_login.go b/cli/cmd/auth_login.go index 628a67d..f68ca92 100644 --- a/cli/cmd/auth_login.go +++ b/cli/cmd/auth_login.go @@ -15,6 +15,7 @@ import ( "path/filepath" "runtime" "strings" + "time" ) const ( @@ -56,7 +57,7 @@ func openUrl(url string) error { return cmd.Start() } -func saveTokenToConfig(ctx *AppContext, token string) error { +func saveTokenToConfig(ctx *AppContext, token string, refreshToken string) error { configPath := defaultConfigPath() if err := os.MkdirAll(filepath.Dir(configPath), 0o755); err != nil { return fmt.Errorf("failed to create config directory: %w", err) @@ -137,5 +138,11 @@ func (l *LoginCmd) Run(ctx *AppContext) error { return fmt.Errorf("invalid token response: missing access_token") } + refreshToken, _ := tokenResp["refresh_token"].(string) + expiresIn, _ := tokenResp["expires_int"].(float64) + tokenType, _ := tokenResp["token_type"].(string) + + expiresAt := time.Now().Add(time.Duration(expiresIn) * time.Second) + return saveTokenToConfig(ctx, accessToken) } diff --git a/cli/cmd/root.go b/cli/cmd/root.go index 4221b35..ae2990d 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -10,7 +10,9 @@ import ( ) type Config struct { - AccessToken string `json:"access_token"` + AccessToken string `json:"access_token"` + RefreshToken string + ExpiresAt time.Time // maybe APIBaseURL, etc. } From 36996e6fb62c82afb39ae7469ea2278faa0e80fc Mon Sep 17 00:00:00 2001 From: Oliver Kwun-Morfitt Date: Mon, 16 Feb 2026 23:07:49 -0500 Subject: [PATCH 4/4] Finished saving oath tokens and added auth check + refresh call on authenticated commands --- cli/cmd/auth_login.go | 51 +++++++++++++++++++++++++++++++++++++++---- cli/cmd/job_list.go | 4 ++++ cli/cmd/job_status.go | 4 ++++ cli/cmd/job_submit.go | 4 +++- cli/cmd/root.go | 6 ++--- 5 files changed, 61 insertions(+), 8 deletions(-) diff --git a/cli/cmd/auth_login.go b/cli/cmd/auth_login.go index f68ca92..88a19b2 100644 --- a/cli/cmd/auth_login.go +++ b/cli/cmd/auth_login.go @@ -57,7 +57,7 @@ func openUrl(url string) error { return cmd.Start() } -func saveTokenToConfig(ctx *AppContext, token string, refreshToken string) error { +func saveTokenToConfig(ctx *AppContext, token string, refreshToken string, expiresAt time.Time) error { configPath := defaultConfigPath() if err := os.MkdirAll(filepath.Dir(configPath), 0o755); err != nil { return fmt.Errorf("failed to create config directory: %w", err) @@ -67,6 +67,8 @@ func saveTokenToConfig(ctx *AppContext, token string, refreshToken string) error ctx.Config = &Config{} } ctx.Config.AccessToken = token + ctx.Config.RefreshToken = refreshToken + ctx.Config.ExpiresAt = expiresAt data, err := json.MarshalIndent(ctx.Config, "", " ") if err != nil { @@ -79,6 +81,48 @@ func saveTokenToConfig(ctx *AppContext, token string, refreshToken string) error return nil } +func (ctx *AppContext) RefreshAccessToken() error { + data := url.Values{} + data.Set("grant_type", "refresh_token") + data.Set("client_id", clientID) + data.Set("refresh_token", ctx.Config.RefreshToken) + + resp, err := http.PostForm(tokenUrl, data) + if err != nil { + return fmt.Errorf("failed to refresh token: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("token refresh failed: %s", body) + } + + var tokenResp map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil { + return fmt.Errorf("failed to decode token response: %w", err) + } + + accessToken, _ := tokenResp["access_token"].(string) + refreshToken, _ := tokenResp["refresh_token"].(string) + expiresIn, _ := tokenResp["expires_in"].(float64) + expiresAt := time.Now().Add(time.Duration(expiresIn) * time.Second) + + return saveTokenToConfig(ctx, accessToken, refreshToken, expiresAt) +} + +func (ctx *AppContext) CheckValidToken() error { + if ctx.Config == nil || ctx.Config.AccessToken == "" { + return fmt.Errorf("not logged in") + } + + if time.Now().Add(30 * time.Second).After(ctx.Config.ExpiresAt) { + return ctx.RefreshAccessToken() + } + + return nil +} + func (l *LoginCmd) Run(ctx *AppContext) error { verifier, err := generateRandomString(32) if err != nil { @@ -139,10 +183,9 @@ func (l *LoginCmd) Run(ctx *AppContext) error { } refreshToken, _ := tokenResp["refresh_token"].(string) - expiresIn, _ := tokenResp["expires_int"].(float64) - tokenType, _ := tokenResp["token_type"].(string) + expiresIn, _ := tokenResp["expires_in"].(float64) expiresAt := time.Now().Add(time.Duration(expiresIn) * time.Second) - return saveTokenToConfig(ctx, accessToken) + return saveTokenToConfig(ctx, accessToken, refreshToken, expiresAt) } diff --git a/cli/cmd/job_list.go b/cli/cmd/job_list.go index 9e4787d..0a25a67 100644 --- a/cli/cmd/job_list.go +++ b/cli/cmd/job_list.go @@ -21,6 +21,10 @@ type Job struct { func (l *ListCmd) Run(ctx *AppContext) error { // Mock data - pull from API in real implementation + if err := ctx.CheckValidToken(); err != nil { + return err + } + jobs := []Job{ { ID: "ID:1", diff --git a/cli/cmd/job_status.go b/cli/cmd/job_status.go index 20634c8..9238113 100644 --- a/cli/cmd/job_status.go +++ b/cli/cmd/job_status.go @@ -13,6 +13,10 @@ type JobStatusCmd struct { func (j *JobStatusCmd) Run(ctx *AppContext) error { // Mock data - pull from API in real implementation + if err := ctx.CheckValidToken(); err != nil { + return err + } + jobs := []Job{{ ID: "ID:1", Name: "docker_container_name_1", diff --git a/cli/cmd/job_submit.go b/cli/cmd/job_submit.go index 22b793f..df0f67a 100644 --- a/cli/cmd/job_submit.go +++ b/cli/cmd/job_submit.go @@ -15,7 +15,9 @@ type JobSubmitCmd struct { func (j *JobSubmitCmd) Run(ctx *AppContext) error { // mist job submit