From 7b8d15a2c9896b5bce117f5edfaa89783d77dd5e Mon Sep 17 00:00:00 2001 From: Vyacheslav1557 Date: Tue, 25 Feb 2025 18:33:15 +0500 Subject: [PATCH] feat(user): migrate from gRPC to REST --- Makefile | 16 +- cmd/ms-auth-proxy/main.go | 144 ------ cmd/ms-auth/main.go | 110 ++--- config.yaml | 5 + go.mod | 58 ++- go.sum | 175 ++++++- internal/models/session.go | 76 ++-- internal/models/user.go | 149 +++++- internal/users/delivery.go | 24 +- internal/users/delivery/grpc/handlers.go | 283 ------------ internal/users/delivery/grpc/handlers_test.go | 428 ------------------ internal/users/delivery/rest/handlers.go | 265 +++++++++++ internal/users/delivery/rest/middleware.go | 74 +++ internal/users/repository.go | 3 +- internal/users/repository/pg_repository.go | 35 +- .../users/repository/valkey_repository.go | 73 ++- internal/users/usecase.go | 4 +- internal/users/usecase/usecase.go | 186 ++++---- pkg/errors.go | 17 +- proto | 2 +- 20 files changed, 977 insertions(+), 1150 deletions(-) delete mode 100644 cmd/ms-auth-proxy/main.go create mode 100644 config.yaml delete mode 100644 internal/users/delivery/grpc/handlers.go delete mode 100644 internal/users/delivery/grpc/handlers_test.go create mode 100644 internal/users/delivery/rest/handlers.go create mode 100644 internal/users/delivery/rest/middleware.go diff --git a/Makefile b/Makefile index c2c236b..a9e3195 100644 --- a/Makefile +++ b/Makefile @@ -1,19 +1,9 @@ tag = latest gen: - @protoc --proto_path=proto --go_opt=paths=source_relative \ - --go_out=proto --go-grpc_out=proto --grpc-gateway_out=proto \ - proto/user/v1/user.proto -gen-openapi: - @protoc --proto_path=proto --openapi_out=proto/user/v1 \ - proto/user/v1/user.proto -dev: - @make gen + @oapi-codegen --config=config.yaml ./proto/user/v1/openapi.yaml +dev: gen @go run cmd/ms-auth/main.go -proxy: - @make gen - @go run cmd/ms-auth-proxy/main.go -build: - @make gen +build: gen @docker build . -t ms-auth:${tag} @#docker push ms-auth:${tag} \ No newline at end of file diff --git a/cmd/ms-auth-proxy/main.go b/cmd/ms-auth-proxy/main.go deleted file mode 100644 index 294be77..0000000 --- a/cmd/ms-auth-proxy/main.go +++ /dev/null @@ -1,144 +0,0 @@ -package main - -import ( - "context" - "errors" - "fmt" - "git.sch9.ru/new_gate/ms-auth/config" - delivery "git.sch9.ru/new_gate/ms-auth/internal/users/delivery/grpc" - userv1gw "git.sch9.ru/new_gate/ms-auth/proto/user/v1" - "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" - "github.com/ilyakaznacheev/cleanenv" - "github.com/rs/cors" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" - "google.golang.org/grpc/metadata" - "net/http" - "os" - "os/signal" - "syscall" - "time" -) - -func CustomOutgoingHeaderMatcher(key string) (string, bool) { - if key == delivery.SessionHeaderName { - return "Set-Cookie", true - } - - return fmt.Sprintf("%s%s", runtime.MetadataHeaderPrefix, key), true -} - -func CustomIncomingHeaderMatcher(key string) (string, bool) { - if key == "Cookie" { - return "Cookie", true - } - - return fmt.Sprintf("%s%s", runtime.MetadataPrefix, key), true -} - -func UnaryClientInterceptor( - ctx context.Context, - method string, - req, reply interface{}, - cc *grpc.ClientConn, - invoker grpc.UnaryInvoker, - opts ...grpc.CallOption, -) error { - md, ok := metadata.FromOutgoingContext(ctx) - - hasCookie := ok && len(md.Get("Cookie")) > 0 - - if hasCookie { - cookies, err := http.ParseCookie(md.Get("Cookie")[0]) - if err != nil { - return err - } - - if len(cookies) != 1 { - return errors.New("invalid cookie") - } - - md.Set(delivery.SessionHeaderName, cookies[0].Value) - } - - ctx = metadata.NewOutgoingContext(ctx, md) - - err := invoker(ctx, method, req, reply, cc, opts...) - if err != nil { - return err - } - - for _, o := range opts { - header, ok := o.(grpc.HeaderCallOption) - if !ok { - continue - } - - values := header.HeaderAddr.Get(delivery.SessionHeaderName) - - if len(values) != 1 { - continue - } - - sessionId := values[0] - - cookie := http.Cookie{ - Name: "SESSIONID", - Value: sessionId, - Path: "/", - HttpOnly: true, - } - - if len(sessionId) == 0 { - cookie.Expires = time.Unix(0, 0) - } else { - cookie.MaxAge = 3600 - } - - header.HeaderAddr.Set(delivery.SessionHeaderName, cookie.String()) - } - - return nil -} - -func main() { - var cfg config.Config - err := cleanenv.ReadConfig(".env", &cfg) - if err != nil { - panic(fmt.Sprintf("error reading config: %s", err.Error())) - } - - ctx := context.Background() - ctx, cancel := context.WithCancel(ctx) - defer cancel() - - mux := runtime.NewServeMux(runtime.WithOutgoingHeaderMatcher(CustomOutgoingHeaderMatcher), runtime.WithIncomingHeaderMatcher(CustomIncomingHeaderMatcher)) - opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithChainUnaryInterceptor(UnaryClientInterceptor)} - err = userv1gw.RegisterUserServiceHandlerFromEndpoint(ctx, mux, cfg.Address, opts) - if err != nil { - panic(err) - } - - c := cors.New(cors.Options{ - AllowedOrigins: []string{"http://*", "https://*"}, - AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, - AllowedHeaders: []string{"Content-Type", "Set-Cookie", "Credentials"}, - ExposedHeaders: []string{"Link"}, - AllowCredentials: true, - }) - - go func() { - err = http.ListenAndServe(cfg.ProxyAddress, c.Handler(mux)) - if err != nil { - panic(err) - } - }() - - fmt.Println("server proxy started") - - stop := make(chan os.Signal, 1) - signal.Notify(stop, syscall.SIGTERM, syscall.SIGINT) - - <-stop - return -} diff --git a/cmd/ms-auth/main.go b/cmd/ms-auth/main.go index 3e7fd99..8ee4c62 100644 --- a/cmd/ms-auth/main.go +++ b/cmd/ms-auth/main.go @@ -5,17 +5,16 @@ import ( "fmt" "git.sch9.ru/new_gate/ms-auth/config" "git.sch9.ru/new_gate/ms-auth/internal/models" - usersDelivery "git.sch9.ru/new_gate/ms-auth/internal/users/delivery/grpc" + "git.sch9.ru/new_gate/ms-auth/internal/users/delivery/rest" usersRepository "git.sch9.ru/new_gate/ms-auth/internal/users/repository" usersUseCase "git.sch9.ru/new_gate/ms-auth/internal/users/usecase" "git.sch9.ru/new_gate/ms-auth/pkg" - "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/logging" + userv1 "git.sch9.ru/new_gate/ms-auth/proto/user/v1" + "github.com/gofiber/fiber/v2" + fiberlogger "github.com/gofiber/fiber/v2/middleware/logger" "github.com/ilyakaznacheev/cleanenv" _ "github.com/jackc/pgx/v5/stdlib" "go.uber.org/zap" - "google.golang.org/grpc" - "google.golang.org/grpc/reflection" - "net" "os" "os/signal" "syscall" @@ -23,42 +22,42 @@ import ( // InterceptorLogger adapts zap logger to interceptor logger. // This code is simple enough to be copied and not imported. -func InterceptorLogger(l *zap.Logger) logging.Logger { - return logging.LoggerFunc(func(ctx context.Context, lvl logging.Level, msg string, fields ...any) { - f := make([]zap.Field, 0, len(fields)/2) - - for i := 0; i < len(fields); i += 2 { - key := fields[i] - value := fields[i+1] - - switch v := value.(type) { - case string: - f = append(f, zap.String(key.(string), v)) - case int: - f = append(f, zap.Int(key.(string), v)) - case bool: - f = append(f, zap.Bool(key.(string), v)) - default: - f = append(f, zap.Any(key.(string), v)) - } - } - - logger := l.WithOptions(zap.AddCallerSkip(1)).With(f...) - - switch lvl { - case logging.LevelDebug: - logger.Debug(msg) - case logging.LevelInfo: - logger.Info(msg) - case logging.LevelWarn: - logger.Warn(msg) - case logging.LevelError: - logger.Error(msg) - default: - panic(fmt.Sprintf("unknown level %v", lvl)) - } - }) -} +//func InterceptorLogger(l *zap.Logger) fiber.Handler { +// return func(ctx context.Context, lvl logging.Level, msg string, fields ...any) { +// f := make([]zap.Field, 0, len(fields)/2) +// +// for i := 0; i < len(fields); i += 2 { +// key := fields[i] +// value := fields[i+1] +// +// switch v := value.(type) { +// case string: +// f = append(f, zap.String(key.(string), v)) +// case int: +// f = append(f, zap.Int(key.(string), v)) +// case bool: +// f = append(f, zap.Bool(key.(string), v)) +// default: +// f = append(f, zap.Any(key.(string), v)) +// } +// } +// +// logger := l.WithOptions(zap.AddCallerSkip(1)).With(f...) +// +// switch lvl { +// case logging.LevelDebug: +// logger.Debug(msg) +// case logging.LevelInfo: +// logger.Info(msg) +// case logging.LevelWarn: +// logger.Warn(msg) +// case logging.LevelError: +// logger.Error(msg) +// default: +// panic(fmt.Sprintf("unknown level %v", lvl)) +// } +// }) +//} func main() { var cfg config.Config @@ -101,22 +100,25 @@ func main() { sessionRepo := usersRepository.NewValkeyRepository(vk) userUC := usersUseCase.NewUseCase(userRepo, sessionRepo, cfg) - gserver := grpc.NewServer(grpc.ChainUnaryInterceptor( - logging.UnaryServerInterceptor(InterceptorLogger(logger)), - )) - defer gserver.GracefulStop() + server := fiber.New() - usersDelivery.NewUserHandlers(gserver, userUC) - reflection.Register(gserver) - - ln, err := net.Listen("tcp", cfg.Address) - if err != nil { - panic(err) - } + userv1.RegisterHandlersWithOptions(server, rest.NewUserHandlers(userUC, cfg.JWTSecret), userv1.FiberServerOptions{ + Middlewares: []userv1.MiddlewareFunc{ + fiberlogger.New(), + rest.AuthMiddleware(cfg.JWTSecret, userUC), + //cors.New(cors.Config{ + // AllowOrigins: "http://localhost:3000", + // AllowMethods: "GET,POST,PUT,DELETE,OPTIONS", + // AllowHeaders: "Content-Type,Set-Cookie,Credentials", + // AllowCredentials: true, + //}), + }, + }) go func() { - if err = gserver.Serve(ln); err != nil { - panic(err) + err := server.Listen(cfg.Address) + if err != nil { + logger.Fatal(fmt.Sprintf("error starting server: %s", err.Error())) } }() diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000..3228d1d --- /dev/null +++ b/config.yaml @@ -0,0 +1,5 @@ +package: userv1 +generate: + fiber-server: true + models: true +output: ./proto/user/v1/user.go \ No newline at end of file diff --git a/go.mod b/go.mod index 022d1bb..ea76000 100644 --- a/go.mod +++ b/go.mod @@ -4,42 +4,76 @@ go 1.23.2 require ( github.com/DATA-DOG/go-sqlmock v1.5.2 + github.com/gofiber/fiber/v2 v2.52.6 github.com/golang-jwt/jwt/v4 v4.5.1 github.com/google/uuid v1.6.0 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.2.0 - github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 github.com/ilyakaznacheev/cleanenv v1.5.0 github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 + github.com/oapi-codegen/runtime v1.1.1 + github.com/open-policy-agent/opa v1.1.0 github.com/stretchr/testify v1.10.0 github.com/valkey-io/valkey-go v1.0.47 github.com/valkey-io/valkey-go/mock v1.0.47 go.uber.org/mock v0.4.0 go.uber.org/zap v1.27.0 - golang.org/x/crypto v0.31.0 - google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 - google.golang.org/grpc v1.68.0 - google.golang.org/protobuf v1.35.2 + golang.org/x/crypto v0.32.0 + google.golang.org/grpc v1.70.0 + google.golang.org/protobuf v1.36.3 ) require ( + github.com/OneOfOne/xxhash v1.2.8 // indirect + github.com/agnivade/levenshtein v1.2.0 // indirect + github.com/andybalholm/brotli v1.1.0 // indirect + github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/go-chi/chi/v5 v5.2.1 // indirect + github.com/go-ini/ini v1.67.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/gobwas/glob v0.2.3 // indirect + github.com/gorilla/mux v1.8.1 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/klauspost/compress v1.17.11 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rogpeppe/go-internal v1.12.0 // indirect - github.com/rs/cors v1.11.1 // indirect + github.com/prometheus/client_golang v1.20.5 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect + github.com/rivo/uniseg v0.2.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/tchap/go-patricia/v2 v2.3.2 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.51.0 // indirect + github.com/valyala/tcplisten v1.0.0 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/yashtewari/glob-intersection v0.2.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.34.0 // indirect + go.opentelemetry.io/otel/metric v1.34.0 // indirect + go.opentelemetry.io/otel/sdk v1.34.0 // indirect + go.opentelemetry.io/otel/trace v1.34.0 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/net v0.29.0 // indirect + golang.org/x/net v0.34.0 // indirect golang.org/x/sync v0.10.0 // indirect - golang.org/x/sys v0.28.0 // indirect + golang.org/x/sys v0.29.0 // indirect golang.org/x/text v0.21.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect + sigs.k8s.io/yaml v1.4.0 // indirect ) require ( - github.com/BurntSushi/toml v1.2.1 // indirect + github.com/BurntSushi/toml v1.3.2 // indirect github.com/jackc/pgx/v5 v5.6.0 github.com/jmoiron/sqlx v1.4.0 github.com/joho/godotenv v1.5.1 // indirect diff --git a/go.sum b/go.sum index 0677202..8d0b28a 100644 --- a/go.sum +++ b/go.sum @@ -1,28 +1,79 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= +github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8= +github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= +github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= +github.com/agnivade/levenshtein v1.2.0 h1:U9L4IOT0Y3i0TIlUIDJ7rVUziKi/zPbrJGaFrtYH3SY= +github.com/agnivade/levenshtein v1.2.0/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= +github.com/bytecodealliance/wasmtime-go/v3 v3.0.2 h1:3uZCA/BLTIu+DqCfguByNMJa2HVHpXvjfy0Dy7g6fuA= +github.com/bytecodealliance/wasmtime-go/v3 v3.0.2/go.mod h1:RnUjnIXxEJcL6BgCvNyzCCRzZcxCgsZCi+RNlvYor5Q= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +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/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/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= -github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/dgraph-io/badger/v4 v4.5.1 h1:7DCIXrQjo1LKmM96YD+hLVJ2EEsyyoWxJfpdd56HLps= +github.com/dgraph-io/badger/v4 v4.5.1/go.mod h1:qn3Be0j3TfV4kPbVoK0arXCD1/nr1ftth6sbL5jxdoA= +github.com/dgraph-io/ristretto/v2 v2.1.0 h1:59LjpOJLNDULHh8MC4UaegN52lC4JnO2dITsie/Pa8I= +github.com/dgraph-io/ristretto/v2 v2.1.0/go.mod h1:uejeqfYXpUomfse0+lO+13ATz4TypQYLJZzBSAemuB4= +github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo= +github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI= +github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= +github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= +github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI= +github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw= github.com/golang-jwt/jwt/v4 v4.5.1 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo= github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/flatbuffers v24.12.23+incompatible h1:ubBKR94NR4pXUCY/MUsRVzd9umNW7ht7EG9hHfS9FX8= +github.com/google/flatbuffers v24.12.23+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.2.0 h1:kQ0NI7W1B3HwiN5gAYtY+XFItDPbLBwYRxAqbFTyDes= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.2.0/go.mod h1:zrT2dxOAjNFPRGjTUe2Xmb4q4YdUwVvQFV6xiCSf+z0= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ= github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4= github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk= github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 h1:Dj0L5fhJ9F82ZJyVOmBx6msDp/kfd1t9GRfny/mfJA0= @@ -39,32 +90,101 @@ github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= +github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro= +github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= +github.com/open-policy-agent/opa v1.1.0 h1:HMz2evdEMTyNqtdLjmu3Vyx06BmhNYAx67Yz3Ll9q2s= +github.com/open-policy-agent/opa v1.1.0/go.mod h1:T1pASQ1/vwfTa+e2fYcfpLCvWgYtqtiUv+IuA/dLPQs= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= -github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 h1:MkV+77GLUNo5oJ0jf870itWm3D0Sjh7+Za9gazKc5LQ= +github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= 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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tchap/go-patricia/v2 v2.3.2 h1:xTHFutuitO2zqKAQ5rCROYgUb7Or/+IC3fts9/Yc7nM= +github.com/tchap/go-patricia/v2 v2.3.2/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= github.com/valkey-io/valkey-go v1.0.47 h1:fW5+m2BaLAbxB1EWEEWmj+i2n+YcYFBDG/jKs6qu5j8= github.com/valkey-io/valkey-go v1.0.47/go.mod h1:BXlVAPIL9rFQinSFM+N32JfWzfCaUAqBpZkc4vPY6fM= github.com/valkey-io/valkey-go/mock v1.0.47 h1:fQZUJrJEx4IG7vH1CjSSqPmx+5Gd6cwwdr7gcDDAIe0= github.com/valkey-io/valkey-go/mock v1.0.47/go.mod h1:k+lHD29cYer1FO+/HyxWLDUU1JakG6uQ/VgR1OGqe+I= +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.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA= +github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g= +github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVSA8= +github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/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/yashtewari/glob-intersection v0.2.0 h1:8iuHdN88yYuCzCdjt0gDe+6bAhUwBeEWqThExu54RFg= +github.com/yashtewari/glob-intersection v0.2.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0 h1:tgJ0uaNS4c98WRNUEx5U3aDlrDOI5Rs+1Vifcw4DJ8U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.34.0/go.mod h1:U7HYyW0zt/a9x5J1Kjs+r1f/d4ZHnYFclhYY2+YbeoE= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU= +go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= +go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= +go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= @@ -73,26 +193,33 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= -golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= -golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= +golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 h1:pgr/4QbFyktUv9CtQ/Fq4gzEE6/Xs7iCXbktaGzLHbQ= -google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697/go.mod h1:+D9ySVjN8nY8YCVjc5O7PZDIdZporIDY3KaGfJunh88= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 h1:LWZqQOEjDyONlF1H6afSWpAL/znlREo2tHfLoe+8LMA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= -google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0= -google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA= -google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= -google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= +google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA= +google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:Ic02D47M+zbarjYYUlK57y316f2MoN0gjAwI3f2S95o= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50= +google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ= +google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw= +google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= +google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -101,3 +228,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ= olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/internal/models/session.go b/internal/models/session.go index 8f92d66..28e8ab9 100644 --- a/internal/models/session.go +++ b/internal/models/session.go @@ -1,19 +1,19 @@ package models import ( - "encoding/json" "errors" "github.com/google/uuid" "time" ) type Session struct { - Id string `json:"id" db:"id"` - UserId int32 `json:"user_id" db:"user_id"` - Role Role `json:"role" db:"role"` - CreatedAt time.Time `json:"created_at" db:"created_at"` - //UserAgent string `json:"user_agent"` - //Ip string `json:"ip"` + Id string `json:"id"` + UserId int32 `json:"user_id"` + Role Role `json:"role"` + CreatedAt time.Time `json:"created_at"` + ExpiresAt time.Time `json:"expires_at"` + UserAgent string `json:"user_agent"` + Ip string `json:"ip"` } func (s Session) Valid() error { @@ -26,40 +26,46 @@ func (s Session) Valid() error { if s.CreatedAt.IsZero() { return errors.New("empty created at") } - if !s.Role.IsAdmin() && !s.Role.IsModerator() && !s.Role.IsParticipant() { - return errors.New("invalid role") + if s.ExpiresAt.IsZero() { + return errors.New("empty expires at") } + //if s.UserAgent == "" { + // return errors.New("empty user agent") + //} + //if s.Ip == "" { + // return errors.New("empty ip") + //} return nil } -func NewSession(userId int32, role Role) (string, string, error) { - s := &Session{ - Id: uuid.NewString(), - UserId: userId, - Role: role, - CreatedAt: time.Now(), - } - if err := s.Valid(); err != nil { - return "", "", err - } - - b, err := json.Marshal(s) - if err != nil { - return "", "", err - } - - return string(b), s.Id, nil +type JWT struct { + SessionId string `json:"session_id"` + UserId int32 `json:"user_id"` + Role Role `json:"role"` + ExpiresAt int64 `json:"exp"` + IssuedAt int64 `json:"iat"` + NotBefore int64 `json:"nbf"` + Permissions []grant `json:"permissions"` } -func ParseSession(s string) (*Session, error) { - sess := &Session{} - if err := json.Unmarshal([]byte(s), sess); err != nil { - return nil, err +func (j JWT) Valid() error { + if uuid.Validate(j.SessionId) != nil { + return errors.New("invalid session id") } - - if err := sess.Valid(); err != nil { - return nil, err + if j.UserId == 0 { + return errors.New("empty user id") } - - return sess, nil + if j.ExpiresAt == 0 { + return errors.New("empty expires at") + } + if j.IssuedAt == 0 { + return errors.New("empty issued at") + } + if j.NotBefore == 0 { + return errors.New("empty not before") + } + if len(j.Permissions) == 0 { + return errors.New("empty permissions") + } + return nil } diff --git a/internal/models/user.go b/internal/models/user.go index e44ed9a..b23cd84 100644 --- a/internal/models/user.go +++ b/internal/models/user.go @@ -1,7 +1,9 @@ package models import ( - "errors" + "context" + "encoding/json" + "github.com/open-policy-agent/opa/v1/rego" "golang.org/x/crypto/bcrypt" "time" ) @@ -9,30 +11,90 @@ import ( type Role int32 const ( - RoleParticipant Role = 0 - RoleModerator Role = 1 - RoleAdmin Role = 2 + RoleGuest Role = -1 + RoleStudent Role = 0 + RoleTeacher Role = 1 + RoleAdmin Role = 2 ) -func (role Role) IsAdmin() bool { - return role == RoleAdmin +func (r Role) String() string { + switch r { + case RoleGuest: + return "guest" + case RoleStudent: + return "student" + case RoleTeacher: + return "teacher" + case RoleAdmin: + return "admin" + } + + panic("invalid role") } -func (role Role) IsModerator() bool { - return role == RoleModerator +type Action string + +const ( + Create Action = "create" + Read Action = "read" + Update Action = "update" + Delete Action = "delete" +) + +type Resource string + +const ( + ResourceAnotherUser Resource = "another-user" + ResourceMeUser Resource = "me-user" + ResourceListUser Resource = "list-user" + + ResourceOwnSession Resource = "own-session" +) + +type grant struct { + Action Action `json:"action"` + Resource Resource `json:"resource"` } -func (role Role) IsParticipant() bool { - return role == RoleParticipant +var Grants = map[string][]grant{ + RoleGuest.String(): {}, + RoleStudent.String(): { + {Read, ResourceAnotherUser}, + {Read, ResourceMeUser}, + {Update, ResourceOwnSession}, + {Delete, ResourceOwnSession}, + }, + RoleTeacher.String(): { + {Create, ResourceAnotherUser}, + {Read, ResourceAnotherUser}, + {Read, ResourceMeUser}, + {Read, ResourceListUser}, + {Update, ResourceOwnSession}, + {Delete, ResourceOwnSession}, + }, + RoleAdmin.String(): { + {Create, ResourceAnotherUser}, + {Read, ResourceAnotherUser}, + {Read, ResourceMeUser}, + {Read, ResourceListUser}, + {Update, ResourceAnotherUser}, + {Update, ResourceOwnSession}, + {Delete, ResourceAnotherUser}, + {Delete, ResourceOwnSession}, + }, } -func (role Role) AtLeast(other Role) bool { - return role >= other -} +const module = `package app.rbac -func (role Role) AtMost(other Role) bool { - return role <= other +default allow := false + +allow if { + some grant in input.role_grants[input.role] + + input.action == grant.action + input.resource == grant.resource } +` type User struct { Id int32 `db:"id"` @@ -43,10 +105,61 @@ type User struct { Role Role `db:"role"` } -func (user *User) ComparePassword(password string) error { +func (user *User) MarshalJSON() ([]byte, error) { + m := map[string]interface{}{ + "id": user.Id, + "username": user.Username, + "created_at": user.CreatedAt, + "modified_at": user.ModifiedAt, + "role": user.Role, + } + + b, err := json.Marshal(m) + if err != nil { + return nil, err + } + + return b, nil +} + +func (user *User) IsSamePwd(password string) bool { err := bcrypt.CompareHashAndPassword([]byte(user.HashedPassword), []byte(password)) if err != nil { - return errors.New("bad username or password") + return false + } + return true +} + +var query rego.PreparedEvalQuery + +func (r Role) HasPermission(action Action, resource Resource) bool { + ctx := context.TODO() + + input := map[string]interface{}{ + "action": action, + "resource": resource, + "role": r.String(), + "role_grants": Grants, + } + + results, err := query.Eval(ctx, rego.EvalInput(input)) + if err != nil { + panic(err) + } + + return results.Allowed() +} + +func init() { + var err error + ctx := context.TODO() + + query, err = rego.New( + rego.Query("data.app.rbac.allow"), + rego.Module("ms-auth.rego", module), + ).PrepareForEval(ctx) + + if err != nil { + panic(err) } - return nil } diff --git a/internal/users/delivery.go b/internal/users/delivery.go index 3c5bb8b..08363b0 100644 --- a/internal/users/delivery.go +++ b/internal/users/delivery.go @@ -1,19 +1,21 @@ package users import ( - "context" userv1 "git.sch9.ru/new_gate/ms-auth/proto/user/v1" - "google.golang.org/protobuf/types/known/emptypb" + "github.com/gofiber/fiber/v2" ) type UserHandlers interface { - CreateUser(ctx context.Context, req *userv1.CreateUserRequest) (*userv1.CreateUserResponse, error) - GetUser(ctx context.Context, req *userv1.GetUserRequest) (*userv1.GetUserResponse, error) - UpdateUser(ctx context.Context, req *userv1.UpdateUserRequest) (*emptypb.Empty, error) - DeleteUser(ctx context.Context, req *userv1.DeleteUserRequest) (*emptypb.Empty, error) - Login(ctx context.Context, req *userv1.LoginRequest) (*emptypb.Empty, error) - Verify(ctx context.Context, req *emptypb.Empty) (*emptypb.Empty, error) - Refresh(ctx context.Context, req *emptypb.Empty) (*emptypb.Empty, error) - Logout(ctx context.Context, req *emptypb.Empty) (*emptypb.Empty, error) - CompleteLogout(ctx context.Context, req *emptypb.Empty) (*emptypb.Empty, error) + ListSessions(c *fiber.Ctx) error + CompleteLogout(c *fiber.Ctx) error + Login(c *fiber.Ctx) error + Logout(c *fiber.Ctx) error + Refresh(c *fiber.Ctx) error + Verify(c *fiber.Ctx) error + ListUsers(c *fiber.Ctx, params userv1.ListUsersParams) error + CreateUser(c *fiber.Ctx) error + GetMe(c *fiber.Ctx) error + DeleteUser(c *fiber.Ctx, id int32) error + GetUser(c *fiber.Ctx, id int32) error + UpdateUser(c *fiber.Ctx, id int32) error } diff --git a/internal/users/delivery/grpc/handlers.go b/internal/users/delivery/grpc/handlers.go deleted file mode 100644 index 4a1816c..0000000 --- a/internal/users/delivery/grpc/handlers.go +++ /dev/null @@ -1,283 +0,0 @@ -package grpc - -import ( - "context" - "errors" - "git.sch9.ru/new_gate/ms-auth/internal/models" - "git.sch9.ru/new_gate/ms-auth/internal/users" - "git.sch9.ru/new_gate/ms-auth/pkg" - userv1 "git.sch9.ru/new_gate/ms-auth/proto/user/v1" - "google.golang.org/grpc" - "google.golang.org/grpc/metadata" - "google.golang.org/protobuf/types/known/emptypb" - "google.golang.org/protobuf/types/known/timestamppb" - "strings" -) - -type UserHandlers struct { - userv1.UnimplementedUserServiceServer - userUC users.UseCase -} - -func NewUserHandlers(gserver *grpc.Server, userUC users.UseCase) { - handlers := &UserHandlers{ - userUC: userUC, - } - - userv1.RegisterUserServiceServer(gserver, handlers) -} - -const ( - SessionHeaderName = "x-session-id" - AuthUserHeaderName = "x-auth-user-id" -) - -func (h *UserHandlers) Login(ctx context.Context, req *userv1.LoginRequest) (*emptypb.Empty, error) { - const op = "UserHandlers.Login" - - var ( - err error - user *models.User - ) - - username := req.GetUsername() - password := req.GetPassword() - - user, err = h.userUC.ReadUserByUsername(ctx, username) - if err != nil { - return nil, pkg.ToGRPC(err) - } - - err = user.ComparePassword(password) - if err != nil { - return nil, pkg.ToGRPC(pkg.Wrap(pkg.ErrNotFound, err, op, "bad username or password")) - } - - sessionId, err := h.userUC.CreateSession(ctx, user.Id, user.Role) - if err != nil { - return nil, pkg.ToGRPC(err) - } - - header := metadata.New(map[string]string{ - SessionHeaderName: sessionId, - }) - err = grpc.SendHeader(ctx, header) - if err != nil { - return nil, err - } - - return &emptypb.Empty{}, nil -} - -func AuthSessionIdFromContext(ctx context.Context) (string, error) { - md, ok := metadata.FromIncomingContext(ctx) - - if !ok { - return "", errors.New("failed to get metadata") - } - tokens := md.Get(SessionHeaderName) - sessionId := strings.Join(tokens, "") - if len(sessionId) == 0 { - return "", errors.New("no session id in context") - } - return sessionId, nil -} - -func (h *UserHandlers) Refresh(ctx context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) { - const op = "UserHandlers.Refresh" - - sessionId, err := AuthSessionIdFromContext(ctx) - if err != nil { - return nil, pkg.ToGRPC(pkg.Wrap(err, pkg.ErrUnauthenticated, op, "no session id in context")) - } - err = h.userUC.UpdateSession(ctx, sessionId) - if err != nil { - return nil, pkg.ToGRPC(err) - } - return &emptypb.Empty{}, nil -} - -func (h *UserHandlers) Logout(ctx context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) { - const op = "UserHandlers.Logout" - - sessionId, err := AuthSessionIdFromContext(ctx) - if err != nil { - return nil, pkg.ToGRPC(pkg.Wrap(err, pkg.ErrUnauthenticated, op, "no session id in context")) - } - err = h.userUC.DeleteSession(ctx, sessionId) - if err != nil { - return nil, pkg.ToGRPC(err) - } - - header := metadata.New(map[string]string{ - SessionHeaderName: "", - }) - err = grpc.SendHeader(ctx, header) - if err != nil { - return nil, err - } - - return &emptypb.Empty{}, nil -} - -func (h *UserHandlers) CompleteLogout(ctx context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) { - const op = "UserHandlers.CompleteLogout" - - sessionId, err := AuthSessionIdFromContext(ctx) - if err != nil { - return nil, pkg.ToGRPC(pkg.Wrap(err, pkg.ErrUnauthenticated, op, "no session id in context")) - } - - session, err := h.userUC.ReadSession(ctx, sessionId) - if err != nil { - return nil, pkg.ToGRPC(err) - } - - err = h.userUC.DeleteAllSessions(ctx, session.UserId) - if err != nil { - return nil, pkg.ToGRPC(err) - } - return &emptypb.Empty{}, nil -} - -func (h *UserHandlers) Verify(ctx context.Context, _ *emptypb.Empty) (*emptypb.Empty, error) { - const op = "UserHandlers.Verify" - - sessionId, err := AuthSessionIdFromContext(ctx) - if err != nil { - return nil, pkg.ToGRPC(pkg.Wrap(err, pkg.ErrUnauthenticated, op, "no session id in context")) - } - token, err := h.userUC.Verify(ctx, sessionId) - if err != nil { - return nil, pkg.ToGRPC(err) - } - - header := metadata.New(map[string]string{ - AuthUserHeaderName: token, - }) - err = grpc.SendHeader(ctx, header) - if err != nil { - return nil, err - } - - return &emptypb.Empty{}, nil -} - -func (h *UserHandlers) CreateUser(ctx context.Context, req *userv1.CreateUserRequest) (*userv1.CreateUserResponse, error) { - const op = "UserHandlers.CreateUser" - - sessionId, err := AuthSessionIdFromContext(ctx) - if err != nil { - return nil, pkg.ToGRPC(pkg.Wrap(err, pkg.ErrUnauthenticated, op, "no session id in context")) - } - - ctx = context.WithValue(ctx, "userId", sessionId) - - id, err := h.userUC.CreateUser( - ctx, - req.GetUsername(), - req.GetPassword(), - models.RoleParticipant, - ) - if err != nil { - return nil, pkg.ToGRPC(err) - } - - return &userv1.CreateUserResponse{ - Id: id, - }, nil -} - -func (h *UserHandlers) GetUser(ctx context.Context, req *userv1.GetUserRequest) (*userv1.GetUserResponse, error) { - const op = "UserHandlers.GetUser" - - var userId = req.GetId() - - if req.GetMe() { - sessionId, err := AuthSessionIdFromContext(ctx) - if err != nil { - return nil, pkg.ToGRPC(pkg.Wrap(err, pkg.ErrUnauthenticated, op, "no session id in context")) - } - - session, err := h.userUC.ReadSession(ctx, sessionId) - if err != nil { - return nil, pkg.ToGRPC(err) - } - - userId = session.UserId - } - - user, err := h.userUC.ReadUserById( - ctx, - userId, - ) - - if err != nil { - return nil, pkg.ToGRPC(err) - } - - return &userv1.GetUserResponse{ - User: &userv1.User{ - Id: user.Id, - Username: user.Username, - CreatedAt: timestamppb.New(user.CreatedAt), - ModifiedAt: timestamppb.New(user.ModifiedAt), - Role: userv1.Role(user.Role), - }, - }, nil -} - -func (h *UserHandlers) UpdateUser(ctx context.Context, req *userv1.UpdateUserRequest) (*emptypb.Empty, error) { - const op = "UserHandlers.UpdateUser" - - sessionId, err := AuthSessionIdFromContext(ctx) - if err != nil { - return nil, pkg.ToGRPC(pkg.Wrap(err, pkg.ErrUnauthenticated, op, "no session id in context")) - } - - ctx = context.WithValue(ctx, "userId", sessionId) - - err = h.userUC.UpdateUser( - ctx, - req.GetId(), - AsStringP(req.Username), - AsMRoleP(req.Role), - ) - if err != nil { - return nil, pkg.ToGRPC(err) - } - return &emptypb.Empty{}, nil -} - -func (h *UserHandlers) DeleteUser(ctx context.Context, req *userv1.DeleteUserRequest) (*emptypb.Empty, error) { - const op = "UserHandlers.DeleteUser" - - sessionId, err := AuthSessionIdFromContext(ctx) - if err != nil { - return nil, pkg.ToGRPC(pkg.Wrap(err, pkg.ErrUnauthenticated, op, "no session id in context")) - } - - ctx = context.WithValue(ctx, "userId", sessionId) - - err = h.userUC.DeleteUser( - ctx, - req.GetId(), - ) - if err != nil { - return nil, pkg.ToGRPC(err) - } - return &emptypb.Empty{}, nil -} - -func AsMRoleP(v userv1.Role) *models.Role { - vv := models.Role(v.Number()) - return &vv -} - -func AsRoleP(v models.Role) *models.Role { - return &v -} - -func AsStringP(str string) *string { - return &str -} diff --git a/internal/users/delivery/grpc/handlers_test.go b/internal/users/delivery/grpc/handlers_test.go deleted file mode 100644 index 34edc51..0000000 --- a/internal/users/delivery/grpc/handlers_test.go +++ /dev/null @@ -1,428 +0,0 @@ -package grpc - -import ( - "context" - "git.sch9.ru/new_gate/ms-auth/internal/models" - "git.sch9.ru/new_gate/ms-auth/internal/users" - mock_users "git.sch9.ru/new_gate/ms-auth/internal/users/delivery/mock" - userv1 "git.sch9.ru/new_gate/ms-auth/proto/user/v1" - "github.com/google/uuid" - "github.com/stretchr/testify/require" - "go.uber.org/mock/gomock" - "golang.org/x/crypto/bcrypt" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/credentials/insecure" - "google.golang.org/grpc/metadata" - "google.golang.org/grpc/status" - "google.golang.org/protobuf/types/known/emptypb" - "net" - "testing" - "time" -) - -func startServer(t *testing.T, uc users.UseCase, addr string) { - t.Helper() - - gserver := grpc.NewServer() - NewUserHandlers(gserver, uc) - - ln, err := net.Listen("tcp", addr) - if err != nil { - panic(err) - } - - go func() { - if err = gserver.Serve(ln); err != nil { - panic(err) - } - }() - - t.Cleanup(func() { - gserver.Stop() - }) -} - -func buildClient(t *testing.T, addr string) userv1.UserServiceClient { - t.Helper() - conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials())) - require.NoError(t, err) - - return userv1.NewUserServiceClient(conn) -} - -func TestUserHandlers_Login(t *testing.T) { - t.Parallel() - - const addr = "127.0.0.1:62999" - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - uc := mock_users.NewMockUseCase(ctrl) - startServer(t, uc, addr) - - client := buildClient(t, addr) - - t.Run("valid login", func(t *testing.T) { - password := "password" - hpwd, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) - require.NoError(t, err) - - user := &models.User{ - Id: 1, - Username: "username", - HashedPassword: string(hpwd), - Role: models.RoleAdmin, - } - sid := uuid.NewString() - - uc.EXPECT().ReadUserByUsername(gomock.Any(), user.Username).Return(user, nil) - uc.EXPECT().CreateSession(gomock.Any(), user.Id, user.Role).Return(sid, nil) - - ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second)) - t.Cleanup(cancel) - - var header metadata.MD - _, err = client.Login(ctx, &userv1.LoginRequest{ - Username: user.Username, - Password: password, - }, grpc.Header(&header)) - require.NoError(t, err) - - require.Equal(t, sid, header.Get(SessionHeaderName)[0]) - }) - - t.Run("invalid login (wrong password)", func(t *testing.T) { - password := "password" - hpwd, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) - require.NoError(t, err) - - user := &models.User{ - Id: 1, - Username: "username", - HashedPassword: string(hpwd), - Role: models.RoleAdmin, - } - - uc.EXPECT().ReadUserByUsername(gomock.Any(), user.Username).Return(user, nil) - - ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second)) - t.Cleanup(cancel) - - _, err = client.Login(ctx, &userv1.LoginRequest{ - Username: user.Username, - Password: "wrongpassword", - }) - - s, ok := status.FromError(err) - require.True(t, ok) - require.Equal(t, codes.NotFound, s.Code()) - }) -} - -func TestUserHandlers_Refresh(t *testing.T) { - t.Parallel() - - const addr = "127.0.0.1:62998" - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - uc := mock_users.NewMockUseCase(ctrl) - startServer(t, uc, addr) - - client := buildClient(t, addr) - - t.Run("valid refresh", func(t *testing.T) { - sid := uuid.NewString() - uc.EXPECT().UpdateSession(gomock.Any(), sid).Return(nil) - - ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second)) - t.Cleanup(cancel) - - ctx = metadata.AppendToOutgoingContext(ctx, SessionHeaderName, sid) - - _, err := client.Refresh(ctx, &emptypb.Empty{}) - require.NoError(t, err) - }) - - t.Run("invalid refresh (no session id in context)", func(t *testing.T) { - ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second)) - t.Cleanup(cancel) - - _, err := client.Refresh(ctx, &emptypb.Empty{}) - - s, ok := status.FromError(err) - require.True(t, ok) - require.Equal(t, codes.Unauthenticated, s.Code()) - }) -} - -func TestUserHandlers_Logout(t *testing.T) { - t.Parallel() - - const addr = "127.0.0.1:62997" - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - uc := mock_users.NewMockUseCase(ctrl) - startServer(t, uc, addr) - - client := buildClient(t, addr) - - t.Run("valid logout", func(t *testing.T) { - sid := uuid.NewString() - uc.EXPECT().DeleteSession(gomock.Any(), sid).Return(nil) - - ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second)) - t.Cleanup(cancel) - - ctx = metadata.AppendToOutgoingContext(ctx, SessionHeaderName, sid) - - _, err := client.Logout(ctx, &emptypb.Empty{}) - require.NoError(t, err) - }) - - t.Run("invalid logout (no session id in context)", func(t *testing.T) { - ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second)) - t.Cleanup(cancel) - - _, err := client.Logout(ctx, &emptypb.Empty{}) - - s, ok := status.FromError(err) - require.True(t, ok) - require.Equal(t, codes.Unauthenticated, s.Code()) - }) -} - -func TestUserHandlers_CompleteLogout(t *testing.T) { - t.Parallel() - - const addr = "127.0.0.1:62996" - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - uc := mock_users.NewMockUseCase(ctrl) - startServer(t, uc, addr) - - client := buildClient(t, addr) - - t.Run("valid complete logout", func(t *testing.T) { - sid := uuid.NewString() - - ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second)) - ctx = metadata.AppendToOutgoingContext(ctx, SessionHeaderName, sid) - t.Cleanup(cancel) - - uc.EXPECT().ReadSession(gomock.Any(), sid).Return(&models.Session{UserId: 1}, nil) - uc.EXPECT().DeleteAllSessions(gomock.Any(), int32(1)).Return(nil) - - _, err := client.CompleteLogout(ctx, &emptypb.Empty{}) - require.NoError(t, err) - }) - - t.Run("invalid complete logout (no session id in context)", func(t *testing.T) { - ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second)) - t.Cleanup(cancel) - - _, err := client.CompleteLogout(ctx, &emptypb.Empty{}) - - s, ok := status.FromError(err) - require.True(t, ok) - require.Equal(t, codes.Unauthenticated, s.Code()) - }) -} - -func TestUserHandlers_Verify(t *testing.T) { - t.Parallel() - - const addr = "127.0.0.1:62995" - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - uc := mock_users.NewMockUseCase(ctrl) - startServer(t, uc, addr) - - client := buildClient(t, addr) - - t.Run("valid verify", func(t *testing.T) { - sid := uuid.NewString() - ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second)) - ctx = metadata.AppendToOutgoingContext(ctx, SessionHeaderName, sid) - t.Cleanup(cancel) - - uc.EXPECT().Verify(gomock.Any(), sid).Return("jwt", nil) - - var header metadata.MD - _, err := client.Verify(ctx, &emptypb.Empty{}, grpc.Header(&header)) - require.NoError(t, err) - require.Equal(t, header.Get(AuthUserHeaderName)[0], "jwt") - }) - - t.Run("invalid verify (no session id in context)", func(t *testing.T) { - ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second)) - t.Cleanup(cancel) - - _, err := client.Verify(ctx, &emptypb.Empty{}) - - s, ok := status.FromError(err) - require.True(t, ok) - require.Equal(t, codes.Unauthenticated, s.Code()) - }) -} - -func TestUserHandlers_CreateUser(t *testing.T) { - t.Parallel() - - const addr = "127.0.0.1:62994" - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - uc := mock_users.NewMockUseCase(ctrl) - startServer(t, uc, addr) - - client := buildClient(t, addr) - - t.Run("valid create user", func(t *testing.T) { - username := "username" - password := "password" - - ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second)) - ctx = metadata.AppendToOutgoingContext(ctx, SessionHeaderName, uuid.NewString()) - t.Cleanup(cancel) - - uc.EXPECT().CreateUser(gomock.Any(), username, password, models.RoleParticipant).Return(int32(2), nil) - - _, err := client.CreateUser(ctx, &userv1.CreateUserRequest{ - Username: username, - Password: password, - }) - require.NoError(t, err) - }) - - t.Run("invalid create user (no session id in context)", func(t *testing.T) { - ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second)) - t.Cleanup(cancel) - - _, err := client.CreateUser(ctx, &userv1.CreateUserRequest{ - Username: "username", - Password: "password", - }) - - s, ok := status.FromError(err) - require.True(t, ok) - require.Equal(t, codes.Unauthenticated, s.Code()) - }) -} - -func TestUserHandlers_GetUser(t *testing.T) { - t.Parallel() - - const addr = "127.0.0.1:62993" - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - uc := mock_users.NewMockUseCase(ctrl) - startServer(t, uc, addr) - - client := buildClient(t, addr) - - t.Run("valid get user", func(t *testing.T) { - ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second)) - t.Cleanup(cancel) - - uc.EXPECT().ReadUserById(gomock.Any(), int32(1)).Return(&models.User{ - Id: 1, - Username: "username", - CreatedAt: time.Now(), - ModifiedAt: time.Now(), - Role: models.RoleParticipant, - }, nil) - - _, err := client.GetUser(ctx, &userv1.GetUserRequest{ - Id: 1, - }) - require.NoError(t, err) - }) -} - -func TestUserHandlers_UpdateUser(t *testing.T) { - t.Parallel() - - const addr = "127.0.0.1:62992" - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - uc := mock_users.NewMockUseCase(ctrl) - startServer(t, uc, addr) - - client := buildClient(t, addr) - - t.Run("valid update user", func(t *testing.T) { - ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second)) - ctx = metadata.AppendToOutgoingContext(ctx, SessionHeaderName, uuid.NewString()) - t.Cleanup(cancel) - - uc.EXPECT().UpdateUser(gomock.Any(), - int32(1), - AsStringP("username"), - AsRoleP(models.RoleModerator), - ).Return(nil) - - _, err := client.UpdateUser(ctx, &userv1.UpdateUserRequest{ - Id: 1, - Username: "username", - Role: userv1.Role_ROLE_MODERATOR, - }) - require.NoError(t, err) - }) - - t.Run("invalid update user (no session id in context)", func(t *testing.T) { - ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second)) - t.Cleanup(cancel) - - _, err := client.UpdateUser(ctx, &userv1.UpdateUserRequest{ - Id: 1, - Username: "username", - Role: userv1.Role_ROLE_MODERATOR, - }) - - s, ok := status.FromError(err) - require.True(t, ok) - require.Equal(t, codes.Unauthenticated, s.Code()) - }) -} - -func TestUserHandlers_DeleteUser(t *testing.T) { - t.Parallel() - - const addr = "127.0.0.1:62991" - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - uc := mock_users.NewMockUseCase(ctrl) - startServer(t, uc, addr) - - client := buildClient(t, addr) - - t.Run("valid delete user", func(t *testing.T) { - ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(5*time.Second)) - ctx = metadata.AppendToOutgoingContext(ctx, SessionHeaderName, uuid.NewString()) - t.Cleanup(cancel) - - uc.EXPECT().DeleteUser(gomock.Any(), int32(1)).Return(nil) - - _, err := client.DeleteUser(ctx, &userv1.DeleteUserRequest{ - Id: 1, - }) - require.NoError(t, err) - }) -} diff --git a/internal/users/delivery/rest/handlers.go b/internal/users/delivery/rest/handlers.go new file mode 100644 index 0000000..0db26f4 --- /dev/null +++ b/internal/users/delivery/rest/handlers.go @@ -0,0 +1,265 @@ +package rest + +import ( + "encoding/base64" + "errors" + "git.sch9.ru/new_gate/ms-auth/internal/models" + "git.sch9.ru/new_gate/ms-auth/internal/users" + "git.sch9.ru/new_gate/ms-auth/pkg" + userv1 "git.sch9.ru/new_gate/ms-auth/proto/user/v1" + "github.com/gofiber/fiber/v2" + "github.com/golang-jwt/jwt/v4" + "strings" + "time" +) + +type UserHandlers struct { + userUC users.UseCase + + jwtSecret string +} + +func NewUserHandlers(userUC users.UseCase, jwtSecret string) *UserHandlers { + return &UserHandlers{ + userUC: userUC, + jwtSecret: jwtSecret, + } +} + +func (h *UserHandlers) Login(c *fiber.Ctx) error { + const op = "UserHandlers.Login" + + authHeader := c.Get("Authorization", "") + if authHeader == "" { + return c.SendStatus(fiber.StatusUnauthorized) + } + + authParts := strings.Split(authHeader, " ") + if len(authParts) != 2 || strings.ToLower(authParts[0]) != "basic" { + return c.SendStatus(fiber.StatusUnauthorized) + } + + decodedAuth, err := base64.StdEncoding.DecodeString(authParts[1]) + if err != nil { + return c.SendStatus(fiber.StatusUnauthorized) + } + + authParts = strings.Split(string(decodedAuth), ":") + if len(authParts) != 2 { + return c.SendStatus(fiber.StatusUnauthorized) + } + + ctx := c.Context() + + user, err := h.userUC.ReadUserByUsername(ctx, authParts[0]) + if err != nil { + if errors.Is(err, pkg.ErrNotFound) { + return c.SendStatus(fiber.StatusUnauthorized) + } + + return c.SendStatus(pkg.ToREST(err)) + } + + if !user.IsSamePwd(authParts[1]) { + return c.SendStatus(fiber.StatusUnauthorized) + } + + userAgent := c.Get("User-Agent", "") + ip := c.IP() + + session, err := h.userUC.CreateSession(ctx, user.Id, user.Role, userAgent, ip) + if err != nil { + return c.SendStatus(pkg.ToREST(err)) + } + + claims := jwt.NewWithClaims(jwt.SigningMethodHS256, models.JWT{ + SessionId: session.Id, + UserId: user.Id, + Role: user.Role, + ExpiresAt: session.ExpiresAt.Unix(), + IssuedAt: time.Now().Unix(), + NotBefore: time.Now().Unix(), + Permissions: models.Grants[user.Role.String()], + }) + + token, err := claims.SignedString([]byte(h.jwtSecret)) + if err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } + + c.Set("Authorization", "Bearer "+token) + + return c.SendStatus(fiber.StatusOK) +} + +func (h *UserHandlers) Refresh(c *fiber.Ctx) error { + const op = "UserHandlers.Refresh" + + token, ok := c.Locals(TokenKey).(*models.JWT) + if !ok { + return c.SendStatus(fiber.StatusUnauthorized) + } + + err := h.userUC.UpdateSession(c.Context(), token.SessionId) + if err != nil { + return c.SendStatus(pkg.ToREST(err)) + } + + return c.SendStatus(fiber.StatusOK) +} + +func (h *UserHandlers) Logout(c *fiber.Ctx) error { + const op = "UserHandlers.Logout" + + token, ok := c.Locals(TokenKey).(*models.JWT) + if !ok { + return c.SendStatus(fiber.StatusUnauthorized) + } + + err := h.userUC.DeleteSession(c.Context(), token.SessionId) + if err != nil { + return c.SendStatus(pkg.ToREST(err)) + } + + return c.SendStatus(fiber.StatusOK) +} + +func (h *UserHandlers) CompleteLogout(c *fiber.Ctx) error { + const op = "UserHandlers.CompleteLogout" + + token, ok := c.Locals(TokenKey).(*models.JWT) + if !ok { + return c.SendStatus(fiber.StatusUnauthorized) + } + + ctx := c.Context() + + err := h.userUC.DeleteAllSessions(ctx, token.UserId) + if err != nil { + return c.SendStatus(pkg.ToREST(err)) + } + + return c.SendStatus(fiber.StatusOK) +} + +func (h *UserHandlers) Verify(c *fiber.Ctx) error { + const op = "UserHandlers.Verify" + + return c.SendStatus(fiber.StatusNotImplemented) +} + +func (h *UserHandlers) CreateUser(c *fiber.Ctx) error { + const op = "UserHandlers.CreateUser" + + ctx := c.Context() + + id, err := h.userUC.CreateUser( + ctx, + c.FormValue("username"), + c.FormValue("password"), + models.RoleStudent, + ) + if err != nil { + return c.SendStatus(pkg.ToREST(err)) + } + + return c.JSON(map[string]interface{}{ + "id": id, + }) +} + +func (h *UserHandlers) GetMe(c *fiber.Ctx) error { + const op = "UserHandlers.GetMe" + + token, ok := c.Locals(TokenKey).(*models.JWT) + if !ok { + return c.SendStatus(fiber.StatusUnauthorized) + } + + user, err := h.userUC.ReadUserById(c.Context(), token.UserId) + if err != nil { + return c.SendStatus(pkg.ToREST(err)) + } + + return c.JSON(map[string]interface{}{ + "user": user, + }) +} + +func (h *UserHandlers) GetUser(c *fiber.Ctx, id int32) error { + const op = "UserHandlers.GetUser" + + user, err := h.userUC.ReadUserById(c.Context(), id) + if err != nil { + return c.SendStatus(pkg.ToREST(err)) + } + + return c.JSON(map[string]interface{}{ + "user": user, + }) +} + +func (h *UserHandlers) UpdateUser(c *fiber.Ctx, id int32) error { + const op = "UserHandlers.UpdateUser" + + var req = &userv1.UpdateUserRequest{} + + err := c.BodyParser(req) + if err != nil { + return c.SendStatus(fiber.StatusBadRequest) + } + + err = h.userUC.UpdateUser(c.Context(), id, req.Username, int32PtrToRolePtr(req.Role)) + if err != nil { + return c.SendStatus(pkg.ToREST(err)) + } + + return c.SendStatus(fiber.StatusOK) +} + +func (h *UserHandlers) DeleteUser(c *fiber.Ctx, id int32) error { + const op = "UserHandlers.DeleteUser" + + ctx := c.Context() + + err := h.userUC.DeleteUser(ctx, id) + if err != nil { + return c.SendStatus(pkg.ToREST(err)) + } + + return c.SendStatus(fiber.StatusOK) +} + +func (h *UserHandlers) ListUsers(c *fiber.Ctx, params userv1.ListUsersParams) error { + const op = "UserHandlers.ListUsers" + + usersList, count, err := h.userUC.ListUsers(c.Context(), params.Page, params.PageSize) + if err != nil { + return c.SendStatus(pkg.ToREST(err)) + } + + return c.JSON(map[string]interface{}{ + "users": usersList, + "page": params.Page, + "max_page": func() int32 { + if count%params.PageSize == 0 { + return count / params.PageSize + } + return count/params.PageSize + 1 + }(), + }) +} + +func (h *UserHandlers) ListSessions(c *fiber.Ctx) error { + const op = "UserHandlers.ListSessions" + + return c.SendStatus(fiber.StatusNotImplemented) +} + +func int32PtrToRolePtr(i *int32) *models.Role { + if i == nil { + return nil + } + ii := models.Role(*i) + return &ii +} diff --git a/internal/users/delivery/rest/middleware.go b/internal/users/delivery/rest/middleware.go new file mode 100644 index 0000000..dffc2c1 --- /dev/null +++ b/internal/users/delivery/rest/middleware.go @@ -0,0 +1,74 @@ +package rest + +import ( + "errors" + "fmt" + "git.sch9.ru/new_gate/ms-auth/internal/models" + "git.sch9.ru/new_gate/ms-auth/internal/users" + "git.sch9.ru/new_gate/ms-auth/pkg" + "github.com/gofiber/fiber/v2" + "github.com/golang-jwt/jwt/v4" + "strings" +) + +const ( + TokenKey = "token" +) + +func AuthMiddleware(jwtSecret string, userUC users.UseCase) fiber.Handler { + return func(c *fiber.Ctx) error { + const op = "AuthMiddleware" + + authHeader := c.Get("Authorization", "") + if authHeader == "" { + c.Locals(TokenKey, nil) + return c.Next() + } + + authParts := strings.Split(authHeader, " ") + if len(authParts) != 2 || strings.ToLower(authParts[0]) != "bearer" { + c.Locals(TokenKey, nil) + return c.Next() + } + + parsedToken, err := jwt.ParseWithClaims(authParts[1], &models.JWT{}, func(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) + } + + return []byte(jwtSecret), nil + }) + if err != nil { + c.Locals(TokenKey, nil) + return c.Next() + } + + token, ok := parsedToken.Claims.(*models.JWT) + if !ok { + c.Locals(TokenKey, nil) + return c.Next() + } + + err = token.Valid() + if err != nil { + c.Locals(TokenKey, nil) + return c.Next() + } + + ctx := c.Context() + + // check if session exists + _, err = userUC.ReadSession(ctx, token.SessionId) + if err != nil { + if errors.Is(err, pkg.ErrNotFound) { + c.Locals(TokenKey, nil) + return c.Next() + } + + return c.SendStatus(pkg.ToREST(err)) + } + + c.Locals(TokenKey, token) + return c.Next() + } +} diff --git a/internal/users/repository.go b/internal/users/repository.go index 14d77e8..09868de 100644 --- a/internal/users/repository.go +++ b/internal/users/repository.go @@ -11,6 +11,7 @@ type Caller interface { ReadUserById(ctx context.Context, id int32) (*models.User, error) UpdateUser(ctx context.Context, id int32, username *string, role *models.Role) error DeleteUser(ctx context.Context, id int32) error + ListUsers(ctx context.Context, page int32, pageSize int32) ([]*models.User, int32, error) } type TxCaller interface { @@ -25,7 +26,7 @@ type PgRepository interface { } type ValkeyRepository interface { - CreateSession(ctx context.Context, userId int32, role models.Role) (string, error) + CreateSession(ctx context.Context, userId int32, role models.Role, userAgent, ip string) (*models.Session, error) ReadSession(ctx context.Context, sessionId string) (*models.Session, error) UpdateSession(ctx context.Context, sessionId string) error DeleteSession(ctx context.Context, sessionId string) error diff --git a/internal/users/repository/pg_repository.go b/internal/users/repository/pg_repository.go index 821473d..ef70fc1 100644 --- a/internal/users/repository/pg_repository.go +++ b/internal/users/repository/pg_repository.go @@ -47,6 +47,7 @@ type TxOrDB interface { GetContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error QueryxContext(ctx context.Context, query string, args ...interface{}) (*sqlx.Rows, error) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) + SelectContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error } type Caller struct { @@ -202,6 +203,36 @@ func (c *Caller) DeleteUser(ctx context.Context, id int32) error { return nil } +const ( + ListUsers = "SELECT * FROM users LIMIT ? OFFSET ?" + CountUsers = "SELECT COUNT(*) FROM users" +) + +func (c *Caller) ListUsers(ctx context.Context, page int32, pageSize int32) ([]*models.User, int32, error) { + const op = "Caller.ListUsers" + + if pageSize > 20 { + return nil, 0, pkg.Wrap(pkg.ErrBadInput, nil, op, "limit > 20") + } + + var usersList []*models.User + query := c.db.Rebind(ListUsers) + + err := c.db.SelectContext(ctx, &usersList, query, pageSize, (page-1)*pageSize) + if err != nil { + return nil, 0, handlePgErr(err, op) + } + + query = c.db.Rebind(CountUsers) + var count int32 + err = c.db.GetContext(ctx, &count, query) + if err != nil { + return nil, 0, handlePgErr(err, op) + } + + return usersList, count, nil +} + func handlePgErr(err error, op string) error { var pgErr *pgconn.PgError if errors.As(err, &pgErr) { @@ -255,9 +286,9 @@ func ValidRole(role models.Role) error { switch role { case models.RoleAdmin: return nil - case models.RoleModerator: + case models.RoleTeacher: return nil - case models.RoleParticipant: + case models.RoleStudent: return nil } return errors.New("invalid role") diff --git a/internal/users/repository/valkey_repository.go b/internal/users/repository/valkey_repository.go index d725abd..18fc949 100644 --- a/internal/users/repository/valkey_repository.go +++ b/internal/users/repository/valkey_repository.go @@ -2,6 +2,9 @@ package repository import ( "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" "fmt" "git.sch9.ru/new_gate/ms-auth/internal/models" "git.sch9.ru/new_gate/ms-auth/pkg" @@ -23,31 +26,55 @@ func NewValkeyRepository(db valkey.Client) *ValkeyRepository { const sessionLifetime = time.Minute * 40 -func (r *ValkeyRepository) CreateSession(ctx context.Context, userId int32, role models.Role) (string, error) { +func sha256string(s string) string { + hasher := sha256.New() + hasher.Write([]byte(s)) + return hex.EncodeToString(hasher.Sum(nil)) +} + +func (r *ValkeyRepository) CreateSession(ctx context.Context, userId int32, role models.Role, userAgent, ip string) (*models.Session, error) { const op = "ValkeyRepository.CreateSession" - sessionData, sessionId, err := models.NewSession(userId, role) + session := &models.Session{ + Id: uuid.NewString(), + UserId: userId, + Role: role, + CreatedAt: time.Now(), + ExpiresAt: time.Now().Add(sessionLifetime), + UserAgent: userAgent, + Ip: ip, + } + + err := session.Valid() if err != nil { - return "", pkg.Wrap(pkg.ErrBadInput, err, op, "building session") + return nil, pkg.Wrap(pkg.ErrInternal, err, op, "validating session") + } + + userIdHash := sha256string(strconv.FormatInt(int64(userId), 10)) + sessionIdHash := sha256string(session.Id) + + sessionData, err := json.Marshal(session) + if err != nil { + return nil, pkg.Wrap(pkg.ErrInternal, err, op, "marshaling session") } resp := r.db.Do(ctx, r.db. B().Set(). - Key(fmt.Sprintf("userid:%d:sessionid:%s", userId, sessionId)). - Value(sessionData). - Ex(sessionLifetime). + Key(fmt.Sprintf("userid:%s:sessionid:%s", userIdHash, sessionIdHash)). + Value(string(sessionData)). + Exat(session.ExpiresAt). Build(), ) err = resp.Error() if err != nil { if valkey.IsValkeyNil(err) { - return "", pkg.Wrap(pkg.ErrBadInput, err, op, "nil response") + return nil, pkg.Wrap(pkg.ErrInternal, err, op, "nil response") } - return "", pkg.Wrap(pkg.ErrUnhandled, err, op, "unhandled valkey error") + return nil, pkg.Wrap(pkg.ErrUnhandled, err, op, "unhandled valkey error") } - return sessionId, nil + return session, nil } const ( @@ -67,11 +94,13 @@ func (r *ValkeyRepository) ReadSession(ctx context.Context, sessionId string) (* return nil, pkg.Wrap(pkg.ErrBadInput, err, op, "uuid validation") } + sessionIdHash := sha256string(sessionId) + resp := valkey.NewLuaScript(readSessionScript).Exec( ctx, r.db, nil, - []string{fmt.Sprintf("userid:*:sessionid:%s", sessionId)}, + []string{fmt.Sprintf("userid:*:sessionid:%s", sessionIdHash)}, ) if err = resp.Error(); err != nil { @@ -86,12 +115,20 @@ func (r *ValkeyRepository) ReadSession(ctx context.Context, sessionId string) (* return nil, pkg.Wrap(pkg.ErrInternal, err, op, "session storage corrupted") } - session, err := models.ParseSession(str) + var session models.Session + + err = json.Unmarshal([]byte(str), &session) + if err != nil { return nil, pkg.Wrap(pkg.ErrInternal, err, op, "session corrupted") } - return session, nil + err = session.Valid() + if err != nil { + return nil, pkg.Wrap(pkg.ErrInternal, err, op, "validating session") + } + + return &session, nil } const ( @@ -111,11 +148,13 @@ func (r *ValkeyRepository) UpdateSession(ctx context.Context, sessionId string) return pkg.Wrap(pkg.ErrBadInput, err, op, "uuid validation") } + sessionIdHash := sha256string(sessionId) + resp := valkey.NewLuaScript(updateSessionScript).Exec( ctx, r.db, nil, - []string{fmt.Sprintf("userid:*:sessionid:%s", sessionId), sessionLifetimeString}, + []string{fmt.Sprintf("userid:*:sessionid:%s", sessionIdHash), sessionLifetimeString}, ) err = resp.Error() @@ -140,11 +179,13 @@ func (r *ValkeyRepository) DeleteSession(ctx context.Context, sessionId string) return pkg.Wrap(pkg.ErrBadInput, err, op, "uuid validation") } + sessionIdHash := sha256string(sessionId) + resp := valkey.NewLuaScript(deleteSessionScript).Exec( ctx, r.db, nil, - []string{fmt.Sprintf("userid:*:sessionid:%s", sessionId)}, + []string{fmt.Sprintf("userid:*:sessionid:%s", sessionIdHash)}, ) err = resp.Error() @@ -175,11 +216,13 @@ return dels` func (r *ValkeyRepository) DeleteAllSessions(ctx context.Context, userId int32) error { const op = "ValkeyRepository.DeleteAllSessions" + userIdHash := sha256string(strconv.FormatInt(int64(userId), 10)) + resp := valkey.NewLuaScript(deleteUserSessionsScript).Exec( ctx, r.db, nil, - []string{fmt.Sprintf("userid:%d:sessionid:*", userId)}, + []string{fmt.Sprintf("userid:%s:sessionid:*", userIdHash)}, ) err := resp.Error() diff --git a/internal/users/usecase.go b/internal/users/usecase.go index 12724b6..e72924f 100644 --- a/internal/users/usecase.go +++ b/internal/users/usecase.go @@ -11,10 +11,10 @@ type UseCase interface { ReadUserByUsername(ctx context.Context, username string) (*models.User, error) UpdateUser(ctx context.Context, id int32, username *string, role *models.Role) error DeleteUser(ctx context.Context, id int32) error - CreateSession(ctx context.Context, userId int32, role models.Role) (string, error) + CreateSession(ctx context.Context, userId int32, role models.Role, userAgent, ip string) (*models.Session, error) ReadSession(ctx context.Context, sessionId string) (*models.Session, error) UpdateSession(ctx context.Context, sessionId string) error DeleteSession(ctx context.Context, sessionId string) error DeleteAllSessions(ctx context.Context, userId int32) error - Verify(ctx context.Context, sessionId string) (string, error) + ListUsers(ctx context.Context, page int32, pageSize int32) ([]*models.User, int32, error) } diff --git a/internal/users/usecase/usecase.go b/internal/users/usecase/usecase.go index 450eb5a..5ddcd67 100644 --- a/internal/users/usecase/usecase.go +++ b/internal/users/usecase/usecase.go @@ -7,9 +7,6 @@ import ( "git.sch9.ru/new_gate/ms-auth/internal/models" "git.sch9.ru/new_gate/ms-auth/internal/users" "git.sch9.ru/new_gate/ms-auth/pkg" - "github.com/golang-jwt/jwt/v4" - "github.com/google/uuid" - "time" ) type UseCase struct { @@ -30,20 +27,19 @@ func NewUseCase( } } +const ( + TokenKey = "token" +) + func (u *UseCase) CreateUser(ctx context.Context, username string, password string, role models.Role) (int32, error) { const op = "UseCase.CreateUser" - meId, ok := ctx.Value("userId").(int32) + token, ok := ctx.Value(TokenKey).(*models.JWT) if !ok { - return 0, pkg.Wrap(pkg.ErrUnauthenticated, nil, op, "no user id in context") + return 0, pkg.Wrap(pkg.ErrUnauthenticated, nil, op, "no token in context") } - me, err := u.userRepo.C().ReadUserById(ctx, meId) - if err != nil { - return 0, pkg.Wrap(nil, err, op, "can't read user by id") - } - - if !me.Role.AtLeast(models.RoleModerator) || me.Role.AtMost(role) && !me.Role.IsAdmin() { + if !token.Role.HasPermission(models.Create, models.ResourceAnotherUser) { return 0, pkg.Wrap(pkg.NoPermission, nil, op, "no permission") } @@ -58,6 +54,19 @@ func (u *UseCase) CreateUser(ctx context.Context, username string, password stri func (u *UseCase) ReadUserById(ctx context.Context, id int32) (*models.User, error) { const op = "UseCase.ReadUserById" + token, ok := ctx.Value(TokenKey).(*models.JWT) + if !ok { + return nil, pkg.Wrap(pkg.ErrUnauthenticated, nil, op, "no token in context") + } + + if token.UserId == id && !token.Role.HasPermission(models.Read, models.ResourceMeUser) { + return nil, pkg.Wrap(pkg.NoPermission, nil, op, "no permission") + } + + if token.UserId != id && !token.Role.HasPermission(models.Read, models.ResourceAnotherUser) { + return nil, pkg.Wrap(pkg.NoPermission, nil, op, "no permission") + } + user, err := u.userRepo.C().ReadUserById(ctx, id) if err != nil { return nil, pkg.Wrap(nil, err, op, "can't read user by id") @@ -65,6 +74,7 @@ func (u *UseCase) ReadUserById(ctx context.Context, id int32) (*models.User, err return user, nil } +// ReadUserByUsername is for login only. There are no permission checks! DO NOT USE IT AS AN ENDPOINT RESPONSE! func (u *UseCase) ReadUserByUsername(ctx context.Context, username string) (*models.User, error) { const op = "UseCase.ReadUserByUsername" @@ -78,38 +88,16 @@ func (u *UseCase) ReadUserByUsername(ctx context.Context, username string) (*mod func (u *UseCase) UpdateUser(ctx context.Context, id int32, username *string, role *models.Role) error { const op = "UseCase.UpdateUser" - meId, ok := ctx.Value("userId").(int32) + token, ok := ctx.Value(TokenKey).(*models.JWT) if !ok { - return pkg.Wrap(pkg.ErrUnauthenticated, nil, op, "no user id in context") + return pkg.Wrap(pkg.ErrUnauthenticated, nil, op, "no token in context") } - me, err := u.userRepo.C().ReadUserById(ctx, meId) - if err != nil { - return pkg.Wrap(nil, err, op, "can't read user by id") + if token.UserId == id && !token.Role.HasPermission(models.Update, models.ResourceMeUser) { + return pkg.Wrap(pkg.NoPermission, nil, op, "no permission") } - user, err := u.userRepo.C().ReadUserById(ctx, id) - if err != nil { - return pkg.Wrap(nil, err, op, "can't read user by id") - } - - hasPermission := func() bool { - if me.Id == user.Id && role != nil { - return false - } - if me.Role.IsAdmin() { - return true - } - if role != nil && me.Role.AtMost(*role) { - return false - } - if !me.Role.AtMost(user.Role) { - return true - } - return false - }() - - if !hasPermission { + if token.UserId != id && !token.Role.HasPermission(models.Update, models.ResourceAnotherUser) { return pkg.Wrap(pkg.NoPermission, nil, op, "no permission") } @@ -137,17 +125,16 @@ func (u *UseCase) UpdateUser(ctx context.Context, id int32, username *string, ro func (u *UseCase) DeleteUser(ctx context.Context, id int32) error { const op = "UseCase.DeleteUser" - userId, ok := ctx.Value("userId").(int32) + token, ok := ctx.Value(TokenKey).(*models.JWT) if !ok { - return pkg.Wrap(pkg.ErrUnauthenticated, nil, op, "no user id in context") + return pkg.Wrap(pkg.ErrUnauthenticated, nil, op, "no token in context") } - me, err := u.ReadUserById(ctx, userId) - if err != nil { - return pkg.Wrap(nil, err, op, "can't read user by id") + if token.UserId == id && !token.Role.HasPermission(models.Delete, models.ResourceMeUser) { + return pkg.Wrap(pkg.NoPermission, nil, op, "no permission") } - if me.Id == id || !me.Role.IsAdmin() { + if token.UserId != id && !token.Role.HasPermission(models.Delete, models.ResourceAnotherUser) { return pkg.Wrap(pkg.NoPermission, nil, op, "no permission") } @@ -173,17 +160,19 @@ func (u *UseCase) DeleteUser(ctx context.Context, id int32) error { return nil } -func (u *UseCase) CreateSession(ctx context.Context, userId int32, role models.Role) (string, error) { +// CreateSession is for login only. There are no permission checks! DO NOT USE IT AS AN ENDPOINT RESPONSE! +func (u *UseCase) CreateSession(ctx context.Context, userId int32, role models.Role, userAgent, ip string) (*models.Session, error) { const op = "UseCase.CreateSession" - sessionId, err := u.sessionRepo.CreateSession(ctx, userId, role) + session, err := u.sessionRepo.CreateSession(ctx, userId, role, userAgent, ip) if err != nil { - return "", pkg.Wrap(nil, err, op, "cannot create session") + return nil, pkg.Wrap(nil, err, op, "cannot create session") } - return sessionId, nil + return session, nil } +// ReadSession is for internal use only. There are no permission checks! DO NOT USE IT AS AN ENDPOINT RESPONSE! func (u *UseCase) ReadSession(ctx context.Context, sessionId string) (*models.Session, error) { const op = "UseCase.ReadSession" @@ -197,6 +186,19 @@ func (u *UseCase) ReadSession(ctx context.Context, sessionId string) (*models.Se func (u *UseCase) UpdateSession(ctx context.Context, sessionId string) error { const op = "UseCase.UpdateSession" + token, ok := ctx.Value(TokenKey).(*models.JWT) + if !ok { + return pkg.Wrap(pkg.ErrUnauthenticated, nil, op, "no token in context") + } + + if token.SessionId != sessionId { + return pkg.Wrap(pkg.NoPermission, nil, op, "no permission") + } + + if token.SessionId == sessionId && !token.Role.HasPermission(models.Update, models.ResourceOwnSession) { + return pkg.Wrap(pkg.NoPermission, nil, op, "no permission") + } + err := u.sessionRepo.UpdateSession(ctx, sessionId) if err != nil { return pkg.Wrap(nil, err, op, "cannot update session") @@ -207,6 +209,19 @@ func (u *UseCase) UpdateSession(ctx context.Context, sessionId string) error { func (u *UseCase) DeleteSession(ctx context.Context, sessionId string) error { const op = "UseCase.DeleteSession" + token, ok := ctx.Value(TokenKey).(*models.JWT) + if !ok { + return pkg.Wrap(pkg.ErrUnauthenticated, nil, op, "no token in context") + } + + if token.SessionId != sessionId { + return pkg.Wrap(pkg.NoPermission, nil, op, "no permission") + } + + if token.SessionId == sessionId && !token.Role.HasPermission(models.Delete, models.ResourceOwnSession) { + return pkg.Wrap(pkg.NoPermission, nil, op, "no permission") + } + err := u.sessionRepo.DeleteSession(ctx, sessionId) if err != nil { return pkg.Wrap(nil, err, op, "cannot delete session") @@ -217,6 +232,19 @@ func (u *UseCase) DeleteSession(ctx context.Context, sessionId string) error { func (u *UseCase) DeleteAllSessions(ctx context.Context, userId int32) error { const op = "UseCase.DeleteAllSessions" + token, ok := ctx.Value(TokenKey).(*models.JWT) + if !ok { + return pkg.Wrap(pkg.ErrUnauthenticated, nil, op, "no token in context") + } + + if token.UserId != userId { + return pkg.Wrap(pkg.NoPermission, nil, op, "no permission") + } + + if token.UserId == userId && !token.Role.HasPermission(models.Delete, models.ResourceOwnSession) { + return pkg.Wrap(pkg.NoPermission, nil, op, "no permission") + } + err := u.sessionRepo.DeleteAllSessions(ctx, userId) if err != nil { return pkg.Wrap(nil, err, op, "cannot delete all sessions") @@ -225,61 +253,21 @@ func (u *UseCase) DeleteAllSessions(ctx context.Context, userId int32) error { return nil } -type Token struct { - SessionId string `json:"sid"` - UserId int32 `json:"sub"` - Role models.Role `json:"rle"` - ExpiresAt time.Time `json:"exp"` - IssuedAt time.Time `json:"iat"` - NotBefore time.Time `json:"nbf"` -} +func (u *UseCase) ListUsers(ctx context.Context, page int32, pageSize int32) ([]*models.User, int32, error) { + const op = "UseCase.ListUsers" -func (t Token) Valid() error { - if err := uuid.Validate(t.SessionId); err != nil { - return err + token, ok := ctx.Value(TokenKey).(*models.JWT) + if !ok { + return nil, 0, pkg.Wrap(pkg.ErrUnauthenticated, nil, op, "no token in context") } - if t.UserId <= 0 { - return errors.New("invalid user id") - } - if t.Role <= 0 { - return errors.New("invalid role") - } - if t.ExpiresAt.IsZero() { - return errors.New("invalid exp") - } - if t.IssuedAt.IsZero() { - return errors.New("invalid iat") - } - if t.NotBefore.IsZero() { - return errors.New("invalid nbf") - } - return nil -} -func (u *UseCase) Verify(ctx context.Context, sessionId string) (string, error) { - const op = "UseCase.Verify" + if !token.Role.HasPermission(models.Read, models.ResourceAnotherUser) { + return nil, 0, pkg.Wrap(pkg.NoPermission, nil, op, "no permission") + } - session, err := u.sessionRepo.ReadSession(ctx, sessionId) + usersList, count, err := u.userRepo.C().ListUsers(ctx, page, pageSize) if err != nil { - return "", pkg.Wrap(nil, err, op, "cannot read session") + return nil, 0, pkg.Wrap(nil, err, op, "can't list users") } - - token := jwt.NewWithClaims( - jwt.SigningMethodHS256, - Token{ - SessionId: sessionId, - UserId: session.UserId, - Role: session.Role, - ExpiresAt: time.Now().Add(time.Hour * 24), - IssuedAt: time.Now(), - NotBefore: time.Now(), - }, - ) - - signedToken, err := token.SignedString([]byte(u.cfg.JWTSecret)) - if err != nil { - return "", pkg.Wrap(pkg.ErrInternal, err, op, "cannot sign token") - } - - return signedToken, nil + return usersList, count, nil } diff --git a/pkg/errors.go b/pkg/errors.go index 20e6efd..6efa146 100644 --- a/pkg/errors.go +++ b/pkg/errors.go @@ -3,8 +3,7 @@ package pkg import ( "errors" "fmt" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/status" + "net/http" ) var ( @@ -20,19 +19,19 @@ func Wrap(basic error, err error, op string, msg string) error { return errors.Join(basic, err, fmt.Errorf("during %s: %s", op, msg)) } -func ToGRPC(err error) error { +func ToREST(err error) int { switch { case errors.Is(err, ErrUnauthenticated): - return status.Errorf(codes.Unauthenticated, err.Error()) + return http.StatusUnauthorized case errors.Is(err, ErrBadInput): - return status.Errorf(codes.InvalidArgument, err.Error()) + return http.StatusBadRequest case errors.Is(err, ErrNotFound): - return status.Errorf(codes.NotFound, err.Error()) + return http.StatusNotFound case errors.Is(err, ErrInternal): - return status.Errorf(codes.Internal, err.Error()) + return http.StatusInternalServerError case errors.Is(err, NoPermission): - return status.Errorf(codes.PermissionDenied, err.Error()) + return http.StatusForbidden } - return status.Errorf(codes.Unknown, err.Error()) + return http.StatusInternalServerError } diff --git a/proto b/proto index 11644ad..b7ea2e6 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit 11644adc889ee8f0e0e90c11ccf386a081ee000f +Subproject commit b7ea2e6cc71f7393f9afe722204c5c33b6a6f2b6