Merge branch 'main' of ssh://github.com/OliveTin/OliveTin

This commit is contained in:
jamesread 2024-05-06 23:01:09 +01:00
commit c24adaafcb
3 changed files with 132 additions and 117 deletions

View File

@ -78,31 +78,34 @@ func startRestAPIServer(globalConfig *config.Config) error {
"address": cfg.ListenAddressGrpcActions, "address": cfg.ListenAddressGrpcActions,
}).Info("Starting REST API") }).Info("Starting REST API")
ctx := context.Background() mux := newMux()
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// The JSONPb.EmitDefaults is necssary, so "empty" fields are returned in JSON. return http.ListenAndServe(cfg.ListenAddressRestActions, cors.AllowCors(mux))
}
func newMux() *runtime.ServeMux {
// The MarshalOptions set some important compatibility settings for the webui. See below.
mux := runtime.NewServeMux( mux := runtime.NewServeMux(
runtime.WithMetadata(parseRequestMetadata), runtime.WithMetadata(parseRequestMetadata),
runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.HTTPBodyMarshaler{ runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.HTTPBodyMarshaler{
Marshaler: &runtime.JSONPb{ Marshaler: &runtime.JSONPb{
MarshalOptions: protojson.MarshalOptions{ MarshalOptions: protojson.MarshalOptions{
UseProtoNames: false, // eg: canExec for js instead of can_exec from protobuf UseProtoNames: false, // eg: canExec for js instead of can_exec from protobuf
EmitUnpopulated: true, EmitUnpopulated: true, // Emit empty fields so that javascript does not get "undefined" when accessing fields with empty values.
}, },
}, },
}), }),
) )
ctx := context.Background()
opts := []grpc.DialOption{grpc.WithInsecure()} opts := []grpc.DialOption{grpc.WithInsecure()}
err := gw.RegisterOliveTinApiServiceHandlerFromEndpoint(ctx, mux, cfg.ListenAddressGrpcActions, opts) err := gw.RegisterOliveTinApiServiceHandlerFromEndpoint(ctx, mux, cfg.ListenAddressGrpcActions, opts)
if err != nil { if err != nil {
log.Errorf("Could not register REST API Handler %v", err) log.Panicf("Could not register REST API Handler %v", err)
return err
} }
return http.ListenAndServe(cfg.ListenAddressRestActions, cors.AllowCors(mux)) return mux
} }

View File

@ -0,0 +1,105 @@
package httpservers
import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
config "github.com/OliveTin/OliveTin/internal/config"
"github.com/golang-jwt/jwt/v4"
"github.com/stretchr/testify/assert"
"io"
"net/http"
"os"
"testing"
"time"
)
func createKeys(t *testing.T) (*rsa.PrivateKey, string) {
tmpFile, _ := os.CreateTemp(os.TempDir(), "olivetin-jwt-")
fmt.Println("Created File: " + tmpFile.Name())
privateKey, _ := rsa.GenerateKey(rand.Reader, 2048)
pubKey := &privateKey.PublicKey
// https://stackoverflow.com/questions/13555085/save-and-load-crypto-rsa-privatekey-to-and-from-the-disk
pkixPubKey, _ := x509.MarshalPKIXPublicKey(pubKey)
pubPem := pem.EncodeToMemory(
&pem.Block{
Type: "RSA PUBLIC KEY",
Bytes: pkixPubKey,
},
)
if err := os.WriteFile(tmpFile.Name(), pubPem, 0755); err != nil {
t.Fatalf("error when dumping pubKey: %s \n", err)
}
return privateKey, tmpFile.Name()
}
func testJwkValidation(t *testing.T, expire int64, expectCode int) {
privateKey, publicKeyPath := createKeys(t)
defer os.Remove(publicKeyPath)
cfg := config.DefaultConfig()
cfg.AuthJwtPubKeyPath = publicKeyPath
cfg.AuthJwtClaimUsername = "sub"
cfg.AuthJwtClaimUserGroup = "olivetinGroup"
cfg.AuthJwtCookieName = "authorization_token"
SetGlobalRestConfig(cfg) // ugly, setting global var, we should pass configs as params to modules... :/
token := jwt.New(jwt.SigningMethodRS256)
claims := token.Claims.(jwt.MapClaims)
claims["nbf"] = time.Now().Unix() - 1000
claims["exp"] = time.Now().Unix() + expire
claims["sub"] = "test"
claims["olivetinGroup"] = "test"
tokenStr, _ := token.SignedString(privateKey)
mux := newMux()
mux.HandlePath("GET", "/", func(w http.ResponseWriter, r *http.Request, pathParams map[string]string) {
username, usergroup := parseJwtCookie(r)
if username == "" {
w.WriteHeader(403)
}
w.Write([]byte(fmt.Sprintf("username=%v, usergroup=%v", username, usergroup)))
})
srv := setupTestingServer(mux, t)
req, client := newReq("")
req.AddCookie(&http.Cookie{
Name: "authorization_token",
Value: tokenStr,
MaxAge: 300,
})
res, err := client.Do(req)
if err != nil {
t.Fatalf("Client err: %+v", err)
} else {
defer res.Body.Close()
assert.Equal(t, expectCode, res.StatusCode)
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}
srv.Shutdown(context.TODO())
}
func TestJWTSignatureVerificationSucceeds(t *testing.T) {
testJwkValidation(t, 1000, 200)
}
func TestJWTSignatureVerificationFails(t *testing.T) {
testJwkValidation(t, -500, 403)
}

View File

@ -1,125 +1,33 @@
package httpservers package httpservers
/*
The REST API actually has very few tests, as the "real" API behind OliveTin
is is implemented as a gRPC in /internal/grpc. The REST API therefore only
handles HTTP specific stuff like authentication cookies and JWT parsing.
*/
import ( import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt" "fmt"
config "github.com/OliveTin/OliveTin/internal/config"
"github.com/OliveTin/OliveTin/internal/cors" "github.com/OliveTin/OliveTin/internal/cors"
"github.com/golang-jwt/jwt/v4"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"github.com/stretchr/testify/assert"
"google.golang.org/protobuf/encoding/protojson"
"io"
"net" "net"
"net/http" "net/http"
"os"
"testing" "testing"
"time"
) )
func createKeys() (*rsa.PrivateKey, string) { func setupTestingServer(mux *runtime.ServeMux, t *testing.T) *http.Server {
tmpFile, _ := os.CreateTemp(os.TempDir(), "olivetin-jwt-")
defer os.Remove(tmpFile.Name())
fmt.Println("Created File: " + tmpFile.Name())
privateKey, _ := rsa.GenerateKey(rand.Reader, 2048)
pubKey := &privateKey.PublicKey
// https://stackoverflow.com/questions/13555085/save-and-load-crypto-rsa-privatekey-to-and-from-the-disk
pkixPubKey, _ := x509.MarshalPKIXPublicKey(pubKey)
pubPem := pem.EncodeToMemory(
&pem.Block{
Type: "RSA PUBLIC KEY",
Bytes: pkixPubKey,
},
)
if err := os.WriteFile(tmpFile.Name(), pubPem, 0755); err != nil {
fmt.Printf("error when dumping pubKey: %s \n", err)
}
return privateKey, tmpFile.Name()
}
func testBase(t *testing.T, expire int64, expectCode int) {
privateKey, publicKeyPath := createKeys()
// default config + overrides
cfg := config.DefaultConfig()
cfg.AuthJwtPubKeyPath = publicKeyPath
cfg.AuthJwtClaimUsername = "sub"
cfg.AuthJwtClaimUserGroup = "olivetinGroup"
cfg.AuthJwtCookieName = "authorization_token"
SetGlobalRestConfig(cfg) // ugly, setting global var, we should pass configs as params to modules... :/
token := jwt.New(jwt.SigningMethodRS256)
claims := token.Claims.(jwt.MapClaims)
claims["nbf"] = time.Now().Unix() - 1000
claims["exp"] = time.Now().Unix() + expire
claims["sub"] = "test"
claims["olivetinGroup"] = "test"
tokenStr, _ := token.SignedString(privateKey)
// init mux endpoint like in restapi.go (but using dummy response handler)
mux := runtime.NewServeMux(
runtime.WithMetadata(parseRequestMetadata), // i am guessing this is critical middleware for authorizing request cookie
runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.HTTPBodyMarshaler{
Marshaler: &runtime.JSONPb{
MarshalOptions: protojson.MarshalOptions{
UseProtoNames: true,
EmitUnpopulated: true,
},
},
}),
)
mux.HandlePath("GET", "/", func(w http.ResponseWriter, r *http.Request, pathParams map[string]string) {
username, usergroup := parseJwtCookie(r)
if username == "" {
w.WriteHeader(403)
}
w.Write([]byte(fmt.Sprintf("username=%v, usergroup=%v", username, usergroup)))
})
// make server and attach handler
setupTestingServer(mux, t)
// make http client and send request to myself
client := &http.Client{}
req, _ := http.NewRequest("GET", "http://localhost:1337/", nil)
cookie := &http.Cookie{
Name: "authorization_token",
Value: tokenStr,
MaxAge: 300,
}
req.AddCookie(cookie)
res, err := client.Do(req)
if err != nil {
assert.Equal(t, expectCode, -1)
} else {
defer res.Body.Close()
assert.Equal(t, expectCode, res.StatusCode)
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}
}
func setupTestingServer(mux *runtime.ServeMux, t *testing.T) {
lis, err := net.Listen("tcp", ":1337") lis, err := net.Listen("tcp", ":1337")
if err != nil || lis == nil { if err != nil || lis == nil {
t.Errorf("Could not listen %v %v", err, lis) t.Errorf("Could not listen %v %v", err, lis)
return return nil
} }
srv := &http.Server{Handler: cors.AllowCors(mux)} srv := &http.Server{Handler: cors.AllowCors(mux)}
go startTestingServer(lis, srv, t) go startTestingServer(lis, srv, t)
return srv
} }
func startTestingServer(lis net.Listener, srv *http.Server, t *testing.T) { func startTestingServer(lis net.Listener, srv *http.Server, t *testing.T) {
@ -130,15 +38,14 @@ func startTestingServer(lis net.Listener, srv *http.Server, t *testing.T) {
go func() { go func() {
if err := srv.Serve(lis); err != nil { if err := srv.Serve(lis); err != nil {
t.Errorf("couldn't start server: %v", err) fmt.Printf("couldn't start server: %+v", err)
} }
}() }()
} }
func TestJWTSignatureVerificationSucceeds(t *testing.T) { func newReq(path string) (*http.Request, *http.Client) {
// testBase(t, 1000, 200) client := &http.Client{}
} req, _ := http.NewRequest("GET", fmt.Sprintf("http://localhost:1337/%v", path), nil)
func TestJWTSignatureVerificationFails(t *testing.T) { return req, client
testBase(t, -500, 403)
} }