Version 0.2.0
Changes: - forbid to overwrite secrets of other users - add some logs of success - add validation on code (length 50) - use same limiter for POST requests
This commit is contained in:
@@ -7,6 +7,7 @@ CREATE TABLE IF NOT EXISTS users (
|
||||
CREATE TABLE IF NOT EXISTS secrets (
|
||||
code TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
user TEXT REFERENCES users(login) NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
)
|
||||
|
||||
var ErrNotFound = errors.New("secret not found")
|
||||
var ErrForbidden = errors.New("code owned by another user")
|
||||
|
||||
type SecretsRepository struct {
|
||||
db *sql.DB
|
||||
@@ -29,14 +30,35 @@ func (r *SecretsRepository) Get(ctx context.Context, code string) (string, error
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func (r *SecretsRepository) Set(ctx context.Context, code, value string) error {
|
||||
_, err := r.db.ExecContext(ctx, `
|
||||
INSERT INTO secrets (code, value, updated_at)
|
||||
VALUES (?, ?, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(code) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP
|
||||
`, code, value)
|
||||
// Set stores value under code, owned by user. If code already exists and is
|
||||
// owned by a different user, it returns ErrForbidden and leaves the stored
|
||||
// value untouched.
|
||||
func (r *SecretsRepository) Set(ctx context.Context, code, value, user string) error {
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not begin transaction: %w", err)
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
var owner string
|
||||
err = tx.QueryRowContext(ctx, "SELECT user FROM secrets WHERE code = ?", code).Scan(&owner)
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return fmt.Errorf("could not check secret owner: %w", err)
|
||||
}
|
||||
if !errors.Is(err, sql.ErrNoRows) && owner != user {
|
||||
return ErrForbidden
|
||||
}
|
||||
|
||||
if _, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO secrets (code, value, user, created_at, updated_at)
|
||||
VALUES (?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||
ON CONFLICT(code) DO UPDATE SET value = excluded.value, updated_at = CURRENT_TIMESTAMP
|
||||
`, code, value, user); err != nil {
|
||||
return fmt.Errorf("could not set secret: %w", err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf("could not commit transaction: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
53
internal/db/secrets_test.go
Normal file
53
internal/db/secrets_test.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package db_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/kanootoko/codevault/internal/db"
|
||||
)
|
||||
|
||||
func openTestDB(t *testing.T) *sql.DB {
|
||||
t.Helper()
|
||||
sqlDB, err := db.Open(filepath.Join(t.TempDir(), "test.db"))
|
||||
if err != nil {
|
||||
t.Fatalf("could not open test db: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = sqlDB.Close() })
|
||||
return sqlDB
|
||||
}
|
||||
|
||||
func TestSecretsRepository_SetOwnership(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
secrets := db.NewSecretsRepository(openTestDB(t))
|
||||
|
||||
if err := secrets.Set(ctx, "code1", "value1", "alice"); err != nil {
|
||||
t.Fatalf("alice's initial set failed: %v", err)
|
||||
}
|
||||
|
||||
if err := secrets.Set(ctx, "code1", "value2", "alice"); err != nil {
|
||||
t.Fatalf("alice's overwrite failed: %v", err)
|
||||
}
|
||||
value, err := secrets.Get(ctx, "code1")
|
||||
if err != nil {
|
||||
t.Fatalf("get failed: %v", err)
|
||||
}
|
||||
if value != "value2" {
|
||||
t.Fatalf("expected value2, got %q", value)
|
||||
}
|
||||
|
||||
err = secrets.Set(ctx, "code1", "value3", "bob")
|
||||
if !errors.Is(err, db.ErrForbidden) {
|
||||
t.Fatalf("expected ErrForbidden for bob, got %v", err)
|
||||
}
|
||||
value, err = secrets.Get(ctx, "code1")
|
||||
if err != nil {
|
||||
t.Fatalf("get failed: %v", err)
|
||||
}
|
||||
if value != "value2" {
|
||||
t.Fatalf("expected value to remain value2 after forbidden write, got %q", value)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user