feat(user): migrate from gRPC to REST

This commit is contained in:
Vyacheslav1557 2025-02-25 18:33:15 +05:00
parent 2b94be99e3
commit 7b8d15a2c9
20 changed files with 977 additions and 1150 deletions

View file

@ -1,19 +1,9 @@
tag = latest tag = latest
gen: gen:
@protoc --proto_path=proto --go_opt=paths=source_relative \ @oapi-codegen --config=config.yaml ./proto/user/v1/openapi.yaml
--go_out=proto --go-grpc_out=proto --grpc-gateway_out=proto \ dev: gen
proto/user/v1/user.proto
gen-openapi:
@protoc --proto_path=proto --openapi_out=proto/user/v1 \
proto/user/v1/user.proto
dev:
@make gen
@go run cmd/ms-auth/main.go @go run cmd/ms-auth/main.go
proxy: build: gen
@make gen
@go run cmd/ms-auth-proxy/main.go
build:
@make gen
@docker build . -t ms-auth:${tag} @docker build . -t ms-auth:${tag}
@#docker push ms-auth:${tag} @#docker push ms-auth:${tag}

View file

@ -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
}

View file

@ -5,17 +5,16 @@ import (
"fmt" "fmt"
"git.sch9.ru/new_gate/ms-auth/config" "git.sch9.ru/new_gate/ms-auth/config"
"git.sch9.ru/new_gate/ms-auth/internal/models" "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" usersRepository "git.sch9.ru/new_gate/ms-auth/internal/users/repository"
usersUseCase "git.sch9.ru/new_gate/ms-auth/internal/users/usecase" usersUseCase "git.sch9.ru/new_gate/ms-auth/internal/users/usecase"
"git.sch9.ru/new_gate/ms-auth/pkg" "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/ilyakaznacheev/cleanenv"
_ "github.com/jackc/pgx/v5/stdlib" _ "github.com/jackc/pgx/v5/stdlib"
"go.uber.org/zap" "go.uber.org/zap"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
"net"
"os" "os"
"os/signal" "os/signal"
"syscall" "syscall"
@ -23,42 +22,42 @@ import (
// InterceptorLogger adapts zap logger to interceptor logger. // InterceptorLogger adapts zap logger to interceptor logger.
// This code is simple enough to be copied and not imported. // This code is simple enough to be copied and not imported.
func InterceptorLogger(l *zap.Logger) logging.Logger { //func InterceptorLogger(l *zap.Logger) fiber.Handler {
return logging.LoggerFunc(func(ctx context.Context, lvl logging.Level, msg string, fields ...any) { // return func(ctx context.Context, lvl logging.Level, msg string, fields ...any) {
f := make([]zap.Field, 0, len(fields)/2) // f := make([]zap.Field, 0, len(fields)/2)
//
for i := 0; i < len(fields); i += 2 { // for i := 0; i < len(fields); i += 2 {
key := fields[i] // key := fields[i]
value := fields[i+1] // value := fields[i+1]
//
switch v := value.(type) { // switch v := value.(type) {
case string: // case string:
f = append(f, zap.String(key.(string), v)) // f = append(f, zap.String(key.(string), v))
case int: // case int:
f = append(f, zap.Int(key.(string), v)) // f = append(f, zap.Int(key.(string), v))
case bool: // case bool:
f = append(f, zap.Bool(key.(string), v)) // f = append(f, zap.Bool(key.(string), v))
default: // default:
f = append(f, zap.Any(key.(string), v)) // f = append(f, zap.Any(key.(string), v))
} // }
} // }
//
logger := l.WithOptions(zap.AddCallerSkip(1)).With(f...) // logger := l.WithOptions(zap.AddCallerSkip(1)).With(f...)
//
switch lvl { // switch lvl {
case logging.LevelDebug: // case logging.LevelDebug:
logger.Debug(msg) // logger.Debug(msg)
case logging.LevelInfo: // case logging.LevelInfo:
logger.Info(msg) // logger.Info(msg)
case logging.LevelWarn: // case logging.LevelWarn:
logger.Warn(msg) // logger.Warn(msg)
case logging.LevelError: // case logging.LevelError:
logger.Error(msg) // logger.Error(msg)
default: // default:
panic(fmt.Sprintf("unknown level %v", lvl)) // panic(fmt.Sprintf("unknown level %v", lvl))
} // }
}) // })
} //}
func main() { func main() {
var cfg config.Config var cfg config.Config
@ -101,22 +100,25 @@ func main() {
sessionRepo := usersRepository.NewValkeyRepository(vk) sessionRepo := usersRepository.NewValkeyRepository(vk)
userUC := usersUseCase.NewUseCase(userRepo, sessionRepo, cfg) userUC := usersUseCase.NewUseCase(userRepo, sessionRepo, cfg)
gserver := grpc.NewServer(grpc.ChainUnaryInterceptor( server := fiber.New()
logging.UnaryServerInterceptor(InterceptorLogger(logger)),
))
defer gserver.GracefulStop()
usersDelivery.NewUserHandlers(gserver, userUC) userv1.RegisterHandlersWithOptions(server, rest.NewUserHandlers(userUC, cfg.JWTSecret), userv1.FiberServerOptions{
reflection.Register(gserver) Middlewares: []userv1.MiddlewareFunc{
fiberlogger.New(),
ln, err := net.Listen("tcp", cfg.Address) rest.AuthMiddleware(cfg.JWTSecret, userUC),
if err != nil { //cors.New(cors.Config{
panic(err) // AllowOrigins: "http://localhost:3000",
} // AllowMethods: "GET,POST,PUT,DELETE,OPTIONS",
// AllowHeaders: "Content-Type,Set-Cookie,Credentials",
// AllowCredentials: true,
//}),
},
})
go func() { go func() {
if err = gserver.Serve(ln); err != nil { err := server.Listen(cfg.Address)
panic(err) if err != nil {
logger.Fatal(fmt.Sprintf("error starting server: %s", err.Error()))
} }
}() }()

5
config.yaml Normal file
View file

@ -0,0 +1,5 @@
package: userv1
generate:
fiber-server: true
models: true
output: ./proto/user/v1/user.go

58
go.mod
View file

@ -4,42 +4,76 @@ go 1.23.2
require ( require (
github.com/DATA-DOG/go-sqlmock v1.5.2 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/golang-jwt/jwt/v4 v4.5.1
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.2.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/ilyakaznacheev/cleanenv v1.5.0
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 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/stretchr/testify v1.10.0
github.com/valkey-io/valkey-go v1.0.47 github.com/valkey-io/valkey-go v1.0.47
github.com/valkey-io/valkey-go/mock v1.0.47 github.com/valkey-io/valkey-go/mock v1.0.47
go.uber.org/mock v0.4.0 go.uber.org/mock v0.4.0
go.uber.org/zap v1.27.0 go.uber.org/zap v1.27.0
golang.org/x/crypto v0.31.0 golang.org/x/crypto v0.32.0
google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 google.golang.org/grpc v1.70.0
google.golang.org/grpc v1.68.0 google.golang.org/protobuf v1.36.3
google.golang.org/protobuf v1.35.2
) )
require ( 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/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/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/puddle/v2 v2.2.1 // 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/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/prometheus/client_golang v1.20.5 // indirect
github.com/rs/cors v1.11.1 // 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 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/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 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 ( 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/jackc/pgx/v5 v5.6.0
github.com/jmoiron/sqlx v1.4.0 github.com/jmoiron/sqlx v1.4.0
github.com/joho/godotenv v1.5.1 // indirect github.com/joho/godotenv v1.5.1 // indirect

175
go.sum
View file

@ -1,28 +1,79 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 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.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 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= 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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/dgraph-io/badger/v4 v4.5.1 h1:7DCIXrQjo1LKmM96YD+hLVJ2EEsyyoWxJfpdd56HLps=
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/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 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= 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 h1:JdqV9zKUdtaa9gdPlywC3aeoEsR681PlKC+4F5gQgeo=
github.com/golang-jwt/jwt/v4 v4.5.1/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= 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 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 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 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 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 h1:kQ0NI7W1B3HwiN5gAYtY+XFItDPbLBwYRxAqbFTyDes=
github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.2.0/go.mod h1:zrT2dxOAjNFPRGjTUe2Xmb4q4YdUwVvQFV6xiCSf+z0= 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.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg=
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/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ=
github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4= github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4=
github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk= github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk=
github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 h1:Dj0L5fhJ9F82ZJyVOmBx6msDp/kfd1t9GRfny/mfJA0= 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/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 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 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/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 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 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 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 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 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= 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/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.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.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 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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 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 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 h1:fQZUJrJEx4IG7vH1CjSSqPmx+5Gd6cwwdr7gcDDAIe0=
github.com/valkey-io/valkey-go/mock v1.0.47/go.mod h1:k+lHD29cYer1FO+/HyxWLDUU1JakG6uQ/VgR1OGqe+I= 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 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= 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/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 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= 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.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 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 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= 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/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= 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 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 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= golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697/go.mod h1:+D9ySVjN8nY8YCVjc5O7PZDIdZporIDY3KaGfJunh88= golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 h1:LWZqQOEjDyONlF1H6afSWpAL/znlREo2tHfLoe+8LMA= google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:Ic02D47M+zbarjYYUlK57y316f2MoN0gjAwI3f2S95o=
google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0= google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA= google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 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 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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 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= 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 h1:slmdOY3vp8a7KQbHkL+FLbvbkgMqmXojpFUO/jENuqQ=
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3/go.mod h1:oVgVk4OWVDi43qWBEyGhXgYxt7+ED4iYNpTngSLX2Iw= 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=

View file

@ -1,19 +1,19 @@
package models package models
import ( import (
"encoding/json"
"errors" "errors"
"github.com/google/uuid" "github.com/google/uuid"
"time" "time"
) )
type Session struct { type Session struct {
Id string `json:"id" db:"id"` Id string `json:"id"`
UserId int32 `json:"user_id" db:"user_id"` UserId int32 `json:"user_id"`
Role Role `json:"role" db:"role"` Role Role `json:"role"`
CreatedAt time.Time `json:"created_at" db:"created_at"` CreatedAt time.Time `json:"created_at"`
//UserAgent string `json:"user_agent"` ExpiresAt time.Time `json:"expires_at"`
//Ip string `json:"ip"` UserAgent string `json:"user_agent"`
Ip string `json:"ip"`
} }
func (s Session) Valid() error { func (s Session) Valid() error {
@ -26,40 +26,46 @@ func (s Session) Valid() error {
if s.CreatedAt.IsZero() { if s.CreatedAt.IsZero() {
return errors.New("empty created at") return errors.New("empty created at")
} }
if !s.Role.IsAdmin() && !s.Role.IsModerator() && !s.Role.IsParticipant() { if s.ExpiresAt.IsZero() {
return errors.New("invalid role") 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 return nil
} }
func NewSession(userId int32, role Role) (string, string, error) { type JWT struct {
s := &Session{ SessionId string `json:"session_id"`
Id: uuid.NewString(), UserId int32 `json:"user_id"`
UserId: userId, Role Role `json:"role"`
Role: role, ExpiresAt int64 `json:"exp"`
CreatedAt: time.Now(), IssuedAt int64 `json:"iat"`
} NotBefore int64 `json:"nbf"`
if err := s.Valid(); err != nil { Permissions []grant `json:"permissions"`
return "", "", err
} }
b, err := json.Marshal(s) func (j JWT) Valid() error {
if err != nil { if uuid.Validate(j.SessionId) != nil {
return "", "", err return errors.New("invalid session id")
} }
if j.UserId == 0 {
return string(b), s.Id, nil return errors.New("empty user id")
} }
if j.ExpiresAt == 0 {
func ParseSession(s string) (*Session, error) { return errors.New("empty expires at")
sess := &Session{}
if err := json.Unmarshal([]byte(s), sess); err != nil {
return nil, err
} }
if j.IssuedAt == 0 {
if err := sess.Valid(); err != nil { return errors.New("empty issued at")
return nil, err
} }
if j.NotBefore == 0 {
return sess, nil return errors.New("empty not before")
}
if len(j.Permissions) == 0 {
return errors.New("empty permissions")
}
return nil
} }

View file

@ -1,7 +1,9 @@
package models package models
import ( import (
"errors" "context"
"encoding/json"
"github.com/open-policy-agent/opa/v1/rego"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"time" "time"
) )
@ -9,30 +11,90 @@ import (
type Role int32 type Role int32
const ( const (
RoleParticipant Role = 0 RoleGuest Role = -1
RoleModerator Role = 1 RoleStudent Role = 0
RoleTeacher Role = 1
RoleAdmin Role = 2 RoleAdmin Role = 2
) )
func (role Role) IsAdmin() bool { func (r Role) String() string {
return role == RoleAdmin switch r {
case RoleGuest:
return "guest"
case RoleStudent:
return "student"
case RoleTeacher:
return "teacher"
case RoleAdmin:
return "admin"
} }
func (role Role) IsModerator() bool { panic("invalid role")
return role == RoleModerator
} }
func (role Role) IsParticipant() bool { type Action string
return role == RoleParticipant
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) AtLeast(other Role) bool { var Grants = map[string][]grant{
return role >= other 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) AtMost(other Role) bool { const module = `package app.rbac
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 { type User struct {
Id int32 `db:"id"` Id int32 `db:"id"`
@ -43,10 +105,61 @@ type User struct {
Role Role `db:"role"` 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)) err := bcrypt.CompareHashAndPassword([]byte(user.HashedPassword), []byte(password))
if err != nil { 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
} }

View file

@ -1,19 +1,21 @@
package users package users
import ( import (
"context"
userv1 "git.sch9.ru/new_gate/ms-auth/proto/user/v1" 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 { type UserHandlers interface {
CreateUser(ctx context.Context, req *userv1.CreateUserRequest) (*userv1.CreateUserResponse, error) ListSessions(c *fiber.Ctx) error
GetUser(ctx context.Context, req *userv1.GetUserRequest) (*userv1.GetUserResponse, error) CompleteLogout(c *fiber.Ctx) error
UpdateUser(ctx context.Context, req *userv1.UpdateUserRequest) (*emptypb.Empty, error) Login(c *fiber.Ctx) error
DeleteUser(ctx context.Context, req *userv1.DeleteUserRequest) (*emptypb.Empty, error) Logout(c *fiber.Ctx) error
Login(ctx context.Context, req *userv1.LoginRequest) (*emptypb.Empty, error) Refresh(c *fiber.Ctx) error
Verify(ctx context.Context, req *emptypb.Empty) (*emptypb.Empty, error) Verify(c *fiber.Ctx) error
Refresh(ctx context.Context, req *emptypb.Empty) (*emptypb.Empty, error) ListUsers(c *fiber.Ctx, params userv1.ListUsersParams) error
Logout(ctx context.Context, req *emptypb.Empty) (*emptypb.Empty, error) CreateUser(c *fiber.Ctx) error
CompleteLogout(ctx context.Context, req *emptypb.Empty) (*emptypb.Empty, 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
} }

View file

@ -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
}

View file

@ -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)
})
}

View file

@ -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
}

View file

@ -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()
}
}

View file

@ -11,6 +11,7 @@ type Caller interface {
ReadUserById(ctx context.Context, id int32) (*models.User, error) ReadUserById(ctx context.Context, id int32) (*models.User, error)
UpdateUser(ctx context.Context, id int32, username *string, role *models.Role) error UpdateUser(ctx context.Context, id int32, username *string, role *models.Role) error
DeleteUser(ctx context.Context, id int32) error DeleteUser(ctx context.Context, id int32) error
ListUsers(ctx context.Context, page int32, pageSize int32) ([]*models.User, int32, error)
} }
type TxCaller interface { type TxCaller interface {
@ -25,7 +26,7 @@ type PgRepository interface {
} }
type ValkeyRepository 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) ReadSession(ctx context.Context, sessionId string) (*models.Session, error)
UpdateSession(ctx context.Context, sessionId string) error UpdateSession(ctx context.Context, sessionId string) error
DeleteSession(ctx context.Context, sessionId string) error DeleteSession(ctx context.Context, sessionId string) error

View file

@ -47,6 +47,7 @@ type TxOrDB interface {
GetContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error GetContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error
QueryxContext(ctx context.Context, query string, args ...interface{}) (*sqlx.Rows, error) QueryxContext(ctx context.Context, query string, args ...interface{}) (*sqlx.Rows, error)
ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, 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 { type Caller struct {
@ -202,6 +203,36 @@ func (c *Caller) DeleteUser(ctx context.Context, id int32) error {
return nil 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 { func handlePgErr(err error, op string) error {
var pgErr *pgconn.PgError var pgErr *pgconn.PgError
if errors.As(err, &pgErr) { if errors.As(err, &pgErr) {
@ -255,9 +286,9 @@ func ValidRole(role models.Role) error {
switch role { switch role {
case models.RoleAdmin: case models.RoleAdmin:
return nil return nil
case models.RoleModerator: case models.RoleTeacher:
return nil return nil
case models.RoleParticipant: case models.RoleStudent:
return nil return nil
} }
return errors.New("invalid role") return errors.New("invalid role")

View file

@ -2,6 +2,9 @@ package repository
import ( import (
"context" "context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt" "fmt"
"git.sch9.ru/new_gate/ms-auth/internal/models" "git.sch9.ru/new_gate/ms-auth/internal/models"
"git.sch9.ru/new_gate/ms-auth/pkg" "git.sch9.ru/new_gate/ms-auth/pkg"
@ -23,31 +26,55 @@ func NewValkeyRepository(db valkey.Client) *ValkeyRepository {
const sessionLifetime = time.Minute * 40 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" 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 { 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. resp := r.db.Do(ctx, r.db.
B().Set(). B().Set().
Key(fmt.Sprintf("userid:%d:sessionid:%s", userId, sessionId)). Key(fmt.Sprintf("userid:%s:sessionid:%s", userIdHash, sessionIdHash)).
Value(sessionData). Value(string(sessionData)).
Ex(sessionLifetime). Exat(session.ExpiresAt).
Build(), Build(),
) )
err = resp.Error() err = resp.Error()
if err != nil { if err != nil {
if valkey.IsValkeyNil(err) { 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 ( const (
@ -67,11 +94,13 @@ func (r *ValkeyRepository) ReadSession(ctx context.Context, sessionId string) (*
return nil, pkg.Wrap(pkg.ErrBadInput, err, op, "uuid validation") return nil, pkg.Wrap(pkg.ErrBadInput, err, op, "uuid validation")
} }
sessionIdHash := sha256string(sessionId)
resp := valkey.NewLuaScript(readSessionScript).Exec( resp := valkey.NewLuaScript(readSessionScript).Exec(
ctx, ctx,
r.db, r.db,
nil, nil,
[]string{fmt.Sprintf("userid:*:sessionid:%s", sessionId)}, []string{fmt.Sprintf("userid:*:sessionid:%s", sessionIdHash)},
) )
if err = resp.Error(); err != nil { 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") 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 { if err != nil {
return nil, pkg.Wrap(pkg.ErrInternal, err, op, "session corrupted") 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 ( const (
@ -111,11 +148,13 @@ func (r *ValkeyRepository) UpdateSession(ctx context.Context, sessionId string)
return pkg.Wrap(pkg.ErrBadInput, err, op, "uuid validation") return pkg.Wrap(pkg.ErrBadInput, err, op, "uuid validation")
} }
sessionIdHash := sha256string(sessionId)
resp := valkey.NewLuaScript(updateSessionScript).Exec( resp := valkey.NewLuaScript(updateSessionScript).Exec(
ctx, ctx,
r.db, r.db,
nil, nil,
[]string{fmt.Sprintf("userid:*:sessionid:%s", sessionId), sessionLifetimeString}, []string{fmt.Sprintf("userid:*:sessionid:%s", sessionIdHash), sessionLifetimeString},
) )
err = resp.Error() 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") return pkg.Wrap(pkg.ErrBadInput, err, op, "uuid validation")
} }
sessionIdHash := sha256string(sessionId)
resp := valkey.NewLuaScript(deleteSessionScript).Exec( resp := valkey.NewLuaScript(deleteSessionScript).Exec(
ctx, ctx,
r.db, r.db,
nil, nil,
[]string{fmt.Sprintf("userid:*:sessionid:%s", sessionId)}, []string{fmt.Sprintf("userid:*:sessionid:%s", sessionIdHash)},
) )
err = resp.Error() err = resp.Error()
@ -175,11 +216,13 @@ return dels`
func (r *ValkeyRepository) DeleteAllSessions(ctx context.Context, userId int32) error { func (r *ValkeyRepository) DeleteAllSessions(ctx context.Context, userId int32) error {
const op = "ValkeyRepository.DeleteAllSessions" const op = "ValkeyRepository.DeleteAllSessions"
userIdHash := sha256string(strconv.FormatInt(int64(userId), 10))
resp := valkey.NewLuaScript(deleteUserSessionsScript).Exec( resp := valkey.NewLuaScript(deleteUserSessionsScript).Exec(
ctx, ctx,
r.db, r.db,
nil, nil,
[]string{fmt.Sprintf("userid:%d:sessionid:*", userId)}, []string{fmt.Sprintf("userid:%s:sessionid:*", userIdHash)},
) )
err := resp.Error() err := resp.Error()

View file

@ -11,10 +11,10 @@ type UseCase interface {
ReadUserByUsername(ctx context.Context, username string) (*models.User, error) ReadUserByUsername(ctx context.Context, username string) (*models.User, error)
UpdateUser(ctx context.Context, id int32, username *string, role *models.Role) error UpdateUser(ctx context.Context, id int32, username *string, role *models.Role) error
DeleteUser(ctx context.Context, id int32) 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) ReadSession(ctx context.Context, sessionId string) (*models.Session, error)
UpdateSession(ctx context.Context, sessionId string) error UpdateSession(ctx context.Context, sessionId string) error
DeleteSession(ctx context.Context, sessionId string) error DeleteSession(ctx context.Context, sessionId string) error
DeleteAllSessions(ctx context.Context, userId int32) 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)
} }

View file

@ -7,9 +7,6 @@ import (
"git.sch9.ru/new_gate/ms-auth/internal/models" "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/internal/users"
"git.sch9.ru/new_gate/ms-auth/pkg" "git.sch9.ru/new_gate/ms-auth/pkg"
"github.com/golang-jwt/jwt/v4"
"github.com/google/uuid"
"time"
) )
type UseCase struct { 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) { func (u *UseCase) CreateUser(ctx context.Context, username string, password string, role models.Role) (int32, error) {
const op = "UseCase.CreateUser" const op = "UseCase.CreateUser"
meId, ok := ctx.Value("userId").(int32) token, ok := ctx.Value(TokenKey).(*models.JWT)
if !ok { 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 !token.Role.HasPermission(models.Create, models.ResourceAnotherUser) {
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() {
return 0, pkg.Wrap(pkg.NoPermission, nil, op, "no permission") 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) { func (u *UseCase) ReadUserById(ctx context.Context, id int32) (*models.User, error) {
const op = "UseCase.ReadUserById" 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) user, err := u.userRepo.C().ReadUserById(ctx, id)
if err != nil { if err != nil {
return nil, pkg.Wrap(nil, err, op, "can't read user by id") 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 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) { func (u *UseCase) ReadUserByUsername(ctx context.Context, username string) (*models.User, error) {
const op = "UseCase.ReadUserByUsername" 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 { func (u *UseCase) UpdateUser(ctx context.Context, id int32, username *string, role *models.Role) error {
const op = "UseCase.UpdateUser" const op = "UseCase.UpdateUser"
meId, ok := ctx.Value("userId").(int32) token, ok := ctx.Value(TokenKey).(*models.JWT)
if !ok { 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 token.UserId == id && !token.Role.HasPermission(models.Update, models.ResourceMeUser) {
if err != nil { return pkg.Wrap(pkg.NoPermission, nil, op, "no permission")
return pkg.Wrap(nil, err, op, "can't read user by id")
} }
user, err := u.userRepo.C().ReadUserById(ctx, id) if token.UserId != id && !token.Role.HasPermission(models.Update, models.ResourceAnotherUser) {
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 {
return pkg.Wrap(pkg.NoPermission, nil, op, "no permission") 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 { func (u *UseCase) DeleteUser(ctx context.Context, id int32) error {
const op = "UseCase.DeleteUser" const op = "UseCase.DeleteUser"
userId, ok := ctx.Value("userId").(int32) token, ok := ctx.Value(TokenKey).(*models.JWT)
if !ok { 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 token.UserId == id && !token.Role.HasPermission(models.Delete, models.ResourceMeUser) {
if err != nil { return pkg.Wrap(pkg.NoPermission, nil, op, "no permission")
return pkg.Wrap(nil, err, op, "can't read user by id")
} }
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") 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 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" 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 { 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) { func (u *UseCase) ReadSession(ctx context.Context, sessionId string) (*models.Session, error) {
const op = "UseCase.ReadSession" 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 { func (u *UseCase) UpdateSession(ctx context.Context, sessionId string) error {
const op = "UseCase.UpdateSession" 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) err := u.sessionRepo.UpdateSession(ctx, sessionId)
if err != nil { if err != nil {
return pkg.Wrap(nil, err, op, "cannot update session") 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 { func (u *UseCase) DeleteSession(ctx context.Context, sessionId string) error {
const op = "UseCase.DeleteSession" 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) err := u.sessionRepo.DeleteSession(ctx, sessionId)
if err != nil { if err != nil {
return pkg.Wrap(nil, err, op, "cannot delete session") 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 { func (u *UseCase) DeleteAllSessions(ctx context.Context, userId int32) error {
const op = "UseCase.DeleteAllSessions" 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) err := u.sessionRepo.DeleteAllSessions(ctx, userId)
if err != nil { if err != nil {
return pkg.Wrap(nil, err, op, "cannot delete all sessions") 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 return nil
} }
type Token struct { func (u *UseCase) ListUsers(ctx context.Context, page int32, pageSize int32) ([]*models.User, int32, error) {
SessionId string `json:"sid"` const op = "UseCase.ListUsers"
UserId int32 `json:"sub"`
Role models.Role `json:"rle"` token, ok := ctx.Value(TokenKey).(*models.JWT)
ExpiresAt time.Time `json:"exp"` if !ok {
IssuedAt time.Time `json:"iat"` return nil, 0, pkg.Wrap(pkg.ErrUnauthenticated, nil, op, "no token in context")
NotBefore time.Time `json:"nbf"`
} }
func (t Token) Valid() error { if !token.Role.HasPermission(models.Read, models.ResourceAnotherUser) {
if err := uuid.Validate(t.SessionId); err != nil { return nil, 0, pkg.Wrap(pkg.NoPermission, nil, op, "no permission")
return err
}
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) { usersList, count, err := u.userRepo.C().ListUsers(ctx, page, pageSize)
const op = "UseCase.Verify"
session, err := u.sessionRepo.ReadSession(ctx, sessionId)
if err != nil { if err != nil {
return "", pkg.Wrap(nil, err, op, "cannot read session") return nil, 0, pkg.Wrap(nil, err, op, "can't list users")
} }
return usersList, count, nil
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
} }

View file

@ -3,8 +3,7 @@ package pkg
import ( import (
"errors" "errors"
"fmt" "fmt"
"google.golang.org/grpc/codes" "net/http"
"google.golang.org/grpc/status"
) )
var ( 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)) return errors.Join(basic, err, fmt.Errorf("during %s: %s", op, msg))
} }
func ToGRPC(err error) error { func ToREST(err error) int {
switch { switch {
case errors.Is(err, ErrUnauthenticated): case errors.Is(err, ErrUnauthenticated):
return status.Errorf(codes.Unauthenticated, err.Error()) return http.StatusUnauthorized
case errors.Is(err, ErrBadInput): case errors.Is(err, ErrBadInput):
return status.Errorf(codes.InvalidArgument, err.Error()) return http.StatusBadRequest
case errors.Is(err, ErrNotFound): case errors.Is(err, ErrNotFound):
return status.Errorf(codes.NotFound, err.Error()) return http.StatusNotFound
case errors.Is(err, ErrInternal): case errors.Is(err, ErrInternal):
return status.Errorf(codes.Internal, err.Error()) return http.StatusInternalServerError
case errors.Is(err, NoPermission): 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
} }

2
proto

@ -1 +1 @@
Subproject commit 11644adc889ee8f0e0e90c11ccf386a081ee000f Subproject commit b7ea2e6cc71f7393f9afe722204c5c33b6a6f2b6