forked from baron/baron-sso
- Simplified RBAC system to two roles: super_admin and user. - Removed tenant_admin and rp_admin roles across backend and frontend. - Removed Dev Role Switcher feature from adminfront. - Updated all handlers, middlewares, and navigation to reflect the new role model. - Fixed backend build errors and updated tests.
267 lines
7.1 KiB
Go
267 lines
7.1 KiB
Go
package middleware
|
|
|
|
import (
|
|
"baron-sso-backend/internal/domain"
|
|
"baron-sso-backend/internal/service"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/mock"
|
|
)
|
|
|
|
// MockAuthProvider is a mock for AuthProvider interface
|
|
type MockAuthProvider struct {
|
|
mock.Mock
|
|
}
|
|
|
|
func (m *MockAuthProvider) GetEnrichedProfile(c *fiber.Ctx) (*domain.UserProfileResponse, error) {
|
|
args := m.Called(c)
|
|
if args.Get(0) == nil {
|
|
return nil, args.Error(1)
|
|
}
|
|
return args.Get(0).(*domain.UserProfileResponse), args.Error(1)
|
|
}
|
|
|
|
// MockKetoService is a mock for KetoService interface
|
|
type MockKetoService struct {
|
|
mock.Mock
|
|
}
|
|
|
|
func (m *MockKetoService) CheckPermission(ctx context.Context, subject, namespace, object, relation string) (bool, error) {
|
|
args := m.Called(ctx, subject, namespace, object, relation)
|
|
return args.Bool(0), args.Error(1)
|
|
}
|
|
|
|
func (m *MockKetoService) CreateRelation(ctx context.Context, namespace, object, relation, subject string) error {
|
|
args := m.Called(ctx, namespace, object, relation, subject)
|
|
return args.Error(0)
|
|
}
|
|
|
|
func (m *MockKetoService) DeleteRelation(ctx context.Context, namespace, object, relation, subject string) error {
|
|
args := m.Called(ctx, namespace, object, relation, subject)
|
|
return args.Error(0)
|
|
}
|
|
|
|
func (m *MockKetoService) ListRelations(ctx context.Context, namespace, object, relation, subject string) ([]service.RelationTuple, error) {
|
|
args := m.Called(ctx, namespace, object, relation, subject)
|
|
if args.Get(0) == nil {
|
|
return nil, args.Error(1)
|
|
}
|
|
return args.Get(0).([]service.RelationTuple), args.Error(1)
|
|
}
|
|
|
|
func (m *MockKetoService) ListObjects(ctx context.Context, namespace, relation, subject string) ([]string, error) {
|
|
args := m.Called(ctx, namespace, relation, subject)
|
|
if args.Get(0) == nil {
|
|
return nil, args.Error(1)
|
|
}
|
|
return args.Get(0).([]string), args.Error(1)
|
|
}
|
|
|
|
func TestRequireRole_Success(t *testing.T) {
|
|
app := fiber.New()
|
|
mockAuth := new(MockAuthProvider)
|
|
config := RBACConfig{
|
|
AllowedRoles: []string{domain.RoleSuperAdmin},
|
|
AuthHandler: mockAuth,
|
|
}
|
|
|
|
mockAuth.On("GetEnrichedProfile", mock.Anything).Return(&domain.UserProfileResponse{
|
|
ID: "user1",
|
|
Role: domain.RoleSuperAdmin,
|
|
}, nil)
|
|
|
|
app.Get("/test", RequireRole(config), func(c *fiber.Ctx) error {
|
|
return c.SendString("ok")
|
|
})
|
|
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
resp, _ := app.Test(req)
|
|
|
|
assert.Equal(t, 200, resp.StatusCode)
|
|
}
|
|
|
|
func TestRequireRole_SetsUserIDForAuditContext(t *testing.T) {
|
|
app := fiber.New()
|
|
mockAuth := new(MockAuthProvider)
|
|
config := RBACConfig{
|
|
AllowedRoles: []string{domain.RoleSuperAdmin},
|
|
AuthHandler: mockAuth,
|
|
}
|
|
|
|
mockAuth.On("GetEnrichedProfile", mock.Anything).Return(&domain.UserProfileResponse{
|
|
ID: "user1",
|
|
Role: domain.RoleSuperAdmin,
|
|
}, nil)
|
|
|
|
app.Get("/test", RequireRole(config), func(c *fiber.Ctx) error {
|
|
return c.JSON(fiber.Map{
|
|
"user_id": c.Locals("user_id"),
|
|
})
|
|
})
|
|
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
resp, _ := app.Test(req)
|
|
|
|
assert.Equal(t, 200, resp.StatusCode)
|
|
|
|
var body map[string]string
|
|
assert.NoError(t, readJSON(resp, &body))
|
|
assert.Equal(t, "user1", body["user_id"])
|
|
}
|
|
|
|
func TestRequireRole_PreservesExistingUserID(t *testing.T) {
|
|
app := fiber.New()
|
|
mockAuth := new(MockAuthProvider)
|
|
config := RBACConfig{
|
|
AllowedRoles: []string{domain.RoleSuperAdmin},
|
|
AuthHandler: mockAuth,
|
|
}
|
|
|
|
mockAuth.On("GetEnrichedProfile", mock.Anything).Return(&domain.UserProfileResponse{
|
|
ID: "profile-user",
|
|
Role: domain.RoleSuperAdmin,
|
|
}, nil)
|
|
|
|
app.Use(func(c *fiber.Ctx) error {
|
|
c.Locals("user_id", "existing-user")
|
|
return c.Next()
|
|
})
|
|
app.Get("/test", RequireRole(config), func(c *fiber.Ctx) error {
|
|
return c.JSON(fiber.Map{
|
|
"user_id": c.Locals("user_id"),
|
|
})
|
|
})
|
|
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
resp, _ := app.Test(req)
|
|
|
|
assert.Equal(t, 200, resp.StatusCode)
|
|
|
|
var body map[string]string
|
|
assert.NoError(t, readJSON(resp, &body))
|
|
assert.Equal(t, "existing-user", body["user_id"])
|
|
}
|
|
|
|
func TestRequireRole_Forbidden(t *testing.T) {
|
|
app := fiber.New()
|
|
mockAuth := new(MockAuthProvider)
|
|
config := RBACConfig{
|
|
AllowedRoles: []string{domain.RoleSuperAdmin},
|
|
AuthHandler: mockAuth,
|
|
}
|
|
|
|
mockAuth.On("GetEnrichedProfile", mock.Anything).Return(&domain.UserProfileResponse{
|
|
ID: "user1",
|
|
Role: "user",
|
|
}, nil)
|
|
|
|
app.Get("/test", RequireRole(config), func(c *fiber.Ctx) error {
|
|
return c.SendString("ok")
|
|
})
|
|
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
resp, _ := app.Test(req)
|
|
|
|
assert.Equal(t, 403, resp.StatusCode)
|
|
}
|
|
|
|
func TestRequireKetoPermission_Success(t *testing.T) {
|
|
app := fiber.New()
|
|
mockAuth := new(MockAuthProvider)
|
|
mockKeto := new(MockKetoService)
|
|
config := RBACConfig{
|
|
AuthHandler: mockAuth,
|
|
KetoService: mockKeto,
|
|
}
|
|
|
|
profile := &domain.UserProfileResponse{ID: "user1", Role: "user"}
|
|
mockAuth.On("GetEnrichedProfile", mock.Anything).Return(profile, nil)
|
|
mockKeto.On("CheckPermission", mock.Anything, "User:user1", "tenants", "tenant1", "read").Return(true, nil)
|
|
|
|
app.Get("/tenants/:id", RequireKetoPermission(config, "tenants", "read"), func(c *fiber.Ctx) error {
|
|
return c.SendString("ok")
|
|
})
|
|
|
|
req := httptest.NewRequest("GET", "/tenants/tenant1", nil)
|
|
resp, _ := app.Test(req)
|
|
|
|
assert.Equal(t, 200, resp.StatusCode)
|
|
}
|
|
|
|
func TestRequireTenantMatch_SuperAdmin(t *testing.T) {
|
|
app := fiber.New()
|
|
mockAuth := new(MockAuthProvider)
|
|
config := RBACConfig{
|
|
AuthHandler: mockAuth,
|
|
}
|
|
|
|
mockAuth.On("GetEnrichedProfile", mock.Anything).Return(&domain.UserProfileResponse{
|
|
ID: "admin1",
|
|
Role: domain.RoleSuperAdmin,
|
|
}, nil)
|
|
|
|
app.Get("/tenants/:tenantId/data", RequireTenantMatch(config), func(c *fiber.Ctx) error {
|
|
return c.SendString("ok")
|
|
})
|
|
|
|
req := httptest.NewRequest("GET", "/tenants/any-tenant/data", nil)
|
|
resp, _ := app.Test(req)
|
|
|
|
assert.Equal(t, 200, resp.StatusCode)
|
|
}
|
|
|
|
func TestRequireTenantMatch_Forbidden(t *testing.T) {
|
|
app := fiber.New()
|
|
mockAuth := new(MockAuthProvider)
|
|
config := RBACConfig{
|
|
AuthHandler: mockAuth,
|
|
}
|
|
|
|
tenant1 := "tenant1"
|
|
mockAuth.On("GetEnrichedProfile", mock.Anything).Return(&domain.UserProfileResponse{
|
|
ID: "user1",
|
|
Role: "user", // Formerly tenant_admin, now mapped to user which is forbidden here for non-superadmin
|
|
TenantID: &tenant1,
|
|
}, nil)
|
|
|
|
app.Get("/tenants/:tenantId/data", RequireTenantMatch(config), func(c *fiber.Ctx) error {
|
|
return c.SendString("ok")
|
|
})
|
|
|
|
req := httptest.NewRequest("GET", "/tenants/tenant2/data", nil)
|
|
resp, _ := app.Test(req)
|
|
|
|
assert.Equal(t, 403, resp.StatusCode)
|
|
}
|
|
|
|
func TestRequireRole_Unauthorized(t *testing.T) {
|
|
app := fiber.New()
|
|
mockAuth := new(MockAuthProvider)
|
|
config := RBACConfig{
|
|
AuthHandler: mockAuth,
|
|
}
|
|
|
|
mockAuth.On("GetEnrichedProfile", mock.Anything).Return(nil, errors.New("unauthorized"))
|
|
|
|
app.Get("/test", RequireRole(config), func(c *fiber.Ctx) error {
|
|
return c.SendString("ok")
|
|
})
|
|
|
|
req := httptest.NewRequest("GET", "/test", nil)
|
|
resp, _ := app.Test(req)
|
|
|
|
assert.Equal(t, 401, resp.StatusCode)
|
|
}
|
|
|
|
func readJSON(resp *http.Response, target any) error {
|
|
defer resp.Body.Close()
|
|
return json.NewDecoder(resp.Body).Decode(target)
|
|
}
|