diff --git a/server/internal/app/app.go b/server/internal/app/app.go index c59c5594..af1fa0bd 100644 --- a/server/internal/app/app.go +++ b/server/internal/app/app.go @@ -2,44 +2,118 @@ package app import ( "context" + "errors" "github.com/google/uuid" + "github.com/jackc/pgx/v5" "github.com/shroff/phylum/server/internal/app/core" "github.com/shroff/phylum/server/internal/db" "github.com/shroff/phylum/server/internal/storage" ) +const defaultUserName = "phylum" + type App struct { - Debug bool - adminfs core.FileSystem - db *db.DbHandler - cs storage.Storage + Debug bool + rootfs core.FileSystem + db *db.DbHandler + cs storage.Storage } var Default *App -func Initialize(db *db.DbHandler, cs storage.Storage, debug bool) error { +func Create(db *db.DbHandler, cs storage.Storage, debug bool) error { Default = &App{ Debug: debug, db: db, cs: cs, } - if root, err := Default.UserByUsername(context.Background(), "phylum"); err != nil { + // if root, err := Default.UserByUsername(context.Background(), "phylum"); err != nil { + // return err + // } else if fs, err := Default.OpenFileSystem(context.Background(), root.ID()); err != nil { + // return err + // } else { + // Default.rootfs = fs + // if _, err := fs.ResourceByPath("/home"); err != nil { + // _, err := fs.CreateMemberResource(fs.Root(), uuid.New(), "home", true) + // return err + // } + // } + return Default.setupAppData() +} + +func (a App) setupAppData() error { + ctx := context.Background() + + _, err := a.db.Queries().UserByUsername(context.Background(), defaultUserName) + // Root user found. We can assume that setup has been done before + if !errors.Is(err, pgx.ErrNoRows) { return err - } else if fs, err := Default.OpenFileSystem(context.Background(), root.ID()); err != nil { - return err - } else { - Default.adminfs = fs - if _, err := fs.ResourceByPath("/home"); err != nil { - _, err := fs.CreateMemberResource(fs.Root(), uuid.New(), "home", true) + } + return a.db.RunInTx(ctx, func(tx pgx.Tx) error { + q := a.db.Queries().WithTx(tx) + // Create phylum user + user, err := q.CreateUser(ctx, db.CreateUserParams{ + Username: defaultUserName, + DisplayName: "Phylum", + PasswordHash: "", + }) + if err != nil { return err } - } - return nil + // Create root folder + root, err := q.CreateResource(ctx, db.CreateResourceParams{ + ID: uuid.New(), + Owner: user.ID, + Name: "Phylum Root", + Dir: true, + }) + if err != nil { + return err + } + + // Create home folder + home, err := q.CreateResource(ctx, db.CreateResourceParams{ + ID: uuid.New(), + Owner: user.ID, + Parent: &root.ID, + Name: "home", + Dir: true, + }) + if err != nil { + return err + } + + // Create personal home folder + phylumHome, err := q.CreateResource(ctx, db.CreateResourceParams{ + ID: uuid.New(), + Owner: user.ID, + Parent: &home.ID, + Name: user.Username, + Dir: true, + }) + if err != nil { + return err + } + + if err := q.UpdateUserDirs(ctx, db.UpdateUserDirsParams{ID: user.ID, Root: root.ID, Home: phylumHome.ID}); err != nil { + return err + } + _, err = tx.Exec(ctx, "SET CONSTRAINTS user_root, user_home IMMEDIATE;") + if err != nil { + return err + } + + _, err = tx.Exec(ctx, "ALTER TABLE users ALTER CONSTRAINT user_root NOT DEFERRABLE;"+ + "ALTER TABLE users ALTER CONSTRAINT user_home NOT DEFERRABLE;", + ) + + return err + }) } -func (a App) OpenFileSystem(ctx context.Context, user int32) (core.FileSystem, error) { - return core.OpenFileSystem(a.db, ctx, a.cs, nil, user) +func (a App) OpenFileSystem(ctx context.Context, user core.User) (core.FileSystem, error) { + return core.OpenFileSystem(a.db, ctx, a.cs, user) } diff --git a/server/internal/app/auth.go b/server/internal/app/auth.go index deb66a45..51a09825 100644 --- a/server/internal/app/auth.go +++ b/server/internal/app/auth.go @@ -8,9 +8,9 @@ import ( "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgtype" + "github.com/shroff/phylum/server/internal/app/core" "github.com/shroff/phylum/server/internal/cryptutil" "github.com/shroff/phylum/server/internal/db" - "github.com/sirupsen/logrus" "golang.org/x/exp/rand" ) @@ -25,32 +25,34 @@ var ErrCredentialsInvalid = errors.New("credentials invalid") var ErrTokenInvalid = errors.New("token invalid") var ErrTokenExpired = errors.New("token expired") -func (a App) VerifyUserPassword(ctx context.Context, username, password string) (int32, error) { +func (a App) VerifyUserPassword(ctx context.Context, username, password string) (core.User, error) { if user, err := a.db.Queries().UserByUsername(context.Background(), username); err != nil { if errors.Is(err, pgx.ErrNoRows) { - return 0, ErrCredentialsInvalid + return nil, ErrCredentialsInvalid } - logrus.Info(err) - return 0, err + return nil, err } else { if b, err := cryptutil.VerifyPassword(password, user.PasswordHash); err != nil { - logrus.Warn(err.Error()) - return 0, err + return nil, err } else if !b { - return 0, ErrCredentialsInvalid + return nil, ErrCredentialsInvalid + } + if user, err := a.UserByID(context.Background(), user.ID); err != nil { + return nil, err + } else { + return user, nil } - return user.ID, nil } } func (a App) CreateAccessToken(username, password string) (db.AccessToken, error) { - if userID, err := a.VerifyUserPassword(context.Background(), username, password); err != nil { + if user, err := a.VerifyUserPassword(context.Background(), username, password); err != nil { return db.AccessToken{}, err } else { if token, err := a.db.Queries().InsertAccessToken(context.Background(), db.InsertAccessTokenParams{ ID: GenerateRandomString(accessTokenLength), Validity: accessTokenValiditiy, - UserID: userID, + UserID: user.ID(), }); err != nil { return db.AccessToken{}, err } else { @@ -59,17 +61,21 @@ func (a App) CreateAccessToken(username, password string) (db.AccessToken, error } } -func (a App) VerifyAccessToken(accessToken string) (int32, error) { +func (a App) VerifyAccessToken(accessToken string) (core.User, error) { token, err := a.db.Queries().AccessTokenById(context.Background(), accessToken) if errors.Is(err, pgx.ErrNoRows) { - return 0, ErrTokenInvalid + return nil, ErrTokenInvalid } else if err != nil { - return 0, err + return nil, err } if time.Now().After(token.Expires.Time) { - return 0, ErrTokenExpired + return nil, ErrTokenExpired + } + if user, err := a.UserByID(context.Background(), token.UserID); err != nil { + return nil, err + } else { + return user, nil } - return token.UserID, nil } const ( diff --git a/server/internal/app/core/filesystem.go b/server/internal/app/core/filesystem.go index 37fa4737..f43eadd6 100644 --- a/server/internal/app/core/filesystem.go +++ b/server/internal/app/core/filesystem.go @@ -31,8 +31,6 @@ type FileSystem interface { UpdateOwner(r Resource, userID int32) error } -var rootUUID = uuid.UUID{} - type filesystem struct { db *db.DbHandler ctx context.Context @@ -41,31 +39,33 @@ type filesystem struct { user int32 } -func OpenFileSystem(db *db.DbHandler, ctx context.Context, cs storage.Storage, root Resource, user int32) (FileSystem, error) { - if root == nil { - if res, err := db.Queries().RootResource(ctx, user); err == nil { - root = resource{ - id: rootUUID, - owner: res.Owner, - permission: res.Permission, - parentID: nil, - name: res.Name, - size: 0, - collection: true, - modTime: res.Modified.Time, - delTime: nil, - etag: "", - } - } else { - return nil, err +func OpenFileSystem(dbh *db.DbHandler, ctx context.Context, cs storage.Storage, user User) (FileSystem, error) { + var root Resource + if res, err := dbh.Queries().ResourceByID(ctx, db.ResourceByIDParams{ + ResourceID: user.Root(), + UserID: user.ID(), + }); err != nil { + return nil, err + } else { + root = resource{ + id: res.ID, + owner: res.Owner, + permission: res.Permission, + parentID: nil, + name: res.Name, + size: 0, + collection: true, + modTime: res.Modified.Time, + delTime: nil, + etag: "", } } return filesystem{ - db: db, + db: dbh, ctx: ctx, cs: cs, root: root, - user: user, + user: user.ID(), }, nil } @@ -114,7 +114,7 @@ func (f filesystem) ResourceByPath(path string) (Resource, error) { } func (f filesystem) ResourceByID(id uuid.UUID) (Resource, error) { - res, err := f.db.Queries().ResourceByID(f.ctx, db.ResourceByIDParams{Root: f.root.ID(), Permission: f.root.Permission(), ResourceID: id, UserID: f.user}) + res, err := f.db.Queries().ResourceByID(f.ctx, db.ResourceByIDParams{Root: f.root.ID(), ResourceID: id, UserID: f.user}) // TODO: verify found if err == pgx.ErrNoRows || !res.Found || res.Permission == 0 { err = fs.ErrNotExist @@ -201,7 +201,7 @@ func (f filesystem) CreateMemberResource(r Resource, id uuid.UUID, name string, return nil, ErrInsufficientPermissions } var result db.Resource - err := f.db.RunInTx(f.ctx, func(q *db.Queries) error { + err := f.db.WithTx(f.ctx, func(q *db.Queries) error { var err error parent := r.ID() if result, err = q.CreateResource(f.ctx, db.CreateResourceParams{ID: id, Owner: f.user, Parent: &parent, Name: name, Dir: dir}); err != nil { @@ -231,7 +231,7 @@ func (f filesystem) DeleteRecursive(r Resource, hardDelete bool) error { return ErrInsufficientPermissions } // TODO: versioning - return f.db.RunInTx(f.ctx, func(q *db.Queries) error { + return f.db.WithTx(f.ctx, func(q *db.Queries) error { if hardDelete { deleted, err := q.HardDeleteRecursive(f.ctx, r.ID()) if err != nil { diff --git a/server/internal/app/core/user.go b/server/internal/app/core/user.go new file mode 100644 index 00000000..8828ba4f --- /dev/null +++ b/server/internal/app/core/user.go @@ -0,0 +1,22 @@ +package core + +import "github.com/google/uuid" + +type User interface { + ID() int32 + Username() string + DisplayName() string + Root() uuid.UUID +} + +type user struct { + id int32 + username string + displayName string + root uuid.UUID +} + +func (u user) ID() int32 { return u.id } +func (u user) Username() string { return u.username } +func (u user) DisplayName() string { return u.displayName } +func (u user) Root() uuid.UUID { return u.root } diff --git a/server/internal/app/users.go b/server/internal/app/users.go index 8599f2d0..23ef0442 100644 --- a/server/internal/app/users.go +++ b/server/internal/app/users.go @@ -6,62 +6,57 @@ import ( "github.com/google/uuid" "github.com/jackc/pgx/v5" + "github.com/shroff/phylum/server/internal/app/core" "github.com/shroff/phylum/server/internal/cryptutil" "github.com/shroff/phylum/server/internal/db" ) var ErrUserNotFound = errors.New("user not found") -type User interface { - ID() int32 - Username() string - DisplayName() string -} - -type user struct { - id int32 - username string - displayName string -} - -func (u user) ID() int32 { return u.id } -func (u user) Username() string { return u.username } -func (u user) DisplayName() string { return u.displayName } - -func (a App) CreateUser(ctx context.Context, username, displayName, password string) error { +func (a App) CreateUser(ctx context.Context, username, displayName, password string, root *uuid.UUID) error { + var rootID = a.rootfs.Root().ID() + if root != nil { + rootID = *root + } if hash, err := cryptutil.GenerateArgon2EncodedHash(password, cryptutil.DefaultArgon2Params()); err != nil { return err - } else if u, err := a.db.Queries().CreateUser(ctx, db.CreateUserParams{Username: username, DisplayName: displayName, PasswordHash: hash}); err != nil { + } else if u, err := a.db.Queries().CreateUser(ctx, db.CreateUserParams{ + Username: username, + DisplayName: displayName, + PasswordHash: hash, + Root: rootID, + Home: rootID, + }); err != nil { return err - } else if home, err := a.adminfs.ResourceByPath("/home"); err != nil { + } else if home, err := a.rootfs.ResourceByPath("/home"); err != nil { return err - } else if home, err := a.adminfs.CreateMemberResource(home, uuid.New(), username, true); err != nil { + } else if home, err := a.rootfs.CreateMemberResource(home, uuid.New(), username, true); err != nil { return err - } else if err := a.adminfs.UpdateOwner(home, u.ID); err != nil { + } else if err := a.rootfs.UpdateOwner(home, u.ID); err != nil { return err } else { - homeID := home.ID() - return a.db.Queries().UpdateUserHome(ctx, db.UpdateUserHomeParams{ID: u.ID, Home: &homeID}) + return a.db.Queries().UpdateUserDirs(ctx, db.UpdateUserDirsParams{ID: u.ID, Root: rootID, Home: home.ID()}) } } -func (a App) ListUsers(ctx context.Context) ([]User, error) { +func (a App) ListUsers(ctx context.Context) ([]core.User, error) { results, err := a.db.Queries().ListUsers(ctx) if err != nil { return nil, err } - users := make([]User, len(results)) + users := make([]core.User, len(results)) for i, r := range results { - users[i] = user{ + users[i] = core.user{ id: r.ID, username: r.Username, displayName: r.DisplayName, + root: r.Root, } } return users, nil } -func (a App) UserByUsername(ctx context.Context, username string) (User, error) { +func (a App) UserByUsername(ctx context.Context, username string) (core.User, error) { result, err := a.db.Queries().UserByUsername(ctx, username) if err != nil { if errors.Is(err, pgx.ErrNoRows) { @@ -73,10 +68,11 @@ func (a App) UserByUsername(ctx context.Context, username string) (User, error) id: result.ID, username: result.Username, displayName: result.DisplayName, + root: result.Root, }, nil } -func (a App) UserByID(ctx context.Context, userID int32) (User, error) { +func (a App) UserByID(ctx context.Context, userID int32) (core.User, error) { result, err := a.db.Queries().UserByID(ctx, userID) if err != nil { if errors.Is(err, pgx.ErrNoRows) { @@ -88,5 +84,6 @@ func (a App) UserByID(ctx context.Context, userID int32) (User, error) { id: result.ID, username: result.Username, displayName: result.DisplayName, + root: result.Root, }, nil } diff --git a/server/internal/command/appcmd/appcmd.go b/server/internal/command/appcmd/appcmd.go index 49e41bc6..b8d072fe 100644 --- a/server/internal/command/appcmd/appcmd.go +++ b/server/internal/command/appcmd/appcmd.go @@ -27,7 +27,7 @@ func SetupCommand(db **db.DbHandler, debug bool) *cobra.Command { if cs, err = storage.Open(*db, viper.GetString("content-dir")); err != nil { logrus.Fatal(err) } else { - if err := app.Initialize(*db, cs, debug); err != nil { + if err := app.Create(*db, cs, debug); err != nil { logrus.Fatal(err) } } diff --git a/server/internal/command/appcmd/resource.go b/server/internal/command/appcmd/resource.go index 6f5c93d7..61d95e0f 100644 --- a/server/internal/command/appcmd/resource.go +++ b/server/internal/command/appcmd/resource.go @@ -14,7 +14,7 @@ import ( "github.com/spf13/cobra" ) -var userID int32 +var fs core.FileSystem func setupResourceCommand() *cobra.Command { cmd := &cobra.Command{ @@ -37,14 +37,10 @@ func setupResourceMkdirCommand() *cobra.Command { Short: "Create Directory", Args: cobra.ExactArgs(1), PreRun: func(cmd *cobra.Command, args []string) { - readUserIDFromFlagsreadUsername(cmd) + openFileSystemFromFlags(cmd) }, Run: func(cmd *cobra.Command, args []string) { path := args[0] - fs, err := app.Default.OpenFileSystem(context.Background(), userID) - if err != nil { - logrus.Fatal(err) - } if _, err := fs.ResourceByPath(path); err == nil { logrus.Fatal("Resource already exists: " + path) } @@ -78,14 +74,10 @@ func setupResourceRmCommand() *cobra.Command { Short: "Delete Resource", Args: cobra.ExactArgs(1), PreRun: func(cmd *cobra.Command, args []string) { - readUserIDFromFlagsreadUsername(cmd) + openFileSystemFromFlags(cmd) }, Run: func(cmd *cobra.Command, args []string) { path := args[0] - fs, err := app.Default.OpenFileSystem(context.Background(), userID) - if err != nil { - logrus.Fatal(err) - } r, err := fs.ResourceByPath(path) if err != nil { if errors.Is(err, iofs.ErrNotExist) { @@ -125,14 +117,10 @@ func setupResourceLsCommand() *cobra.Command { Short: "List Resource Details", Args: cobra.ExactArgs(1), PreRun: func(cmd *cobra.Command, args []string) { - readUserIDFromFlagsreadUsername(cmd) + openFileSystemFromFlags(cmd) }, Run: func(cmd *cobra.Command, args []string) { path := args[0] - fs, err := app.Default.OpenFileSystem(context.Background(), userID) - if err != nil { - logrus.Fatal(err) - } var r core.Resource if path[0] != '/' { var id uuid.UUID @@ -184,10 +172,11 @@ func setupResourceChownCommand() *cobra.Command { Short: "Change Resource Owner", Args: cobra.ExactArgs(2), PreRun: func(cmd *cobra.Command, args []string) { - readUserIDFromFlagsreadUsername(cmd) + openFileSystemFromFlags(cmd) }, Run: func(cmd *cobra.Command, args []string) { username := args[0] + path := args[1] var owner int32 if user, err := app.Default.UserByUsername(context.Background(), username); err != nil { logrus.Fatal(err) @@ -195,11 +184,6 @@ func setupResourceChownCommand() *cobra.Command { owner = user.ID() } - path := args[1] - fs, err := app.Default.OpenFileSystem(context.Background(), userID) - if err != nil { - logrus.Fatal(err) - } var r core.Resource if path[0] != '/' { var id uuid.UUID @@ -248,14 +232,15 @@ func setupResourceChownCommand() *cobra.Command { return &cmd } -func readUserIDFromFlagsreadUsername(cmd *cobra.Command) { +func openFileSystemFromFlags(cmd *cobra.Command) { + var user core.User if value, err := cmd.Flags().GetInt32("user"); err != nil { logrus.Fatal(err) } else if value != 0 { if user, err := app.Default.UserByID(context.Background(), value); err != nil { logrus.Fatal(err) } else { - userID = user.ID() + user = user return } } @@ -266,10 +251,16 @@ func readUserIDFromFlagsreadUsername(cmd *cobra.Command) { if user, err := app.Default.UserByUsername(context.Background(), value); err != nil { logrus.Fatal(err) } else { - userID = user.ID() + user = user } } + var err error + fs, err = app.Default.OpenFileSystem(context.Background(), user) + if err != nil { + logrus.Fatal(err) + } + } func setupUsernameFlags(cmd *cobra.Command) { diff --git a/server/internal/command/appcmd/user.go b/server/internal/command/appcmd/user.go index 168061b4..5c01671b 100644 --- a/server/internal/command/appcmd/user.go +++ b/server/internal/command/appcmd/user.go @@ -61,7 +61,7 @@ func setupUserAddCommand() *cobra.Command { } } - if err := app.Default.CreateUser(context.Background(), username, displayName, password); err != nil { + if err := app.Default.CreateUser(context.Background(), username, displayName, password, nil); err != nil { logrus.Fatal(err) } }, diff --git a/server/internal/db/db_handler.go b/server/internal/db/db_handler.go index 0a864d92..dc7f6ffc 100644 --- a/server/internal/db/db_handler.go +++ b/server/internal/db/db_handler.go @@ -125,7 +125,13 @@ func (d DbHandler) Queries() *Queries { return d.queries } -func (d DbHandler) RunInTx(ctx context.Context, fn func(*Queries) error) error { +func (d DbHandler) RunInTx(ctx context.Context, fn func(pgx.Tx) error) error { + return pgx.BeginFunc(ctx, d.pool, func(tx pgx.Tx) error { + return fn(tx) + }) +} + +func (d DbHandler) WithTx(ctx context.Context, fn func(*Queries) error) error { return pgx.BeginFunc(ctx, d.pool, func(tx pgx.Tx) error { q := d.queries.WithTx(tx) return fn(q) diff --git a/server/internal/db/migrations/data/001_core.sql b/server/internal/db/migrations/data/001_core.sql new file mode 100644 index 00000000..d00b00e8 --- /dev/null +++ b/server/internal/db/migrations/data/001_core.sql @@ -0,0 +1,50 @@ +CREATE TABLE users( + id SERIAL PRIMARY KEY, + username TEXT NOT NULL UNIQUE, + created TIMESTAMP NOT NULL, + modified TIMESTAMP NOT NULL, + display_name TEXT NOT NULL, + password_hash TEXT NOT NULL, + deleted TIMESTAMP, + root uuid NOT NULL, + home uuid NOT NULL +); +CREATE TABLE resources ( + id uuid PRIMARY KEY, + owner INT NOT NULL, + parent uuid REFERENCES resources(id) ON UPDATE CASCADE ON DELETE CASCADE, + name TEXT NOT NULL, + dir BOOLEAN NOT NULL, + created TIMESTAMP NOT NULL, + modified TIMESTAMP NOT NULL, + deleted TIMESTAMP, + size BIGINT, + etag TEXT +); + +ALTER TABLE resources + ADD CONSTRAINT resource_owner + FOREIGN KEY (owner) + REFERENCES users(id) + ON UPDATE CASCADE + ON DELETE CASCADE; + +ALTER TABLE users + ADD CONSTRAINT user_root + FOREIGN KEY (root) + REFERENCES resources(id) + ON UPDATE RESTRICT + DEFERRABLE INITIALLY DEFERRED; + +ALTER TABLE users + ADD CONSTRAINT user_home + FOREIGN KEY (home) + REFERENCES resources(id) + ON UPDATE RESTRICT + DEFERRABLE INITIALLY DEFERRED; + +---- create above / drop below ---- + +DROP TABLE users; + +DROP TABLE resources; \ No newline at end of file diff --git a/server/internal/db/migrations/data/001_users.sql b/server/internal/db/migrations/data/001_users.sql deleted file mode 100644 index d42d1723..00000000 --- a/server/internal/db/migrations/data/001_users.sql +++ /dev/null @@ -1,21 +0,0 @@ -CREATE TABLE users( - id SERIAL PRIMARY KEY, - username TEXT NOT NULL UNIQUE, - created TIMESTAMP NOT NULL, - modified TIMESTAMP NOT NULL, - display_name TEXT NOT NULL, - password_hash TEXT NOT NULL, - deleted TIMESTAMP -); - -INSERT INTO users(username, created, modified, display_name, password_hash) VALUES( - 'phylum', - NOW(), - NOW(), - 'Phylum', - 'CANNOT_LOG_IN' -); - ----- create above / drop below ---- - -DROP TABLE users; \ No newline at end of file diff --git a/server/internal/db/migrations/data/002_resources.sql b/server/internal/db/migrations/data/002_resources.sql deleted file mode 100644 index 926410fb..00000000 --- a/server/internal/db/migrations/data/002_resources.sql +++ /dev/null @@ -1,33 +0,0 @@ -CREATE TABLE resources ( - id uuid PRIMARY KEY, - owner INT NOT NULL REFERENCES users(id) ON UPDATE CASCADE ON DELETE CASCADE, - parent uuid REFERENCES resources(id) ON UPDATE CASCADE ON DELETE CASCADE, - name TEXT NOT NULL, - dir BOOLEAN NOT NULL, - created TIMESTAMP NOT NULL, - modified TIMESTAMP NOT NULL, - deleted TIMESTAMP, - size BIGINT, - etag TEXT -); - -INSERT INTO resources( -SELECT '00000000-0000-0000-0000-000000000000', - id, - NULL, - 'root', - true, - NOW(), - NOW(), - NULL, - 0, - '' -FROM users where username = 'phylum'); - -CREATE UNIQUE INDEX unique_member_resource_name ON resources(parent, name) WHERE deleted IS NULL; - ----- create above / drop below ---- - -DROP INDEX unique_member_resource_name; - -DROP TABLE resources; \ No newline at end of file diff --git a/server/internal/db/migrations/data/003_storage_backends.sql b/server/internal/db/migrations/data/002_storage_backends.sql similarity index 100% rename from server/internal/db/migrations/data/003_storage_backends.sql rename to server/internal/db/migrations/data/002_storage_backends.sql diff --git a/server/internal/db/migrations/data/004_access_tokens.sql b/server/internal/db/migrations/data/003_access_tokens.sql similarity index 100% rename from server/internal/db/migrations/data/004_access_tokens.sql rename to server/internal/db/migrations/data/003_access_tokens.sql diff --git a/server/internal/db/migrations/data/005_permissions.sql b/server/internal/db/migrations/data/004_permissions.sql similarity index 100% rename from server/internal/db/migrations/data/005_permissions.sql rename to server/internal/db/migrations/data/004_permissions.sql diff --git a/server/internal/db/migrations/data/006_user_home.sql b/server/internal/db/migrations/data/006_user_home.sql deleted file mode 100644 index a41e4ec3..00000000 --- a/server/internal/db/migrations/data/006_user_home.sql +++ /dev/null @@ -1,5 +0,0 @@ -ALTER TABLE users ADD home uuid REFERENCES resources(id) ON UPDATE CASCADE ON DELETE CASCADE; - ----- create above / drop below ---- - -ALTER TABLE users DROP COLUMN home; \ No newline at end of file diff --git a/server/internal/db/models.go b/server/internal/db/models.go index e864dccf..7c5d3b92 100644 --- a/server/internal/db/models.go +++ b/server/internal/db/models.go @@ -49,5 +49,6 @@ type User struct { DisplayName string PasswordHash string Deleted pgtype.Timestamp - Home *uuid.UUID + Root uuid.UUID + Home uuid.UUID } diff --git a/server/internal/db/permissions.sql.go b/server/internal/db/permissions.sql.go index 5ff62ea3..f66da016 100644 --- a/server/internal/db/permissions.sql.go +++ b/server/internal/db/permissions.sql.go @@ -105,22 +105,25 @@ WITH RECURSIVE nodes(resid, id, parent, found, permission) AS ( WHEN r.id = $1::uuid THEN true ELSE false END, - $3::int + CASE + WHEN r.owner = $3::int THEN 127 + WHEN p.permission IS NOT NULL THEN p.permission + ELSE 0 + END FROM resources r LEFT JOIN permissions p - on r.id = p.resource_id - AND p.user_id = $4::int + ON p.resource_id = r.id + AND p.user_id = $3::int WHERE r.id = $2::uuid UNION ALL SELECT n.resid, r.id, r.parent, CASE - WHEN r.id = $1::uuid - THEN true + WHEN r.id = $1::uuid THEN true ELSE n.found END, CASE WHEN n.permission IS NOT NULL THEN n.permission - WHEN r.owner = $4::int THEN 127 + WHEN r.owner = $3::int THEN 127 WHEN p.permission IS NOT NULL THEN p.permission ELSE 0 END @@ -128,10 +131,10 @@ WITH RECURSIVE nodes(resid, id, parent, found, permission) AS ( JOIN nodes n ON r.id = n.parent LEFT JOIN permissions p - ON r.id = p.resource_id AND p.user_id = $4::int + ON r.id = p.resource_id AND p.user_id = $3::int WHERE n.parent IS NOT NULL ) -SELECT resid AS id, found, r.owner, permission, r.id, r.parent, name, dir, created, modified, deleted, size, etag FROM nodes n +SELECT resid AS id, found, r.owner, permission, r.parent, name, dir, created, modified, deleted, size, etag FROM nodes n JOIN resources r ON r.id = n.resid WHERE n.id = $1::uuid @@ -140,7 +143,6 @@ WHERE n.id = $1::uuid type ResourceByIDParams struct { Root uuid.UUID ResourceID uuid.UUID - Permission int32 UserID int32 } @@ -149,7 +151,6 @@ type ResourceByIDRow struct { Found bool Owner int32 Permission int32 - ID_2 uuid.UUID Parent *uuid.UUID Name string Dir bool @@ -161,19 +162,13 @@ type ResourceByIDRow struct { } func (q *Queries) ResourceByID(ctx context.Context, arg ResourceByIDParams) (ResourceByIDRow, error) { - row := q.db.QueryRow(ctx, resourceByID, - arg.Root, - arg.ResourceID, - arg.Permission, - arg.UserID, - ) + row := q.db.QueryRow(ctx, resourceByID, arg.Root, arg.ResourceID, arg.UserID) var i ResourceByIDRow err := row.Scan( &i.ID, &i.Found, &i.Owner, &i.Permission, - &i.ID_2, &i.Parent, &i.Name, &i.Dir, @@ -259,7 +254,7 @@ func (q *Queries) ResourceByPath(ctx context.Context, arg ResourceByPathParams) } const rootResource = `-- name: RootResource :one -SELECT r.name, r.owner, r.modified, +SELECT r.id, r.name, r.owner, r.modified, CASE WHEN r.owner = $1::int THEN 127 WHEN p.permission IS NOT NULL THEN p.permission @@ -273,6 +268,7 @@ SELECT r.name, r.owner, r.modified, ` type RootResourceRow struct { + ID uuid.UUID Name string Owner int32 Modified pgtype.Timestamp @@ -283,6 +279,7 @@ func (q *Queries) RootResource(ctx context.Context, userID int32) (RootResourceR row := q.db.QueryRow(ctx, rootResource, userID) var i RootResourceRow err := row.Scan( + &i.ID, &i.Name, &i.Owner, &i.Modified, diff --git a/server/internal/db/resources.sql.go b/server/internal/db/resources.sql.go index e8b79917..84ac6ab0 100644 --- a/server/internal/db/resources.sql.go +++ b/server/internal/db/resources.sql.go @@ -105,28 +105,6 @@ func (q *Queries) HardDeleteRecursive(ctx context.Context, id uuid.UUID) ([]uuid return items, nil } -const resourceById = `-- name: ResourceById :one -SELECT id, owner, parent, name, dir, created, modified, deleted, size, etag from resources WHERE id = $1 -` - -func (q *Queries) ResourceById(ctx context.Context, id uuid.UUID) (Resource, error) { - row := q.db.QueryRow(ctx, resourceById, id) - var i Resource - err := row.Scan( - &i.ID, - &i.Owner, - &i.Parent, - &i.Name, - &i.Dir, - &i.Created, - &i.Modified, - &i.Deleted, - &i.Size, - &i.Etag, - ) - return i, err -} - const updateResourceContents = `-- name: UpdateResourceContents :exec UPDATE resources SET diff --git a/server/internal/db/users.sql.go b/server/internal/db/users.sql.go index 62f6133f..bd784305 100644 --- a/server/internal/db/users.sql.go +++ b/server/internal/db/users.sql.go @@ -13,20 +13,28 @@ import ( const createUser = `-- name: CreateUser :one INSERT INTO users( - username, created, modified, display_name, password_hash + username, created, modified, display_name, password_hash, root, home ) VALUES ( - $1, NOW(), NOW(), $2, $3 -) RETURNING id, username, created, modified, display_name, password_hash, deleted, home + $1, NOW(), NOW(), $2, $3, $4, $5 +) RETURNING id, username, created, modified, display_name, password_hash, deleted, root, home ` type CreateUserParams struct { Username string DisplayName string PasswordHash string + Root uuid.UUID + Home uuid.UUID } func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) { - row := q.db.QueryRow(ctx, createUser, arg.Username, arg.DisplayName, arg.PasswordHash) + row := q.db.QueryRow(ctx, createUser, + arg.Username, + arg.DisplayName, + arg.PasswordHash, + arg.Root, + arg.Home, + ) var i User err := row.Scan( &i.ID, @@ -36,13 +44,14 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, e &i.DisplayName, &i.PasswordHash, &i.Deleted, + &i.Root, &i.Home, ) return i, err } const listUsers = `-- name: ListUsers :many -SELECT id, username, created, modified, display_name, password_hash, deleted, home from users WHERE deleted IS NULL +SELECT id, username, created, modified, display_name, password_hash, deleted, root, home from users WHERE deleted IS NULL ` func (q *Queries) ListUsers(ctx context.Context) ([]User, error) { @@ -62,6 +71,7 @@ func (q *Queries) ListUsers(ctx context.Context) ([]User, error) { &i.DisplayName, &i.PasswordHash, &i.Deleted, + &i.Root, &i.Home, ); err != nil { return nil, err @@ -74,6 +84,26 @@ func (q *Queries) ListUsers(ctx context.Context) ([]User, error) { return items, nil } +const updateUserDirs = `-- name: UpdateUserDirs :exec +UPDATE users +SET + root = $1, + home = $2, + modified = NOW() +WHERE id = $3 +` + +type UpdateUserDirsParams struct { + Root uuid.UUID + Home uuid.UUID + ID int32 +} + +func (q *Queries) UpdateUserDirs(ctx context.Context, arg UpdateUserDirsParams) error { + _, err := q.db.Exec(ctx, updateUserDirs, arg.Root, arg.Home, arg.ID) + return err +} + const updateUserDisplayName = `-- name: UpdateUserDisplayName :exec UPDATE users SET @@ -92,24 +122,6 @@ func (q *Queries) UpdateUserDisplayName(ctx context.Context, arg UpdateUserDispl return err } -const updateUserHome = `-- name: UpdateUserHome :exec -UPDATE users -SET - home = $1, - modified = NOW() -WHERE id = $2 -` - -type UpdateUserHomeParams struct { - Home *uuid.UUID - ID int32 -} - -func (q *Queries) UpdateUserHome(ctx context.Context, arg UpdateUserHomeParams) error { - _, err := q.db.Exec(ctx, updateUserHome, arg.Home, arg.ID) - return err -} - const updateUserPasswordHash = `-- name: UpdateUserPasswordHash :exec UPDATE users SET @@ -129,7 +141,7 @@ func (q *Queries) UpdateUserPasswordHash(ctx context.Context, arg UpdateUserPass } const userByID = `-- name: UserByID :one -SELECT id, username, created, modified, display_name, password_hash, deleted, home from users WHERE id = $1 +SELECT id, username, created, modified, display_name, password_hash, deleted, root, home from users WHERE id = $1 ` func (q *Queries) UserByID(ctx context.Context, id int32) (User, error) { @@ -143,13 +155,14 @@ func (q *Queries) UserByID(ctx context.Context, id int32) (User, error) { &i.DisplayName, &i.PasswordHash, &i.Deleted, + &i.Root, &i.Home, ) return i, err } const userByUsername = `-- name: UserByUsername :one -SELECT id, username, created, modified, display_name, password_hash, deleted, home from users WHERE username = $1 +SELECT id, username, created, modified, display_name, password_hash, deleted, root, home from users WHERE username = $1 ` func (q *Queries) UserByUsername(ctx context.Context, username string) (User, error) { @@ -163,6 +176,7 @@ func (q *Queries) UserByUsername(ctx context.Context, username string) (User, er &i.DisplayName, &i.PasswordHash, &i.Deleted, + &i.Root, &i.Home, ) return i, err diff --git a/server/internal/webdav/auth_basic.go b/server/internal/webdav/auth_basic.go index 6eadd56e..d1ce2a69 100644 --- a/server/internal/webdav/auth_basic.go +++ b/server/internal/webdav/auth_basic.go @@ -1,6 +1,7 @@ package webdav import ( + "errors" "net/http" "github.com/gin-gonic/gin" @@ -10,18 +11,18 @@ import ( const keyFileSystem = "filesystem" -func createBasicAuthHandler(app *app.App) func(c *gin.Context) { +func createBasicAuthHandler(a *app.App) func(c *gin.Context) { return func(c *gin.Context) { - var userID int32 - if username, pass, ok := c.Request.BasicAuth(); ok { - if res, err := app.VerifyUserPassword(c.Request.Context(), username, pass); err == nil { - userID = res + if username, pass, ok := c.Request.BasicAuth(); !ok { + } else if user, err := a.VerifyUserPassword(c.Request.Context(), username, pass); err != nil { + if errors.Is(err, app.ErrCredentialsInvalid) { + c.Header("WWW-Authenticate", "Basic realm=\"Phylum WebDAV\"") + c.AbortWithStatus(http.StatusUnauthorized) + } else { + logrus.Warn(err) + c.AbortWithStatus(http.StatusInternalServerError) } - } - if userID == 0 { - c.Header("WWW-Authenticate", "Basic realm=\"Phylum WebDAV\"") - c.AbortWithStatus(http.StatusUnauthorized) - } else if fs, err := app.OpenFileSystem(c.Request.Context(), userID); err != nil { + } else if fs, err := a.OpenFileSystem(c.Request.Context(), user); err != nil { logrus.Warn(err) c.AbortWithStatus(http.StatusInternalServerError) } else { diff --git a/server/sql/queries/permissions.sql b/server/sql/queries/permissions.sql index 12112a16..8c5c4935 100644 --- a/server/sql/queries/permissions.sql +++ b/server/sql/queries/permissions.sql @@ -51,17 +51,20 @@ WITH RECURSIVE nodes(resid, id, parent, found, permission) AS ( WHEN r.id = @root::uuid THEN true ELSE false END, - @permission::int + CASE + WHEN r.owner = @user_id::int THEN 127 + WHEN p.permission IS NOT NULL THEN p.permission + ELSE 0 + END FROM resources r LEFT JOIN permissions p - on r.id = p.resource_id + ON p.resource_id = r.id AND p.user_id = @user_id::int WHERE r.id = @resource_id::uuid UNION ALL SELECT n.resid, r.id, r.parent, CASE - WHEN r.id = @root::uuid - THEN true + WHEN r.id = @root::uuid THEN true ELSE n.found END, CASE @@ -77,13 +80,13 @@ WITH RECURSIVE nodes(resid, id, parent, found, permission) AS ( ON r.id = p.resource_id AND p.user_id = @user_id::int WHERE n.parent IS NOT NULL ) -SELECT resid AS id, found, r.owner, permission, r.id, r.parent, name, dir, created, modified, deleted, size, etag FROM nodes n +SELECT resid AS id, found, r.owner, permission, r.parent, name, dir, created, modified, deleted, size, etag FROM nodes n JOIN resources r ON r.id = n.resid WHERE n.id = @root::uuid; -- name: RootResource :one -SELECT r.name, r.owner, r.modified, +SELECT r.id, r.name, r.owner, r.modified, CASE WHEN r.owner = @user_id::int THEN 127 WHEN p.permission IS NOT NULL THEN p.permission diff --git a/server/sql/queries/resources.sql b/server/sql/queries/resources.sql index e776e9dd..0dbfbd48 100644 --- a/server/sql/queries/resources.sql +++ b/server/sql/queries/resources.sql @@ -1,6 +1,3 @@ --- name: ResourceById :one -SELECT * from resources WHERE id = $1; - -- name: CreateResource :one INSERT INTO resources( id, owner, parent, name, dir, created, modified diff --git a/server/sql/queries/users.sql b/server/sql/queries/users.sql index 138163a4..6955e58a 100644 --- a/server/sql/queries/users.sql +++ b/server/sql/queries/users.sql @@ -1,8 +1,8 @@ -- name: CreateUser :one INSERT INTO users( - username, created, modified, display_name, password_hash + username, created, modified, display_name, password_hash, root, home ) VALUES ( - $1, NOW(), NOW(), $2, $3 + $1, NOW(), NOW(), $2, $3, $4, $5 ) RETURNING *; -- name: UserByUsername :one @@ -28,9 +28,11 @@ SET modified = NOW() WHERE id = $2; --- name: UpdateUserHome :exec + +-- name: UpdateUserDirs :exec UPDATE users SET - home = $1, + root = $1, + home = $2, modified = NOW() -WHERE id = $2; \ No newline at end of file +WHERE id = $3; \ No newline at end of file