Files
oslstats/internal/db/migrations/20260202231414_add_rbac_system.go
2026-02-14 19:48:59 +11:00

254 lines
6.1 KiB
Go

package migrations
import (
"context"
"time"
"git.haelnorr.com/h/oslstats/internal/db"
"github.com/pkg/errors"
"github.com/uptrace/bun"
)
func init() {
Migrations.MustRegister(
// UP migration
func(ctx context.Context, conn *bun.DB) error {
conn.RegisterModel((*db.RolePermission)(nil), (*db.UserRole)(nil))
// Create permissions table
_, err := conn.NewCreateTable().
Model((*db.Role)(nil)).
Exec(ctx)
if err != nil {
return err
}
// Create permissions table
_, err = conn.NewCreateTable().
Model((*db.Permission)(nil)).
Exec(ctx)
if err != nil {
return err
}
// Create indexes for permissions
_, err = conn.NewCreateIndex().
Model((*db.Permission)(nil)).
Index("idx_permissions_resource").
Column("resource").
Exec(ctx)
if err != nil {
return err
}
_, err = conn.NewCreateIndex().
Model((*db.Permission)(nil)).
Index("idx_permissions_action").
Column("action").
Exec(ctx)
if err != nil {
return err
}
_, err = conn.NewCreateTable().
Model((*db.RolePermission)(nil)).
Exec(ctx)
if err != nil {
return err
}
_, err = conn.ExecContext(ctx, `
CREATE INDEX idx_role_permissions_role ON role_permissions(role_id)
`)
if err != nil {
return err
}
_, err = conn.ExecContext(ctx, `
CREATE INDEX idx_role_permissions_permission ON role_permissions(permission_id)
`)
if err != nil {
return err
}
// Create user_roles table
_, err = conn.NewCreateTable().
Model((*db.UserRole)(nil)).
Exec(ctx)
if err != nil {
return err
}
// Create indexes for user_roles
_, err = conn.NewCreateIndex().
Model((*db.UserRole)(nil)).
Index("idx_user_roles_user").
Column("user_id").
Exec(ctx)
if err != nil {
return err
}
_, err = conn.NewCreateIndex().
Model((*db.UserRole)(nil)).
Index("idx_user_roles_role").
Column("role_id").
Exec(ctx)
if err != nil {
return err
}
// Create audit_log table
_, err = conn.NewCreateTable().
Model((*db.AuditLog)(nil)).
Exec(ctx)
if err != nil {
return err
}
// Create indexes for audit_log
_, err = conn.NewCreateIndex().
Model((*db.AuditLog)(nil)).
Index("idx_audit_log_user").
Column("user_id").
Exec(ctx)
if err != nil {
return err
}
_, err = conn.NewCreateIndex().
Model((*db.AuditLog)(nil)).
Index("idx_audit_log_action").
Column("action").
Exec(ctx)
if err != nil {
return err
}
_, err = conn.NewCreateIndex().
Model((*db.AuditLog)(nil)).
Index("idx_audit_log_resource").
Column("resource_type", "resource_id").
Exec(ctx)
if err != nil {
return err
}
_, err = conn.NewCreateIndex().
Model((*db.AuditLog)(nil)).
Index("idx_audit_log_created").
Column("created_at").
Exec(ctx)
if err != nil {
return err
}
err = seedSystemRBAC(ctx, conn)
if err != nil {
return err
}
return nil
},
// DOWN migration
func(ctx context.Context, dbConn *bun.DB) error {
// Drop tables in reverse order
// Use raw SQL to avoid relationship resolution issues
tables := []string{
"audit_log",
"user_roles",
"role_permissions",
"permissions",
"roles",
}
for _, table := range tables {
_, err := dbConn.ExecContext(ctx, "DROP TABLE IF EXISTS "+table+" CASCADE")
if err != nil {
return err
}
}
return nil
},
)
}
func seedSystemRBAC(ctx context.Context, conn *bun.DB) error {
// Seed system roles
now := time.Now().Unix()
adminRole := &db.Role{
Name: "admin",
DisplayName: "Administrator",
Description: "Full system access with all permissions",
IsSystem: true,
CreatedAt: now,
}
_, err := conn.NewInsert().
Model(adminRole).
Returning("id").
Exec(ctx)
if err != nil {
return errors.Wrap(err, "dbConn.NewInsert")
}
userRole := &db.Role{
Name: "user",
DisplayName: "User",
Description: "Standard user with basic permissions",
IsSystem: true,
CreatedAt: now,
}
_, err = conn.NewInsert().
Model(userRole).
Exec(ctx)
if err != nil {
return errors.Wrap(err, "dbConn.NewInsert")
}
// Seed system permissions
permissionsData := []*db.Permission{
{Name: "*", DisplayName: "Wildcard (All Permissions)", Description: "Grants access to all permissions, past, present, and future", Resource: "*", Action: "*", IsSystem: true, CreatedAt: now},
{Name: "seasons.create", DisplayName: "Create Seasons", Description: "Create new seasons", Resource: "seasons", Action: "create", IsSystem: true, CreatedAt: now},
{Name: "seasons.update", DisplayName: "Update Seasons", Description: "Update existing seasons", Resource: "seasons", Action: "update", IsSystem: true, CreatedAt: now},
{Name: "seasons.delete", DisplayName: "Delete Seasons", Description: "Delete seasons", Resource: "seasons", Action: "delete", IsSystem: true, CreatedAt: now},
{Name: "users.update", DisplayName: "Update Users", Description: "Update user information", Resource: "users", Action: "update", IsSystem: true, CreatedAt: now},
{Name: "users.ban", DisplayName: "Ban Users", Description: "Ban users from the system", Resource: "users", Action: "ban", IsSystem: true, CreatedAt: now},
{Name: "users.manage_roles", DisplayName: "Manage User Roles", Description: "Assign and revoke user roles", Resource: "users", Action: "manage_roles", IsSystem: true, CreatedAt: now},
}
_, err = conn.NewInsert().
Model(&permissionsData).
Exec(ctx)
if err != nil {
return errors.Wrap(err, "dbConn.NewInsert")
}
// Grant wildcard permission to admin role using Bun
// First, get the IDs
var wildcardPerm db.Permission
err = conn.NewSelect().
Model(&wildcardPerm).
Where("name = ?", "*").
Scan(ctx)
if err != nil {
return err
}
// Insert role_permission mapping
adminRolePerms := &db.RolePermission{
RoleID: adminRole.ID,
PermissionID: wildcardPerm.ID,
}
_, err = conn.NewInsert().
Model(adminRolePerms).
On("CONFLICT (role_id, permission_id) DO NOTHING").
Exec(ctx)
if err != nil {
return errors.Wrap(err, "dbConn.NewInsert")
}
return nil
}