Просмотр исходного кода

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

jamesread 2 лет назад
Родитель
Сommit
c24adaafcb

+ 12 - 9
internal/httpservers/restapi.go

@@ -78,31 +78,34 @@ func startRestAPIServer(globalConfig *config.Config) error {
 		"address": cfg.ListenAddressGrpcActions,
 	}).Info("Starting REST API")
 
-	ctx := context.Background()
-	ctx, cancel := context.WithCancel(ctx)
-	defer cancel()
+	mux := newMux()
 
-	// 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(
 		runtime.WithMetadata(parseRequestMetadata),
 		runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.HTTPBodyMarshaler{
 			Marshaler: &runtime.JSONPb{
 				MarshalOptions: protojson.MarshalOptions{
 					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()}
 
 	err := gw.RegisterOliveTinApiServiceHandlerFromEndpoint(ctx, mux, cfg.ListenAddressGrpcActions, opts)
 
 	if err != nil {
-		log.Errorf("Could not register REST API Handler %v", err)
-
-		return err
+		log.Panicf("Could not register REST API Handler %v", err)
 	}
 
-	return http.ListenAndServe(cfg.ListenAddressRestActions, cors.AllowCors(mux))
+	return mux
 }

+ 105 - 0
internal/httpservers/restapi_auth_jwt_test.go

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

+ 15 - 108
internal/httpservers/restapi_test.go

@@ -1,125 +1,33 @@
 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 (
-	"crypto/rand"
-	"crypto/rsa"
-	"crypto/x509"
-	"encoding/pem"
 	"fmt"
-	config "github.com/OliveTin/OliveTin/internal/config"
 	"github.com/OliveTin/OliveTin/internal/cors"
-	"github.com/golang-jwt/jwt/v4"
 	"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
-	"github.com/stretchr/testify/assert"
-	"google.golang.org/protobuf/encoding/protojson"
-	"io"
 	"net"
 	"net/http"
-	"os"
 	"testing"
-	"time"
 )
 
-func createKeys() (*rsa.PrivateKey, string) {
-	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) {
+func setupTestingServer(mux *runtime.ServeMux, t *testing.T) *http.Server {
 	lis, err := net.Listen("tcp", ":1337")
 
 	if err != nil || lis == nil {
 		t.Errorf("Could not listen %v %v", err, lis)
-		return
+		return nil
 	}
 
 	srv := &http.Server{Handler: cors.AllowCors(mux)}
 
 	go startTestingServer(lis, srv, t)
+
+	return srv
 }
 
 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() {
 		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) {
-	// testBase(t, 1000, 200)
-}
+func newReq(path string) (*http.Request, *http.Client) {
+	client := &http.Client{}
+	req, _ := http.NewRequest("GET", fmt.Sprintf("http://localhost:1337/%v", path), nil)
 
-func TestJWTSignatureVerificationFails(t *testing.T) {
-	testBase(t, -500, 403)
+	return req, client
 }