some refactoring started

This commit is contained in:
r8zavetr8v 2024-12-29 12:38:20 -08:00
parent 5ce5cce076
commit 640255357e
10 changed files with 525 additions and 30 deletions

View File

@ -18,7 +18,7 @@ func main() {
plugin.MustNewPluginLoader(ctx, 8081, plugin.NewPluginStore()).
Run(ctx)
pg := postgres.New(ctx, "postgres://draincloud:draincloud-rw-password-123@192.168.1.68:5433/draincloud?sslmode=disable")
pg := postgres.New(ctx, "postgres://draincloud:mysuperstrongpassword@127.0.0.1:5432/draincloud?sslmode=disable")
// TODO move cron on a separate job (k8s cronjob / docker cron)
// cleanupSessionsCron := cleanupsessions.New(pg)

View File

@ -13,38 +13,38 @@ services:
networks:
- draincloud-pg
rw_2:
image: postgres:17
container_name: draincloud-db-rw-2
ports:
- 5433:5432
environment:
- POSTGRES_USER=draincloud
- POSTGRES_PASSWORD=mysuperstrongpassword
- POSTGRES_DB=draincloud
volumes:
- draincloud-rw-2:/var/lib/postgresql/data
networks:
- draincloud-pg
# rw_2:
# image: postgres:17
# container_name: draincloud-db-rw-2
# ports:
# - 5433:5432
# environment:
# - POSTGRES_USER=draincloud
# - POSTGRES_PASSWORD=mysuperstrongpassword
# - POSTGRES_DB=draincloud
# volumes:
# - draincloud-rw-2:/var/lib/postgresql/data
# networks:
# - draincloud-pg
rw_3:
image: postgres:17
container_name: draincloud-db-rw-3
ports:
- 5434:5432
environment:
- POSTGRES_USER=draincloud
- POSTGRES_PASSWORD=mysuperstrongpassword
- POSTGRES_DB=draincloud
volumes:
- draincloud-rw-3:/var/lib/postgresql/data
networks:
- draincloud-pg
# rw_3:
# image: postgres:17
# container_name: draincloud-db-rw-3
# ports:
# - 5434:5432
# environment:
# - POSTGRES_USER=draincloud
# - POSTGRES_PASSWORD=mysuperstrongpassword
# - POSTGRES_DB=draincloud
# volumes:
# - draincloud-rw-3:/var/lib/postgresql/data
# networks:
# - draincloud-pg
volumes:
draincloud-rw-1: {}
draincloud-rw-2: {}
draincloud-rw-3: {}
# draincloud-rw-2: {}
# draincloud-rw-3: {}
networks:
draincloud-pg: {}

View File

@ -0,0 +1,63 @@
package common
import (
"fmt"
"net/http"
"sync"
"git.optclblast.xyz/draincloud/draincloud-core/internal/storage/models"
"github.com/google/uuid"
)
type RequestPool struct {
sp sync.Pool
}
type Request struct {
ID string
Session *models.Session
User *models.User
ResolveValues sync.Map
Metadata map[string]any
Body []byte
}
// NewRequest builds a new *Request struct from raw http Request. No auth data validated.
func NewRequest(pool *RequestPool, req *http.Request) *Request {
out := pool.sp.Get().(*Request)
cookies := req.Cookies()
headers := req.Header
out.Metadata = make(map[string]any, len(cookies))
for _, cookie := range cookies {
out.Metadata[cookie.Name] = cookie.Value
}
for hname, hval := range headers {
out.Metadata[hname] = hval
}
reqID := uuid.NewString()
out.ID = reqID
return out
}
func GetValue[T any](vals map[string]any, key string) (T, error) {
var out T
if vals == nil {
return out, fmt.Errorf("nil vals map")
}
rawVal, ok := vals[key]
if !ok {
return out, fmt.Errorf("value not found in resolve values set")
}
out, ok = rawVal.(T)
if !ok {
return out, fmt.Errorf("type of a value is unexpected")
}
return out, nil
}

View File

@ -0,0 +1,250 @@
package common
import (
"reflect"
"testing"
)
func TestGetValue_string(t *testing.T) {
t.Parallel()
type args struct {
vals map[string]any
key string
}
tests := []struct {
name string
args args
want string
wantErr bool
}{
{
name: "ok",
args: args{
vals: map[string]any{
"1": "123",
"2": "234",
},
key: "1",
},
want: "123",
wantErr: false,
},
{
name: "value not presented",
args: args{
vals: map[string]any{
"1": "123",
"2": "234",
},
key: "3",
},
want: "",
wantErr: true,
},
{
name: "nil map",
args: args{
vals: nil,
key: "1",
},
want: "",
wantErr: true,
},
{
name: "invalid type",
args: args{
vals: map[string]any{
"1": "123",
"2": 234,
},
key: "2",
},
want: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := GetValue[string](tt.args.vals, tt.args.key)
if (err != nil) != tt.wantErr {
t.Errorf("GetValue() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("GetValue() = %v, want %v", got, tt.want)
}
})
}
}
func TestGetValue_struct(t *testing.T) {
t.Parallel()
type val struct {
a int
b string
c bool
}
type args struct {
vals map[string]any
key string
}
tests := []struct {
name string
args args
want val
wantErr bool
}{
{
name: "ok",
args: args{
vals: map[string]any{
"1": val{
a: 1,
b: "2",
c: true,
},
"2": "234",
},
key: "1",
},
want: val{
a: 1,
b: "2",
c: true,
},
wantErr: false,
},
{
name: "value not presented",
args: args{
vals: map[string]any{
"1": "123",
"2": "234",
},
key: "3",
},
want: val{},
wantErr: true,
},
{
name: "nil map",
args: args{
vals: nil,
key: "1",
},
want: val{},
wantErr: true,
},
{
name: "invalid type",
args: args{
vals: map[string]any{
"1": "123",
"2": 234,
},
key: "2",
},
want: val{},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := GetValue[val](tt.args.vals, tt.args.key)
if (err != nil) != tt.wantErr {
t.Errorf("GetValue() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("GetValue() = %v, want %v", got, tt.want)
}
})
}
}
func TestGetValue_structptr(t *testing.T) {
t.Parallel()
type val struct {
a int
b string
c bool
}
type args struct {
vals map[string]any
key string
}
tests := []struct {
name string
args args
want *val
wantErr bool
}{
{
name: "ok",
args: args{
vals: map[string]any{
"1": &val{
a: 1,
b: "2",
c: true,
},
"2": "234",
},
key: "1",
},
want: &val{
a: 1,
b: "2",
c: true,
},
wantErr: false,
},
{
name: "value not presented",
args: args{
vals: map[string]any{
"1": "123",
"2": "234",
},
key: "3",
},
want: nil,
wantErr: true,
},
{
name: "nil map",
args: args{
vals: nil,
key: "1",
},
want: nil,
wantErr: true,
},
{
name: "invalid type",
args: args{
vals: map[string]any{
"1": "123",
"2": 234,
},
key: "2",
},
want: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := GetValue[*val](tt.args.vals, tt.args.key)
if (err != nil) != tt.wantErr {
t.Errorf("GetValue() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("GetValue() = %v, want %v", got, tt.want)
}
})
}
}

9
internal/errs/errors.go Normal file
View File

@ -0,0 +1,9 @@
package errs
import "errors"
var (
ErrorUnauthorized = errors.New("unauthorized")
ErrorAccessDenied = errors.New("access denied")
ErrorSessionExpired = errors.New("session expired")
)

View File

@ -0,0 +1,17 @@
package handler
import (
"context"
"git.optclblast.xyz/draincloud/draincloud-core/internal/common"
)
type Writer interface {
Write(ctx context.Context, resp any)
}
type Handler struct {
Name string
RequiredResolveParams []string
ProcessFn func(ctx context.Context, req *common.Request, w Writer) error
}

View File

@ -1 +0,0 @@
package jetstream

View File

@ -0,0 +1,51 @@
package processor
import (
"errors"
"net/http"
"git.optclblast.xyz/draincloud/draincloud-core/internal/common"
"git.optclblast.xyz/draincloud/draincloud-core/internal/domain"
"git.optclblast.xyz/draincloud/draincloud-core/internal/errs"
"git.optclblast.xyz/draincloud/draincloud-core/internal/handler"
"git.optclblast.xyz/draincloud/draincloud-core/internal/storage"
"github.com/gin-gonic/gin"
)
type Processor struct {
rp *common.RequestPool
authStorage storage.AuthStorage
}
func (p *Processor) Process(handler *handler.Handler) gin.HandlerFunc {
return func(ctx *gin.Context) {
//req := common.NewRequest(p.rp, ctx.Request)
// if handler.WithAuth {
// if err := p.authorize(ctx, req); err != nil {
// p.writeError(ctx, err)
// return
// }
// }
}
}
func (p *Processor) writeError(ctx *gin.Context, err error) {
switch {
case errors.Is(err, errs.ErrorAccessDenied):
ctx.JSON(http.StatusInternalServerError, domain.ErrorJson{
Code: http.StatusForbidden,
Message: err.Error(),
})
case errors.Is(err, errs.ErrorSessionExpired):
ctx.JSON(http.StatusInternalServerError, domain.ErrorJson{
Code: http.StatusForbidden,
Message: err.Error(),
})
default:
ctx.JSON(http.StatusInternalServerError, domain.ErrorJson{
Code: http.StatusInternalServerError,
Message: "Internal Error",
})
}
}

View File

@ -0,0 +1,95 @@
package auth
import (
"context"
"errors"
"fmt"
"log/slog"
"net/http"
"time"
"git.optclblast.xyz/draincloud/draincloud-core/internal/common"
"git.optclblast.xyz/draincloud/draincloud-core/internal/errs"
"git.optclblast.xyz/draincloud/draincloud-core/internal/logger"
"git.optclblast.xyz/draincloud/draincloud-core/internal/storage"
"git.optclblast.xyz/draincloud/draincloud-core/internal/storage/models"
)
const (
csrfTokenCookie = "__Csrf_token"
sessionTokenCookie = "__Session_token"
)
type AuthResolver struct {
authStorage storage.AuthStorage
}
func (r *AuthResolver) Resolve(ctx context.Context, req *common.Request) error {
return r.authorize(ctx, req)
}
func (p *AuthResolver) authorize(ctx context.Context, req *common.Request) error {
session, err := p.getSession(ctx, req)
if err != nil && !errors.Is(err, http.ErrNoCookie) {
return errs.ErrorUnauthorized
}
if session == nil {
return errs.ErrorUnauthorized
}
if err := validateSession(ctx, req, session); err != nil {
// TODO add audit log entry
return errs.ErrorUnauthorized
}
user, err := p.authStorage.GetUserByID(ctx, session.UserID)
if err != nil {
return fmt.Errorf("failed to fetch user by id: %w", err)
}
logger.Debug(ctx, "[authorize] user authorized", slog.String("session_id", session.ID.String()))
req.User = user
req.Session = session
return nil
}
func (d *AuthResolver) getSession(ctx context.Context, req *common.Request) (*models.Session, error) {
token, err := common.GetValue[string](req.Metadata, sessionTokenCookie)
if err != nil {
return nil, fmt.Errorf("failed to fetch session cookie from request: %w", err)
}
if len(token) == 0 {
return nil, fmt.Errorf("session token or csrf token is empty")
}
session, err := d.authStorage.GetSession(ctx, token)
if err != nil {
return nil, fmt.Errorf("failed to fetch session from repo: %w", err)
}
return session, nil
}
func validateSession(ctx context.Context, req *common.Request, session *models.Session) error {
if session == nil {
return errs.ErrorAccessDenied
}
csrfToken, err := common.GetValue[string](req.Metadata, csrfTokenCookie)
if err != nil {
return fmt.Errorf("failed to fetch csrf cookie from request: %w", err)
}
if session.CsrfToken != csrfToken {
return errs.ErrorAccessDenied
}
if session.ExpiredAt.Before(time.Now()) {
return errs.ErrorSessionExpired
}
return nil
}

View File

@ -0,0 +1,11 @@
package resolvers
import (
"context"
"git.optclblast.xyz/draincloud/draincloud-core/internal/common"
)
type Resolver interface {
Resolve(ctx context.Context, req *common.Request) error
}