Merge branch 'main' of ssh://github.com/OliveTin/OliveTin
This commit is contained in:
commit
c24adaafcb
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue