Compare commits

..

7 Commits

Author SHA1 Message Date
f42d12ffd9 JWT refresh 2021-09-26 16:56:32 +02:00
87dc249371 even more stupid 2021-09-24 17:57:01 +02:00
a060ed0fe4 me stupid 2021-09-24 17:03:06 +02:00
a35ce7c24b fixed error package name 2021-09-24 15:54:51 +02:00
9cfc4da5f3 moved LoginResult in types 2021-09-24 15:54:03 +02:00
f59f3183f2 improved authentication check 2021-09-24 15:49:51 +02:00
ed932e3c92 the big refactor 2021-09-24 15:39:23 +02:00
13 changed files with 232 additions and 206 deletions

View File

@@ -4,6 +4,7 @@ import (
"time" "time"
s3browser "git.kapelle.org/niklas/s3browser/internal" s3browser "git.kapelle.org/niklas/s3browser/internal"
types "git.kapelle.org/niklas/s3browser/internal/types"
"github.com/alexflint/go-arg" "github.com/alexflint/go-arg"
) )
@@ -28,7 +29,7 @@ func main() {
var args args var args args
arg.MustParse(&args) arg.MustParse(&args)
s3browser.Start(s3browser.AppConfig{ s3browser.Start(types.AppConfig{
S3Endoint: args.S3Endpoint, S3Endoint: args.S3Endpoint,
S3SSL: !args.S3DisableSSL, S3SSL: !args.S3DisableSSL,
S3AccessKey: args.S3AccessKey, S3AccessKey: args.S3AccessKey,

View File

@@ -6,6 +6,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
types "git.kapelle.org/niklas/s3browser/internal/types"
"github.com/graph-gophers/dataloader" "github.com/graph-gophers/dataloader"
"github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@@ -81,7 +82,7 @@ func getFilesBatch(c context.Context, k dataloader.Keys) []*dataloader.Result {
for _, v := range k { for _, v := range k {
path := v.String() path := v.String()
files := make([]File, 0) files := make([]types.File, 0)
if !strings.HasSuffix(path, "/") { if !strings.HasSuffix(path, "/") {
path += "/" path += "/"
@@ -97,7 +98,7 @@ func getFilesBatch(c context.Context, k dataloader.Keys) []*dataloader.Result {
if obj.Err != nil { if obj.Err != nil {
// TODO: how to handle? // TODO: how to handle?
} else if !strings.HasSuffix(obj.Key, "/") { } else if !strings.HasSuffix(obj.Key, "/") {
files = append(files, File{ files = append(files, types.File{
ID: obj.Key, ID: obj.Key,
Name: filepath.Base(obj.Key), Name: filepath.Base(obj.Key),
Size: obj.Size, Size: obj.Size,
@@ -139,7 +140,7 @@ func getFileBatch(c context.Context, k dataloader.Keys) []*dataloader.Result {
}) })
} else { } else {
results = append(results, &dataloader.Result{ results = append(results, &dataloader.Result{
Data: &File{ Data: &types.File{
ID: obj.Key, ID: obj.Key,
Size: obj.Size, Size: obj.Size,
ContentType: obj.ContentType, ContentType: obj.ContentType,
@@ -166,7 +167,7 @@ func getDirsBatch(c context.Context, k dataloader.Keys) []*dataloader.Result {
for _, v := range k { for _, v := range k {
path := v.String() path := v.String()
dirs := make([]Directory, 0) dirs := make([]types.Directory, 0)
if !strings.HasSuffix(path, "/") { if !strings.HasSuffix(path, "/") {
path += "/" path += "/"
@@ -182,7 +183,7 @@ func getDirsBatch(c context.Context, k dataloader.Keys) []*dataloader.Result {
if obj.Err != nil { if obj.Err != nil {
// TODO: how to handle? // TODO: how to handle?
} else if strings.HasSuffix(obj.Key, "/") { } else if strings.HasSuffix(obj.Key, "/") {
dirs = append(dirs, Directory{ dirs = append(dirs, types.Directory{
ID: obj.Key, ID: obj.Key,
Name: filepath.Base(obj.Key), Name: filepath.Base(obj.Key),
}) })
@@ -213,7 +214,7 @@ func handleLoaderError(k dataloader.Keys, err error) []*dataloader.Result {
} }
// createDataloader create all dataloaders and return a map of them plus a cache for objects // createDataloader create all dataloaders and return a map of them plus a cache for objects
func createDataloader(config AppConfig) map[string]*dataloader.Loader { func createDataloader(config types.AppConfig) map[string]*dataloader.Loader {
loaderMap := make(map[string]*dataloader.Loader, 0) loaderMap := make(map[string]*dataloader.Loader, 0)
loaderMap["getFiles"] = dataloader.NewBatchedLoader( loaderMap["getFiles"] = dataloader.NewBatchedLoader(

View File

@@ -1,25 +0,0 @@
package s3browser
import "fmt"
type extendedError struct {
Message string
Code string
}
func (err *extendedError) Error() string {
return err.Message
}
func (err *extendedError) Extensions() map[string]interface{} {
return map[string]interface{}{
"code": err.Code,
}
}
func extendError(code, format string, a ...interface{}) *extendedError {
return &extendedError{
Message: fmt.Sprintf(format, a...),
Code: code,
}
}

29
internal/errors/errors.go Normal file
View File

@@ -0,0 +1,29 @@
package errors
import "fmt"
var (
ErrNotAuthenticated = ExtendError("UNAUTHENTICATED", "No valid authentication provided")
)
type ExtendedError struct {
Message string
Code string
}
func (err *ExtendedError) Error() string {
return err.Message
}
func (err *ExtendedError) Extensions() map[string]interface{} {
return map[string]interface{}{
"code": err.Code,
}
}
func ExtendError(code, format string, a ...interface{}) *ExtendedError {
return &ExtendedError{
Message: fmt.Sprintf(format, a...),
Code: code,
}
}

View File

@@ -1,4 +1,4 @@
package s3browser package gql
import ( import (
"fmt" "fmt"
@@ -8,19 +8,17 @@ import (
"github.com/graph-gophers/dataloader" "github.com/graph-gophers/dataloader"
"github.com/graphql-go/graphql" "github.com/graphql-go/graphql"
"github.com/graphql-go/graphql/language/ast" "github.com/graphql-go/graphql/language/ast"
)
type LoginResult struct { helper "git.kapelle.org/niklas/s3browser/internal/helper"
Token string `json:"token"` types "git.kapelle.org/niklas/s3browser/internal/types"
Successful bool `json:"successful"` )
}
var graphqlDirType *graphql.Object var graphqlDirType *graphql.Object
var graphqlFileType *graphql.Object var graphqlFileType *graphql.Object
var graphqlLoginResultType *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() {
var dateTimeType = graphql.NewScalar(graphql.ScalarConfig{ var dateTimeType = graphql.NewScalar(graphql.ScalarConfig{
Name: "DateTime", Name: "DateTime",
@@ -63,7 +61,7 @@ func graphqlTypes() {
"name": &graphql.Field{ "name": &graphql.Field{
Type: graphql.String, Type: graphql.String,
Resolve: func(p graphql.ResolveParams) (interface{}, error) { Resolve: func(p graphql.ResolveParams) (interface{}, error) {
source, ok := p.Source.(Directory) source, ok := p.Source.(types.Directory)
if !ok { if !ok {
return nil, fmt.Errorf("Failed to parse source for resolve") return nil, fmt.Errorf("Failed to parse source for resolve")
} }
@@ -85,7 +83,7 @@ func graphqlTypes() {
"name": &graphql.Field{ "name": &graphql.Field{
Type: graphql.String, Type: graphql.String,
Resolve: func(p graphql.ResolveParams) (interface{}, error) { Resolve: func(p graphql.ResolveParams) (interface{}, error) {
source, ok := p.Source.(File) source, ok := p.Source.(types.File)
if !ok { if !ok {
return nil, fmt.Errorf("Failed to parse source for resolve") return nil, fmt.Errorf("Failed to parse source for resolve")
} }
@@ -140,14 +138,14 @@ func graphqlTypes() {
"parent": &graphql.Field{ "parent": &graphql.Field{
Type: graphqlDirType, Type: graphqlDirType,
Resolve: func(p graphql.ResolveParams) (interface{}, error) { Resolve: func(p graphql.ResolveParams) (interface{}, error) {
source, ok := p.Source.(File) source, ok := p.Source.(types.File)
if !ok { if !ok {
return nil, fmt.Errorf("Failed to parse Source for parent resolve") return nil, fmt.Errorf("Failed to parse Source for parent resolve")
} }
basename := getPathFromId(source.ID) basename := helper.GetPathFromId(source.ID)
return Directory{ return types.Directory{
ID: basename, ID: basename,
}, nil }, nil
}, },
@@ -158,14 +156,14 @@ func graphqlTypes() {
graphqlDirType.AddFieldConfig("files", &graphql.Field{ graphqlDirType.AddFieldConfig("files", &graphql.Field{
Type: graphql.NewList(graphqlFileType), Type: graphql.NewList(graphqlFileType),
Resolve: func(p graphql.ResolveParams) (interface{}, error) { Resolve: func(p graphql.ResolveParams) (interface{}, error) {
source, ok := p.Source.(Directory) source, ok := p.Source.(types.Directory)
if !ok { if !ok {
return nil, fmt.Errorf("Failed to parse Source for files resolve") return nil, fmt.Errorf("Failed to parse Source for files resolve")
} }
loader := p.Context.Value("loader").(map[string]*dataloader.Loader) loader := p.Context.Value("loader").(map[string]*dataloader.Loader)
thunk := loader["getFiles"].Load(p.Context, dataloader.StringKey(nomalizeID(source.ID))) thunk := loader["getFiles"].Load(p.Context, dataloader.StringKey(helper.NomalizeID(source.ID)))
return thunk() return thunk()
}, },
}) })
@@ -173,13 +171,13 @@ func graphqlTypes() {
graphqlDirType.AddFieldConfig("directorys", &graphql.Field{ graphqlDirType.AddFieldConfig("directorys", &graphql.Field{
Type: graphql.NewList(graphqlDirType), Type: graphql.NewList(graphqlDirType),
Resolve: func(p graphql.ResolveParams) (interface{}, error) { Resolve: func(p graphql.ResolveParams) (interface{}, error) {
source, ok := p.Source.(Directory) source, ok := p.Source.(types.Directory)
if !ok { if !ok {
return nil, fmt.Errorf("Failed to parse Source for directories resolve") return nil, fmt.Errorf("Failed to parse Source for directories resolve")
} }
loader := p.Context.Value("loader").(map[string]*dataloader.Loader) loader := p.Context.Value("loader").(map[string]*dataloader.Loader)
thunk := loader["getDirs"].Load(p.Context, dataloader.StringKey(nomalizeID(source.ID))) thunk := loader["getDirs"].Load(p.Context, dataloader.StringKey(helper.NomalizeID(source.ID)))
return thunk() return thunk()
}, },
@@ -188,13 +186,13 @@ func graphqlTypes() {
graphqlDirType.AddFieldConfig("parent", &graphql.Field{ graphqlDirType.AddFieldConfig("parent", &graphql.Field{
Type: graphqlDirType, Type: graphqlDirType,
Resolve: func(p graphql.ResolveParams) (interface{}, error) { Resolve: func(p graphql.ResolveParams) (interface{}, error) {
source, ok := p.Source.(Directory) source, ok := p.Source.(types.Directory)
if !ok { if !ok {
return nil, fmt.Errorf("Failed to parse Source for directories resolve") return nil, fmt.Errorf("Failed to parse Source for directories resolve")
} }
return Directory{ return types.Directory{
ID: getParentDir(source.ID), ID: helper.GetParentDir(source.ID),
}, nil }, nil
}, },
}) })
@@ -216,23 +214,23 @@ func graphqlTypes() {
} }
// graphqlTypes helper func for using the dataloader to get a file //loadFile helper func for using the dataloader to get a file
func loadFile(p graphql.ResolveParams) (*File, error) { func loadFile(p graphql.ResolveParams) (*types.File, error) {
source, ok := p.Source.(File) source, ok := p.Source.(types.File)
if !ok { if !ok {
return nil, fmt.Errorf("Failed to parse source for resolve") return nil, fmt.Errorf("Failed to parse source for resolve")
} }
loader := p.Context.Value("loader").(map[string]*dataloader.Loader) loader := p.Context.Value("loader").(map[string]*dataloader.Loader)
thunk := loader["getFile"].Load(p.Context, dataloader.StringKey(nomalizeID(source.ID))) thunk := loader["getFile"].Load(p.Context, dataloader.StringKey(helper.NomalizeID(source.ID)))
result, err := thunk() result, err := thunk()
if err != nil { if err != nil {
return nil, err return nil, err
} }
file, ok := result.(*File) file, ok := result.(*types.File)
if !ok { if !ok {
return nil, fmt.Errorf("Failed to load file") return nil, fmt.Errorf("Failed to load file")

View File

@@ -1,4 +1,4 @@
package s3browser package gql
import ( import (
"context" "context"
@@ -7,6 +7,9 @@ import (
"github.com/graph-gophers/dataloader" "github.com/graph-gophers/dataloader"
"github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7"
helper "git.kapelle.org/niklas/s3browser/internal/helper"
types "git.kapelle.org/niklas/s3browser/internal/types"
) )
func deleteMutation(ctx context.Context, id string) error { func deleteMutation(ctx context.Context, id string) error {
@@ -18,17 +21,17 @@ func deleteMutation(ctx context.Context, id string) error {
// TODO: it is posible to remove multiple objects with a single call. // TODO: it is posible to remove multiple objects with a single call.
// Is it better to batch this? // Is it better to batch this?
err := s3Client.RemoveObject(ctx, bucketName, id, minio.RemoveObjectOptions{}) err := s3Client.RemoveObject(ctx, "dev", id, minio.RemoveObjectOptions{})
if err != nil { if err != nil {
return err return err
} }
// Invalidate cache // Invalidate cache
return invalidateCache(ctx, nomalizeID(id)) return helper.InvalidateCache(ctx, helper.NomalizeID(id))
} }
func copyMutation(ctx context.Context, src, dest string) (*File, error) { func copyMutation(ctx context.Context, src, dest string) (*types.File, error) {
s3Client, ok := ctx.Value("s3Client").(*minio.Client) s3Client, ok := ctx.Value("s3Client").(*minio.Client)
if !ok { if !ok {
@@ -39,14 +42,14 @@ func copyMutation(ctx context.Context, src, dest string) (*File, error) {
if strings.HasSuffix(dest, "/") { if strings.HasSuffix(dest, "/") {
// create new dest id // create new dest id
// TODO: What if a file with this id already exists? // TODO: What if a file with this id already exists?
dest += getFilenameFromID(src) dest += helper.GetFilenameFromID(src)
} }
info, err := s3Client.CopyObject(ctx, minio.CopyDestOptions{ info, err := s3Client.CopyObject(ctx, minio.CopyDestOptions{
Bucket: bucketName, Bucket: "dev",
Object: dest, Object: dest,
}, minio.CopySrcOptions{ }, minio.CopySrcOptions{
Bucket: bucketName, Bucket: "dev",
Object: src, Object: src,
}) })
@@ -56,15 +59,15 @@ func copyMutation(ctx context.Context, src, dest string) (*File, error) {
// Invalidate cache // Invalidate cache
// TODO: check error // TODO: check error
invalidateCache(ctx, nomalizeID(info.Key)) helper.InvalidateCache(ctx, helper.NomalizeID(info.Key))
return &File{ return &types.File{
ID: info.Key, ID: info.Key,
}, nil }, nil
} }
func moveMutation(ctx context.Context, src, dest string) (*File, error) { func moveMutation(ctx context.Context, src, dest string) (*types.File, error) {
s3Client, ok := ctx.Value("s3Client").(*minio.Client) s3Client, ok := ctx.Value("s3Client").(*minio.Client)
if !ok { if !ok {
@@ -75,15 +78,15 @@ func moveMutation(ctx context.Context, src, dest string) (*File, error) {
if strings.HasSuffix(dest, "/") { if strings.HasSuffix(dest, "/") {
// create new dest id // create new dest id
// TODO: What if a file with this id already exists? // TODO: What if a file with this id already exists?
dest += getFilenameFromID(src) dest += helper.GetFilenameFromID(src)
} }
// There is no (spoon) move. Only copy and delete // There is no (spoon) move. Only copy and delete
info, err := s3Client.CopyObject(ctx, minio.CopyDestOptions{ info, err := s3Client.CopyObject(ctx, minio.CopyDestOptions{
Bucket: bucketName, Bucket: "dev",
Object: dest, Object: dest,
}, minio.CopySrcOptions{ }, minio.CopySrcOptions{
Bucket: bucketName, Bucket: "dev",
Object: src, Object: src,
}) })
@@ -97,15 +100,15 @@ func moveMutation(ctx context.Context, src, dest string) (*File, error) {
return nil, err return nil, err
} }
invalidateCache(ctx, nomalizeID(info.Key)) helper.InvalidateCache(ctx, helper.NomalizeID(info.Key))
return &File{ return &types.File{
ID: info.Key, ID: info.Key,
}, nil }, nil
} }
func createDirectory(ctx context.Context, path string) (*Directory, error) { func createDirectory(ctx context.Context, path string) (*types.Directory, error) {
s3Client, ok := ctx.Value("s3Client").(*minio.Client) s3Client, ok := ctx.Value("s3Client").(*minio.Client)
if !ok { if !ok {
@@ -116,7 +119,7 @@ func createDirectory(ctx context.Context, path string) (*Directory, error) {
path += "/" path += "/"
} }
info, err := s3Client.PutObject(ctx, bucketName, path, strings.NewReader(""), 0, minio.PutObjectOptions{ info, err := s3Client.PutObject(ctx, "dev", path, strings.NewReader(""), 0, minio.PutObjectOptions{
ContentType: "application/x-directory", ContentType: "application/x-directory",
}) })
@@ -126,9 +129,9 @@ func createDirectory(ctx context.Context, path string) (*Directory, error) {
// Invalidate cache // Invalidate cache
// TODO: check error // TODO: check error
invalidateCacheForDir(ctx, nomalizeID(info.Key)) helper.InvalidateCacheForDir(ctx, helper.NomalizeID(info.Key))
return &Directory{ return &types.Directory{
ID: info.Key, ID: info.Key,
}, nil }, nil
@@ -152,7 +155,7 @@ func deleteDirectory(ctx context.Context, path string) error {
} }
// Get all files inside the directory // Get all files inside the directory
thunk := loader["listObjectsRecursive"].Load(ctx, dataloader.StringKey(nomalizeID(path))) thunk := loader["listObjectsRecursive"].Load(ctx, dataloader.StringKey(helper.NomalizeID(path)))
result, err := thunk() result, err := thunk()
@@ -166,7 +169,7 @@ func deleteDirectory(ctx context.Context, path string) error {
} }
// Delete all child files // Delete all child files
err = deleteMultiple(ctx, *s3Client, files) err = helper.DeleteMultiple(ctx, *s3Client, files)
if err != nil { if err != nil {
return err return err
@@ -178,39 +181,39 @@ func deleteDirectory(ctx context.Context, path string) error {
// This is at least the behavior when working with minio as s3 backend // This is at least the behavior when working with minio as s3 backend
// TODO: check if this is normal behavior when working with s3 // TODO: check if this is normal behavior when working with s3
if len(files) == 0 { if len(files) == 0 {
err := s3Client.RemoveObject(ctx, bucketName, path, minio.RemoveObjectOptions{}) err := s3Client.RemoveObject(ctx, "dev", path, minio.RemoveObjectOptions{})
if err != nil { if err != nil {
return err return err
} }
} }
//Invalidate cache //Invalidate cache
invalidateCacheForDir(ctx, nomalizeID(path)) helper.InvalidateCacheForDir(ctx, helper.NomalizeID(path))
return nil return nil
} }
//login Checks for valid username password combination. Returns singed jwt string //login Checks for valid username password combination. Returns singed jwt string
func login(ctx context.Context, username, password string) (LoginResult, error) { func login(ctx context.Context, username, password string) (types.LoginResult, error) {
// TODO: replace with propper user management // TODO: replace with propper user management
if username != "admin" && password != "hunter2" { if username != "admin" && password != "hunter2" {
return LoginResult{ return types.LoginResult{
Successful: false, Successful: false,
}, nil }, nil
} }
token := createJWT(createClaims(username)) token := helper.CreateJWT(helper.CreateClaims(username))
tokenString, err := token.SignedString([]byte("TODO")) tokenString, err := token.SignedString([]byte("TODO"))
if err != nil { if err != nil {
return LoginResult{ return types.LoginResult{
Successful: false, Successful: false,
}, err }, err
} }
return LoginResult{ return types.LoginResult{
Token: tokenString, Token: tokenString,
Successful: true, Successful: true,
}, nil }, nil

View File

@@ -1,4 +1,4 @@
package s3browser package gql
import ( import (
"fmt" "fmt"
@@ -6,11 +6,14 @@ import (
"github.com/graph-gophers/dataloader" "github.com/graph-gophers/dataloader"
"github.com/graphql-go/graphql" "github.com/graphql-go/graphql"
s3errors "git.kapelle.org/niklas/s3browser/internal/errors"
helper "git.kapelle.org/niklas/s3browser/internal/helper"
types "git.kapelle.org/niklas/s3browser/internal/types"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
// graphqlSchema generate the schema with its root query and mutation //GraphqlSchema generate the schema with its root query and mutation
func graphqlSchema() (graphql.Schema, error) { func GraphqlSchema() (graphql.Schema, error) {
queryFields := graphql.Fields{ queryFields := graphql.Fields{
"files": &graphql.Field{ "files": &graphql.Field{
@@ -21,8 +24,8 @@ func graphqlSchema() (graphql.Schema, error) {
}, },
}, },
Resolve: func(p graphql.ResolveParams) (interface{}, error) { Resolve: func(p graphql.ResolveParams) (interface{}, error) {
if is, err := isAuth(p.Context); !is { if !helper.IsAuthenticated(p.Context) {
return nil, err return nil, s3errors.ErrNotAuthenticated
} }
path, ok := p.Args["path"].(string) path, ok := p.Args["path"].(string)
@@ -46,8 +49,8 @@ func graphqlSchema() (graphql.Schema, error) {
}, },
}, },
Resolve: func(p graphql.ResolveParams) (interface{}, error) { Resolve: func(p graphql.ResolveParams) (interface{}, error) {
if is, err := isAuth(p.Context); !is { if !helper.IsAuthenticated(p.Context) {
return nil, err return nil, s3errors.ErrNotAuthenticated
} }
path, ok := p.Args["path"].(string) path, ok := p.Args["path"].(string)
@@ -71,8 +74,8 @@ func graphqlSchema() (graphql.Schema, error) {
}, },
}, },
Resolve: func(p graphql.ResolveParams) (interface{}, error) { Resolve: func(p graphql.ResolveParams) (interface{}, error) {
if is, err := isAuth(p.Context); !is { if !helper.IsAuthenticated(p.Context) {
return nil, err return nil, s3errors.ErrNotAuthenticated
} }
id, ok := p.Args["id"].(string) id, ok := p.Args["id"].(string)
@@ -82,7 +85,7 @@ func graphqlSchema() (graphql.Schema, error) {
log.Debug("querry 'file': ", id) log.Debug("querry 'file': ", id)
return File{ return types.File{
ID: id, ID: id,
}, nil }, nil
}, },
@@ -92,7 +95,7 @@ func graphqlSchema() (graphql.Schema, error) {
Type: graphql.NewNonNull(graphql.Boolean), Type: graphql.NewNonNull(graphql.Boolean),
Description: "True if the user is authorized", Description: "True if the user is authorized",
Resolve: func(p graphql.ResolveParams) (interface{}, error) { Resolve: func(p graphql.ResolveParams) (interface{}, error) {
auth, _ := isAuth(p.Context) auth := helper.IsAuthenticated(p.Context)
return auth, nil return auth, nil
}, },
@@ -108,8 +111,8 @@ func graphqlSchema() (graphql.Schema, error) {
}, },
}, },
Resolve: func(p graphql.ResolveParams) (interface{}, error) { Resolve: func(p graphql.ResolveParams) (interface{}, error) {
if is, err := isAuth(p.Context); !is { if !helper.IsAuthenticated(p.Context) {
return nil, err return nil, s3errors.ErrNotAuthenticated
} }
id, ok := p.Args["id"].(string) id, ok := p.Args["id"].(string)
@@ -133,8 +136,8 @@ func graphqlSchema() (graphql.Schema, error) {
}, },
}, },
Resolve: func(p graphql.ResolveParams) (interface{}, error) { Resolve: func(p graphql.ResolveParams) (interface{}, error) {
if is, err := isAuth(p.Context); !is { if !helper.IsAuthenticated(p.Context) {
return nil, err return nil, s3errors.ErrNotAuthenticated
} }
src, ok := p.Args["src"].(string) src, ok := p.Args["src"].(string)
@@ -162,8 +165,8 @@ func graphqlSchema() (graphql.Schema, error) {
}, },
}, },
Resolve: func(p graphql.ResolveParams) (interface{}, error) { Resolve: func(p graphql.ResolveParams) (interface{}, error) {
if is, err := isAuth(p.Context); !is { if !helper.IsAuthenticated(p.Context) {
return nil, err return nil, s3errors.ErrNotAuthenticated
} }
src, ok := p.Args["src"].(string) src, ok := p.Args["src"].(string)
@@ -188,8 +191,8 @@ func graphqlSchema() (graphql.Schema, error) {
}, },
}, },
Resolve: func(p graphql.ResolveParams) (interface{}, error) { Resolve: func(p graphql.ResolveParams) (interface{}, error) {
if is, err := isAuth(p.Context); !is { if !helper.IsAuthenticated(p.Context) {
return nil, err return nil, s3errors.ErrNotAuthenticated
} }
path, ok := p.Args["path"].(string) path, ok := p.Args["path"].(string)
@@ -210,8 +213,8 @@ func graphqlSchema() (graphql.Schema, error) {
}, },
}, },
Resolve: func(p graphql.ResolveParams) (interface{}, error) { Resolve: func(p graphql.ResolveParams) (interface{}, error) {
if is, err := isAuth(p.Context); !is { if !helper.IsAuthenticated(p.Context) {
return nil, err return nil, s3errors.ErrNotAuthenticated
} }
path, ok := p.Args["path"].(string) path, ok := p.Args["path"].(string)

View File

@@ -1,4 +1,4 @@
package s3browser package helper
import ( import (
"context" "context"
@@ -11,9 +11,11 @@ import (
"github.com/graph-gophers/dataloader" "github.com/graph-gophers/dataloader"
"github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
types "git.kapelle.org/niklas/s3browser/internal/types"
) )
func invalidateCache(ctx context.Context, id string) error { func InvalidateCache(ctx context.Context, id string) error {
loader, ok := ctx.Value("loader").(map[string]*dataloader.Loader) loader, ok := ctx.Value("loader").(map[string]*dataloader.Loader)
if !ok { if !ok {
return fmt.Errorf("Failed to get loader from context") return fmt.Errorf("Failed to get loader from context")
@@ -21,7 +23,7 @@ func invalidateCache(ctx context.Context, id string) error {
log.Debug("Invalidate cache for id: ", id) log.Debug("Invalidate cache for id: ", id)
path := getPathFromId(id) path := GetPathFromId(id)
loader["getFile"].Clear(ctx, dataloader.StringKey(id)) loader["getFile"].Clear(ctx, dataloader.StringKey(id))
loader["getFiles"].Clear(ctx, dataloader.StringKey(path)) loader["getFiles"].Clear(ctx, dataloader.StringKey(path))
@@ -30,21 +32,21 @@ func invalidateCache(ctx context.Context, id string) error {
return nil return nil
} }
func getPathFromId(id string) string { func GetPathFromId(id string) string {
dir := filepath.Dir(id) dir := filepath.Dir(id)
if dir == "." { if dir == "." {
return "/" return "/"
} }
return nomalizeID(dir + "/") return NomalizeID(dir + "/")
} }
func getFilenameFromID(id string) string { func GetFilenameFromID(id string) string {
return filepath.Base(id) return filepath.Base(id)
} }
func invalidateCacheForDir(ctx context.Context, path string) error { func InvalidateCacheForDir(ctx context.Context, path string) error {
loader, ok := ctx.Value("loader").(map[string]*dataloader.Loader) loader, ok := ctx.Value("loader").(map[string]*dataloader.Loader)
if !ok { if !ok {
return fmt.Errorf("Failed to get loader from context") return fmt.Errorf("Failed to get loader from context")
@@ -52,7 +54,7 @@ func invalidateCacheForDir(ctx context.Context, path string) error {
log.Debug("Invalidate cache for dir: ", path) log.Debug("Invalidate cache for dir: ", path)
parent := getParentDir(path) parent := GetParentDir(path)
log.Debug("Cache clear dir: ", path, " parent: ", parent) log.Debug("Cache clear dir: ", path, " parent: ", parent)
@@ -67,7 +69,7 @@ func invalidateCacheForDir(ctx context.Context, path string) error {
return nil return nil
} }
func deleteMultiple(ctx context.Context, s3Client minio.Client, ids []minio.ObjectInfo) error { func DeleteMultiple(ctx context.Context, s3Client minio.Client, ids []minio.ObjectInfo) error {
log.Debug("Delte multiple") log.Debug("Delte multiple")
objectsCh := make(chan minio.ObjectInfo, 1) objectsCh := make(chan minio.ObjectInfo, 1)
@@ -78,7 +80,7 @@ func deleteMultiple(ctx context.Context, s3Client minio.Client, ids []minio.Obje
} }
}() }()
for err := range s3Client.RemoveObjects(ctx, bucketName, objectsCh, minio.RemoveObjectsOptions{}) { for err := range s3Client.RemoveObjects(ctx, "dev", objectsCh, minio.RemoveObjectsOptions{}) {
log.Error("Failed to delete object ", err.ObjectName, " because: ", err.Err.Error()) log.Error("Failed to delete object ", err.ObjectName, " because: ", err.Err.Error())
// TODO: error handel // TODO: error handel
} }
@@ -86,8 +88,8 @@ func deleteMultiple(ctx context.Context, s3Client minio.Client, ids []minio.Obje
return nil return nil
} }
// nomalizeID makes sure there is a leading "/" in the id // NomalizeID makes sure there is a leading "/" in the id
func nomalizeID(id string) string { func NomalizeID(id string) string {
if !strings.HasPrefix(id, "/") { if !strings.HasPrefix(id, "/") {
if id == "." { if id == "." {
return "/" return "/"
@@ -98,7 +100,7 @@ func nomalizeID(id string) string {
return id return id
} }
func getParentDir(id string) string { func GetParentDir(id string) string {
dirs := strings.Split(id, "/") dirs := strings.Split(id, "/")
cut := 1 cut := 1
@@ -108,32 +110,23 @@ func getParentDir(id string) string {
parent := strings.Join(dirs[:len(dirs)-cut], "/") + "/" parent := strings.Join(dirs[:len(dirs)-cut], "/") + "/"
return nomalizeID(parent) return NomalizeID(parent)
} }
func isAuth(ctx context.Context) (bool, error) { func IsAuthenticated(ctx context.Context) bool {
token, ok := ctx.Value("jwt").(*jwt.Token) token, ok := ctx.Value("jwt").(*jwt.Token)
return (ok && token.Valid)
if !ok {
return false, extendError("UNAUTHORIZED", "Unauthorized")
}
if token.Valid {
return true, nil
} else {
return false, extendError("UNAUTHORIZED", "Unauthorized")
}
} }
func createJWT(claims *JWTClaims) *jwt.Token { func CreateJWT(claims *types.JWTClaims) *jwt.Token {
claims.ExpiresAt = time.Now().Add(time.Hour * 24).Unix() claims.ExpiresAt = time.Now().Add(time.Hour * 24).Unix()
return jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
} }
func createClaims(username string) *JWTClaims { func CreateClaims(username string) *types.JWTClaims {
return &JWTClaims{ return &types.JWTClaims{
StandardClaims: jwt.StandardClaims{ StandardClaims: jwt.StandardClaims{
Subject: username, Subject: username,
}, },

View File

@@ -1,7 +1,7 @@
//go:build !prod //go:build !prod
// +build !prod // +build !prod
package s3browser package httpserver
import "github.com/gorilla/mux" import "github.com/gorilla/mux"

View File

@@ -1,4 +1,4 @@
package s3browser package httpserver
import ( import (
"context" "context"
@@ -18,17 +18,20 @@ import (
"github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
helper "git.kapelle.org/niklas/s3browser/internal/helper"
types "git.kapelle.org/niklas/s3browser/internal/types"
) )
type JWTClaims struct { var (
jwt.StandardClaims tokenExp = int64((time.Hour * 23).Seconds())
} )
type CookieExtractor struct { type cookieExtractor struct {
Name string Name string
} }
func (c *CookieExtractor) ExtractToken(req *http.Request) (string, error) { func (c *cookieExtractor) ExtractToken(req *http.Request) (string, error) {
cookie, err := req.Cookie(c.Name) cookie, err := req.Cookie(c.Name)
if err == nil && len(cookie.Value) != 0 { if err == nil && len(cookie.Value) != 0 {
@@ -36,11 +39,10 @@ func (c *CookieExtractor) ExtractToken(req *http.Request) (string, error) {
} }
return "", jwtRequest.ErrNoTokenInRequest return "", jwtRequest.ErrNoTokenInRequest
} }
// 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 {
r := mux.NewRouter() r := mux.NewRouter()
gqlHandler := handler.New(&handler.Config{ gqlHandler := handler.New(&handler.Config{
@@ -64,8 +66,8 @@ func initHttp(resolveContext context.Context, schema graphql.Schema, address str
parsedToken, err := jwtRequest.ParseFromRequestWithClaims(r, jwtRequest.MultiExtractor{ parsedToken, err := jwtRequest.ParseFromRequestWithClaims(r, jwtRequest.MultiExtractor{
jwtRequest.AuthorizationHeaderExtractor, jwtRequest.AuthorizationHeaderExtractor,
&CookieExtractor{Name: "jwt"}, &cookieExtractor{Name: "jwt"},
}, &JWTClaims{}, jwtKeyFunc) }, &types.JWTClaims{}, jwtKeyFunc)
if err == nil && parsedToken.Valid { if err == nil && parsedToken.Valid {
newRequest := r.WithContext(context.WithValue(r.Context(), "jwt", parsedToken)) newRequest := r.WithContext(context.WithValue(r.Context(), "jwt", parsedToken))
@@ -79,14 +81,17 @@ func initHttp(resolveContext context.Context, schema graphql.Schema, address str
r.HandleFunc("/api/graphql", func(rw http.ResponseWriter, r *http.Request) { r.HandleFunc("/api/graphql", func(rw http.ResponseWriter, r *http.Request) {
token := r.Context().Value("jwt") token := r.Context().Value("jwt")
refreshTokenIfNeeded(rw, r)
gqlHandler.ContextHandler(context.WithValue(resolveContext, "jwt", token), rw, r) gqlHandler.ContextHandler(context.WithValue(resolveContext, "jwt", token), rw, r)
}) })
r.HandleFunc("/api/file", func(rw http.ResponseWriter, r *http.Request) { r.HandleFunc("/api/file", func(rw http.ResponseWriter, r *http.Request) {
refreshTokenIfNeeded(rw, r)
httpGetFile(resolveContext, rw, r) httpGetFile(resolveContext, rw, r)
}).Methods("GET") }).Methods("GET")
r.HandleFunc("/api/file", func(rw http.ResponseWriter, r *http.Request) { r.HandleFunc("/api/file", func(rw http.ResponseWriter, r *http.Request) {
refreshTokenIfNeeded(rw, r)
httpPostFile(resolveContext, rw, r) httpPostFile(resolveContext, rw, r)
}).Methods("POST") }).Methods("POST")
@@ -94,8 +99,6 @@ func initHttp(resolveContext context.Context, schema graphql.Schema, address str
r.HandleFunc("/api/logout", logout).Methods("POST") r.HandleFunc("/api/logout", logout).Methods("POST")
r.HandleFunc("/api/refresh", refreshToken).Methods("POST")
// Init the embedded static files // Init the embedded static files
initStatic(r) initStatic(r)
@@ -103,7 +106,7 @@ func initHttp(resolveContext context.Context, schema graphql.Schema, address str
} }
func httpGetFile(ctx context.Context, rw http.ResponseWriter, r *http.Request) { func httpGetFile(ctx context.Context, rw http.ResponseWriter, r *http.Request) {
if is, _ := isAuth(r.Context()); !is { if !helper.IsAuthenticated(r.Context()) {
rw.WriteHeader(http.StatusUnauthorized) rw.WriteHeader(http.StatusUnauthorized)
return return
} }
@@ -112,7 +115,7 @@ func httpGetFile(ctx context.Context, rw http.ResponseWriter, r *http.Request) {
id := r.URL.Query().Get("id") id := r.URL.Query().Get("id")
log.Debug("S3 call 'StatObject': ", id) log.Debug("S3 call 'StatObject': ", id)
objInfo, err := s3Client.StatObject(context.Background(), bucketName, id, minio.GetObjectOptions{}) objInfo, err := s3Client.StatObject(context.Background(), "dev", id, minio.GetObjectOptions{})
if err != nil { if err != nil {
rw.WriteHeader(http.StatusInternalServerError) rw.WriteHeader(http.StatusInternalServerError)
@@ -126,7 +129,7 @@ func httpGetFile(ctx context.Context, rw http.ResponseWriter, r *http.Request) {
} }
log.Debug("S3 call 'GetObject': ", id) log.Debug("S3 call 'GetObject': ", id)
obj, err := s3Client.GetObject(context.Background(), bucketName, id, minio.GetObjectOptions{}) obj, err := s3Client.GetObject(context.Background(), "dev", id, minio.GetObjectOptions{})
if err != nil { if err != nil {
rw.WriteHeader(http.StatusInternalServerError) rw.WriteHeader(http.StatusInternalServerError)
@@ -147,7 +150,7 @@ func httpGetFile(ctx context.Context, rw http.ResponseWriter, r *http.Request) {
} }
func httpPostFile(ctx context.Context, rw http.ResponseWriter, r *http.Request) { func httpPostFile(ctx context.Context, rw http.ResponseWriter, r *http.Request) {
if is, _ := isAuth(r.Context()); !is { if !helper.IsAuthenticated(r.Context()) {
rw.WriteHeader(http.StatusUnauthorized) rw.WriteHeader(http.StatusUnauthorized)
return return
} }
@@ -162,7 +165,7 @@ func httpPostFile(ctx context.Context, rw http.ResponseWriter, r *http.Request)
mimeType, _, _ := mime.ParseMediaType(contentType) mimeType, _, _ := mime.ParseMediaType(contentType)
log.Debug("S3 call 'PutObject': ", id) log.Debug("S3 call 'PutObject': ", id)
info, err := s3Client.PutObject(context.Background(), bucketName, id, r.Body, r.ContentLength, minio.PutObjectOptions{ info, err := s3Client.PutObject(context.Background(), "dev", id, r.Body, r.ContentLength, minio.PutObjectOptions{
ContentType: mimeType, ContentType: mimeType,
}) })
@@ -172,7 +175,7 @@ func httpPostFile(ctx context.Context, rw http.ResponseWriter, r *http.Request)
} }
// Invalidate cache // Invalidate cache
invalidateCache(ctx, info.Key) helper.InvalidateCache(ctx, info.Key)
rw.WriteHeader(http.StatusCreated) rw.WriteHeader(http.StatusCreated)
} }
@@ -196,7 +199,7 @@ func setLoginCookie(rw http.ResponseWriter, r *http.Request) {
tokenString := string(body) tokenString := string(body)
token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, jwtKeyFunc) token, err := jwt.ParseWithClaims(tokenString, &types.JWTClaims{}, jwtKeyFunc)
if err != nil { if err != nil {
rw.WriteHeader(http.StatusInternalServerError) rw.WriteHeader(http.StatusInternalServerError)
@@ -208,7 +211,7 @@ func setLoginCookie(rw http.ResponseWriter, r *http.Request) {
return return
} }
claims, ok := token.Claims.(*JWTClaims) claims, ok := token.Claims.(*types.JWTClaims)
if !ok { if !ok {
rw.WriteHeader(http.StatusInternalServerError) rw.WriteHeader(http.StatusInternalServerError)
@@ -245,32 +248,31 @@ func logout(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(http.StatusNoContent) rw.WriteHeader(http.StatusNoContent)
} }
func refreshToken(rw http.ResponseWriter, r *http.Request) { func refreshTokenIfNeeded(rw http.ResponseWriter, r *http.Request) {
if is, _ := isAuth(r.Context()); !is { currentToken, ok := r.Context().Value("jwt").(*jwt.Token)
rw.WriteHeader(http.StatusUnauthorized)
if !ok && currentToken == nil {
return return
} }
oldToken, ok := r.Context().Value("jwt").(*jwt.Token) claims, ok := currentToken.Claims.(*types.JWTClaims)
if !ok { if !ok {
rw.WriteHeader(http.StatusInternalServerError) log.Error("Failed to refresh JWT")
return return
} }
claims, ok := oldToken.Claims.(*JWTClaims) // Refresh only if token older than 1 hour
if (claims.ExpiresAt - time.Now().Unix()) > tokenExp {
if !ok {
rw.WriteHeader(http.StatusInternalServerError)
return return
} }
token := createJWT(claims) newToken := helper.CreateJWT(claims)
tokenString, err := token.SignedString([]byte("TODO")) tokenString, err := newToken.SignedString([]byte("TODO"))
if err != nil { if err != nil {
rw.WriteHeader(http.StatusInternalServerError) log.Error("Failed to refresh JWT")
return return
} }
@@ -284,4 +286,6 @@ func refreshToken(rw http.ResponseWriter, r *http.Request) {
} }
http.SetCookie(rw, cookie) http.SetCookie(rw, cookie)
log.Debug("Refreshed JWT")
} }

View File

@@ -1,7 +1,7 @@
//go:build prod //go:build prod
// +build prod // +build prod
package s3browser package httpserver
import ( import (
"embed" "embed"

View File

@@ -3,48 +3,20 @@ package s3browser
import ( import (
"context" "context"
"fmt" "fmt"
"time"
"github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials" "github.com/minio/minio-go/v7/pkg/credentials"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
gql "git.kapelle.org/niklas/s3browser/internal/gql"
httpserver "git.kapelle.org/niklas/s3browser/internal/httpserver"
types "git.kapelle.org/niklas/s3browser/internal/types"
) )
// AppConfig general config
type AppConfig struct {
S3Endoint string
S3AccessKey string
S3SecretKey string
S3SSL bool
S3Bucket string
CacheTTL time.Duration
CacheCleanup time.Duration
Address string
LogDebug bool
}
// File represents a file with its metadata
type File struct {
ID string `json:"id"`
Name string `json:"name"`
Size int64 `json:"size"`
ContentType string `json:"contentType"`
ETag string `json:"etag"`
LastModified time.Time `json:"lastModified"`
}
// Directory represents a directory with its metadata
type Directory struct {
ID string `json:"id"`
Name string `json:"name"`
Files []File `json:"files"`
Directorys []Directory `json:"directorys"`
}
var bucketName string var bucketName string
// setupS3Client connect the s3Client // setupS3Client connect the s3Client
func setupS3Client(config AppConfig) (*minio.Client, error) { func setupS3Client(config types.AppConfig) (*minio.Client, error) {
minioClient, err := minio.New(config.S3Endoint, &minio.Options{ minioClient, err := minio.New(config.S3Endoint, &minio.Options{
Creds: credentials.NewStaticV4(config.S3AccessKey, config.S3SecretKey, ""), Creds: credentials.NewStaticV4(config.S3AccessKey, config.S3SecretKey, ""),
Secure: config.S3SSL, Secure: config.S3SSL,
@@ -70,7 +42,7 @@ func setupS3Client(config AppConfig) (*minio.Client, error) {
} }
// Start starts the app // Start starts the app
func Start(config AppConfig) { func Start(config types.AppConfig) {
if config.LogDebug { if config.LogDebug {
log.SetLevel(log.DebugLevel) log.SetLevel(log.DebugLevel)
@@ -90,8 +62,8 @@ func Start(config AppConfig) {
loaderMap := createDataloader(config) loaderMap := createDataloader(config)
log.Debug("Generating graphq schema") log.Debug("Generating graphq schema")
graphqlTypes() gql.GraphqlTypes()
schema, err := graphqlSchema() schema, err := gql.GraphqlSchema()
if err != nil { if err != nil {
log.Error("Failed to generate graphq schemas: ", err.Error()) log.Error("Failed to generate graphq schemas: ", err.Error())
@@ -102,7 +74,7 @@ func Start(config AppConfig) {
resolveContext = context.WithValue(resolveContext, "loader", loaderMap) resolveContext = context.WithValue(resolveContext, "loader", loaderMap)
log.Debug("Starting HTTP server") log.Debug("Starting HTTP server")
err = initHttp(resolveContext, schema, config.Address) err = httpserver.InitHttp(resolveContext, schema, config.Address)
if err != nil { if err != nil {
log.Error("Failed to start webserver: ", err.Error()) log.Error("Failed to start webserver: ", err.Error())

47
internal/types/types.go Normal file
View File

@@ -0,0 +1,47 @@
package types
import (
"time"
"github.com/golang-jwt/jwt"
)
// AppConfig general config
type AppConfig struct {
S3Endoint string
S3AccessKey string
S3SecretKey string
S3SSL bool
S3Bucket string
CacheTTL time.Duration
CacheCleanup time.Duration
Address string
LogDebug bool
}
// File represents a file with its metadata
type File struct {
ID string `json:"id"`
Name string `json:"name"`
Size int64 `json:"size"`
ContentType string `json:"contentType"`
ETag string `json:"etag"`
LastModified time.Time `json:"lastModified"`
}
// Directory represents a directory with its metadata
type Directory struct {
ID string `json:"id"`
Name string `json:"name"`
Files []File `json:"files"`
Directorys []Directory `json:"directorys"`
}
type JWTClaims struct {
jwt.StandardClaims
}
type LoginResult struct {
Token string `json:"token"`
Successful bool `json:"successful"`
}