Compare commits

...

5 Commits

Author SHA1 Message Date
28ff9006df added loginResult type 2021-09-14 16:22:32 +02:00
426531e634 implemented simple login with JWT 2021-09-14 15:16:37 +02:00
2b25003344 fixed typo 2021-09-14 15:12:09 +02:00
4c4ef95ee7 changed http handler to mux 2021-09-11 22:17:22 +02:00
0daa71dc72 added env file for dev 2021-09-11 15:44:14 +02:00
11 changed files with 229 additions and 23 deletions

8
.env Normal file
View File

@@ -0,0 +1,8 @@
# Env file used for developing
S3_ENDPOINT=localhost:9000
S3_ACCESS_KEY=testo
S3_SECRET_KEY=testotesto
S3_BUCKET=dev
S3_DISABLE_SSL=true
ADDRESS=:8080
VERBOSE=true

View File

@@ -11,7 +11,7 @@ type args struct {
S3Endpoint string `arg:"--s3-endpoint,required,env:S3_ENDPOINT" help:"host[:port]" placeholder:"ENDPOINT"` S3Endpoint string `arg:"--s3-endpoint,required,env:S3_ENDPOINT" help:"host[:port]" placeholder:"ENDPOINT"`
S3AccessKey string `arg:"--s3-access-key,required,env:S3_ACCESS_KEY" placeholder:"ACCESS_KEY"` S3AccessKey string `arg:"--s3-access-key,required,env:S3_ACCESS_KEY" placeholder:"ACCESS_KEY"`
S3SecretKey string `arg:"--s3-secret-key,required,env:S3_SECRET_KEY" placeholder:"SECRET_KEY"` S3SecretKey string `arg:"--s3-secret-key,required,env:S3_SECRET_KEY" placeholder:"SECRET_KEY"`
S3Buket string `arg:"--s3-buket,required,env:S3_BUKET" placeholder:"BUKET"` S3Bucket string `arg:"--s3-bucket,required,env:S3_BUCKET" placeholder:"BUCKET"`
S3DisableSSL bool `arg:"--s3-disable-ssl,env:S3_DISABLE_SSL" default:"false"` S3DisableSSL bool `arg:"--s3-disable-ssl,env:S3_DISABLE_SSL" default:"false"`
Address string `arg:"--address,env:ADDRESS" default:":3000" help:"what address to listen on" placeholder:"ADDRESS"` Address string `arg:"--address,env:ADDRESS" default:":3000" help:"what address to listen on" placeholder:"ADDRESS"`
CacheTTL int64 `arg:"--cache-ttl,env:CACHE_TTL" help:"Time in seconds" default:"30" placeholder:"TTL"` CacheTTL int64 `arg:"--cache-ttl,env:CACHE_TTL" help:"Time in seconds" default:"30" placeholder:"TTL"`
@@ -33,7 +33,7 @@ func main() {
S3SSL: !args.S3DisableSSL, S3SSL: !args.S3DisableSSL,
S3AccessKey: args.S3AccessKey, S3AccessKey: args.S3AccessKey,
S3SecretKey: args.S3SecretKey, S3SecretKey: args.S3SecretKey,
S3Buket: args.S3Buket, S3Bucket: args.S3Bucket,
CacheTTL: time.Duration(args.CacheTTL) * time.Second, CacheTTL: time.Duration(args.CacheTTL) * time.Second,
CacheCleanup: time.Duration(args.CacheCleanup) * time.Second, CacheCleanup: time.Duration(args.CacheCleanup) * time.Second,
Address: args.Address, Address: args.Address,

2
go.mod
View File

@@ -4,6 +4,8 @@ go 1.16
require ( require (
github.com/alexflint/go-arg v1.4.2 github.com/alexflint/go-arg v1.4.2
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/gorilla/mux v1.8.0
github.com/graph-gophers/dataloader v5.0.0+incompatible github.com/graph-gophers/dataloader v5.0.0+incompatible
github.com/graphql-go/graphql v0.7.9 github.com/graphql-go/graphql v0.7.9
github.com/graphql-go/handler v0.2.3 github.com/graphql-go/handler v0.2.3

4
go.sum
View File

@@ -7,11 +7,15 @@ 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/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/graph-gophers/dataloader v5.0.0+incompatible h1:R+yjsbrNq1Mo3aPG+Z/EKYrXrXXUNJHOgbRt+U6jOug= github.com/graph-gophers/dataloader v5.0.0+incompatible h1:R+yjsbrNq1Mo3aPG+Z/EKYrXrXXUNJHOgbRt+U6jOug=
github.com/graph-gophers/dataloader v5.0.0+incompatible/go.mod h1:jk4jk0c5ZISbKaMe8WsVopGB5/15GvGHMdMdPtwlRp4= github.com/graph-gophers/dataloader v5.0.0+incompatible/go.mod h1:jk4jk0c5ZISbKaMe8WsVopGB5/15GvGHMdMdPtwlRp4=
github.com/graphql-go/graphql v0.7.9 h1:5Va/Rt4l5g3YjwDnid3vFfn43faaQBq7rMcIZ0VnV34= github.com/graphql-go/graphql v0.7.9 h1:5Va/Rt4l5g3YjwDnid3vFfn43faaQBq7rMcIZ0VnV34=

View File

@@ -3,7 +3,9 @@
package s3browser package s3browser
import "github.com/gorilla/mux"
// Since we dont have the static directory when developing we replace the function with an empty one // Since we dont have the static directory when developing we replace the function with an empty one
func initStatic() { func initStatic(r *mux.Router) {
// NOOP // NOOP
} }

View File

@@ -10,8 +10,14 @@ import (
"github.com/graphql-go/graphql/language/ast" "github.com/graphql-go/graphql/language/ast"
) )
type LoginResult struct {
Token string `json:"token"`
Successful bool `json:"successful"`
}
var graphqlDirType *graphql.Object var graphqlDirType *graphql.Object
var graphqlFileType *graphql.Object var graphqlFileType *graphql.Object
var graphqlLoginResultType *graphql.Object
// graphqlTypes create all graphql types and stores the in the global variables // graphqlTypes create all graphql types and stores the in the global variables
func graphqlTypes() { func graphqlTypes() {
@@ -192,6 +198,22 @@ func graphqlTypes() {
}, nil }, nil
}, },
}) })
graphqlLoginResultType = graphql.NewObject(graphql.ObjectConfig{
Name: "LoginResut",
Description: "Result of a login",
Fields: graphql.Fields{
"token": &graphql.Field{
Type: graphql.String,
Description: "JWT token if login was successful",
},
"successful": &graphql.Field{
Type: graphql.NewNonNull(graphql.Boolean),
Description: "If the login was successful",
},
},
})
} }
// graphqlTypes helper func for using the dataloader to get a file // graphqlTypes helper func for using the dataloader to get a file

View File

@@ -7,7 +7,11 @@ import (
"mime" "mime"
"net/http" "net/http"
"path/filepath" "path/filepath"
"strings"
"time"
"github.com/golang-jwt/jwt"
"github.com/gorilla/mux"
"github.com/graphql-go/graphql" "github.com/graphql-go/graphql"
"github.com/graphql-go/graphql/gqlerrors" "github.com/graphql-go/graphql/gqlerrors"
"github.com/graphql-go/handler" "github.com/graphql-go/handler"
@@ -16,9 +20,15 @@ import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
type JWTClaims struct {
jwt.StandardClaims
}
// initHttp setup and start the http server. Blocking // initHttp setup and start the http server. Blocking
func initHttp(resolveContext context.Context, schema graphql.Schema, address string) error { func initHttp(resolveContext context.Context, schema graphql.Schema, address string) error {
h := handler.New(&handler.Config{ r := mux.NewRouter()
gqlHandler := handler.New(&handler.Config{
Schema: &schema, Schema: &schema,
Pretty: true, Pretty: true,
GraphiQL: false, GraphiQL: false,
@@ -34,26 +44,41 @@ func initHttp(resolveContext context.Context, schema graphql.Schema, address str
}, },
}) })
http.HandleFunc("/api/graphql", func(rw http.ResponseWriter, r *http.Request) { r.Use(func(h http.Handler) http.Handler {
h.ContextHandler(resolveContext, rw, r) return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
token := getTokenFromRequest(r)
if token != "" {
parsedToken, err := parseJWT(token)
if err == nil && parsedToken.Valid {
r.WithContext(context.WithValue(r.Context(), "jwt", parsedToken))
}
}
h.ServeHTTP(rw, r)
})
}) })
http.HandleFunc("/api/file", func(rw http.ResponseWriter, r *http.Request) { r.HandleFunc("/api/graphql", func(rw http.ResponseWriter, r *http.Request) {
if r.Method == "GET" { gqlHandler.ContextHandler(resolveContext, rw, r)
httpGetFile(resolveContext, rw, r)
return
}
if r.Method == "POST" {
httpPostFile(resolveContext, rw, r)
return
}
}) })
r.HandleFunc("/api/file", func(rw http.ResponseWriter, r *http.Request) {
httpGetFile(resolveContext, rw, r)
}).Methods("GET")
r.HandleFunc("/api/file", func(rw http.ResponseWriter, r *http.Request) {
httpPostFile(resolveContext, rw, r)
}).Methods("POST")
r.HandleFunc("/api/cookie", setLoginCookie).Methods("POST")
// Init the embedded static files // Init the embedded static files
initStatic() initStatic(r)
return http.ListenAndServe(address, nil) return http.ListenAndServe(address, r)
} }
func httpGetFile(ctx context.Context, rw http.ResponseWriter, r *http.Request) { func httpGetFile(ctx context.Context, rw http.ResponseWriter, r *http.Request) {
@@ -120,3 +145,87 @@ func httpPostFile(ctx context.Context, rw http.ResponseWriter, r *http.Request)
rw.WriteHeader(http.StatusCreated) rw.WriteHeader(http.StatusCreated)
} }
//parseJWT parse a JWT. does not check if token is valid. Returns error if non was provided
func parseJWT(token string) (*jwt.Token, error) {
if token == "" {
return nil, fmt.Errorf("No token provided")
}
parsed, err := jwt.Parse(token, 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("TODO"), nil
})
if err != nil {
return nil, err
}
return parsed, nil
}
//getTokenFromRequest looks for a JWT in the "Authorization" header or in the cookies
func getTokenFromRequest(r *http.Request) string {
// get token from header
authHeader := strings.Split(r.Header.Get("Authorization"), "Bearer ")
if len(authHeader) == 2 {
return authHeader[1]
}
// Get cookie from cookie
cookie, err := r.Cookie("jwt") //TODO: change cookie name
if err == nil && len(cookie.Value) != 0 {
return cookie.Value
}
return ""
}
//setLoginCookie if provieded a valid JWT in the body then set a httpOnly cookie with the token
func setLoginCookie(rw http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
defer r.Body.Close()
if err != nil {
rw.WriteHeader(http.StatusInternalServerError)
return
}
tokenString := string(body)
token, err := parseJWT(tokenString)
if err != nil {
rw.WriteHeader(http.StatusInternalServerError)
return
}
if !token.Valid {
rw.WriteHeader(http.StatusUnauthorized)
return
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
rw.WriteHeader(http.StatusInternalServerError)
return
}
cookie := &http.Cookie{
Name: "jwt",
Value: tokenString,
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
Path: "/api",
Expires: time.Unix(int64(claims["exp"].(float64)), 0),
}
http.SetCookie(rw, cookie)
rw.WriteHeader(http.StatusNoContent)
}

View File

@@ -4,7 +4,9 @@ import (
"context" "context"
"fmt" "fmt"
"strings" "strings"
"time"
"github.com/golang-jwt/jwt"
"github.com/graph-gophers/dataloader" "github.com/graph-gophers/dataloader"
"github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7"
) )
@@ -189,3 +191,34 @@ func deleteDirectory(ctx context.Context, path string) error {
return nil return nil
} }
//login Checks for valid username password combination. Returns singed jwt string
func login(ctx context.Context, username, password string) (LoginResult, error) {
// TODO: replace with propper user management
if username != "admin" && password != "hunter2" {
return LoginResult{
Successful: false,
}, nil
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, JWTClaims{
StandardClaims: jwt.StandardClaims{
Subject: username,
ExpiresAt: time.Now().Add(time.Hour * 24).Unix(),
},
})
tokenString, err := token.SignedString([]byte("TODO"))
if err != nil {
return LoginResult{
Successful: false,
}, err
}
return LoginResult{
Token: tokenString,
Successful: true,
}, nil
}

View File

@@ -16,7 +16,7 @@ type AppConfig struct {
S3AccessKey string S3AccessKey string
S3SecretKey string S3SecretKey string
S3SSL bool S3SSL bool
S3Buket string S3Bucket string
CacheTTL time.Duration CacheTTL time.Duration
CacheCleanup time.Duration CacheCleanup time.Duration
Address string Address string
@@ -54,14 +54,14 @@ func setupS3Client(config AppConfig) (*minio.Client, error) {
return nil, err return nil, err
} }
exists, err := minioClient.BucketExists(context.Background(), config.S3Buket) exists, err := minioClient.BucketExists(context.Background(), config.S3Bucket)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if !exists { if !exists {
return nil, fmt.Errorf("Bucket '%s' does not exist", config.S3Buket) return nil, fmt.Errorf("Bucket '%s' does not exist", config.S3Bucket)
} }
return minioClient, nil return minioClient, nil

View File

@@ -183,6 +183,32 @@ func graphqlSchema() (graphql.Schema, error) {
return path, deleteDirectory(p.Context, path) return path, deleteDirectory(p.Context, path)
}, },
}, },
"login": &graphql.Field{
Type: graphql.NewNonNull(graphqlLoginResultType),
Args: graphql.FieldConfigArgument{
"username": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
},
"password": &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
},
},
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
username, ok := p.Args["username"].(string)
if !ok {
return nil, fmt.Errorf("Failed to parse args")
}
password, ok := p.Args["password"].(string)
if !ok {
return nil, fmt.Errorf("Failed to parse args")
}
return login(p.Context, username, password)
},
},
} }
rootQuery := graphql.ObjectConfig{ rootQuery := graphql.ObjectConfig{

View File

@@ -26,7 +26,7 @@ func (spa *spaFileSystem) Open(name string) (http.File, error) {
return f, err return f, err
} }
func initStatic() { func initStatic(e *mux.Router) {
staticFS, _ := fs.Sub(staticFiles, "static") staticFS, _ := fs.Sub(staticFiles, "static")
http.Handle("/", http.FileServer(&spaFileSystem{http.FS(staticFS)})) r.Handle("/", http.FileServer(&spaFileSystem{http.FS(staticFS)}))
} }