Initial commit: breakpilot-core - Shared Infrastructure
Docker Compose with 24+ services: - PostgreSQL (PostGIS), Valkey, MinIO, Qdrant - Vault (PKI/TLS), Nginx (Reverse Proxy) - Backend Core API, Consent Service, Billing Service - RAG Service, Embedding Service - Gitea, Woodpecker CI/CD - Night Scheduler, Health Aggregator - Jitsi (Web/XMPP/JVB/Jicofo), Mailpit Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
403
consent-service/internal/session/rbac_middleware.go
Normal file
403
consent-service/internal/session/rbac_middleware.go
Normal file
@@ -0,0 +1,403 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Employee permissions
|
||||
var EmployeePermissions = []string{
|
||||
"grades:read", "grades:write",
|
||||
"attendance:read", "attendance:write",
|
||||
"students:read", "students:write",
|
||||
"reports:generate", "consent:admin",
|
||||
"corrections:read", "corrections:write",
|
||||
"classes:read", "classes:write",
|
||||
"timetable:read", "timetable:write",
|
||||
"substitutions:read", "substitutions:write",
|
||||
"parent_communication:read", "parent_communication:write",
|
||||
}
|
||||
|
||||
// Customer permissions
|
||||
var CustomerPermissions = []string{
|
||||
"own_data:read", "own_grades:read", "own_attendance:read",
|
||||
"consent:manage",
|
||||
"meetings:join",
|
||||
"messages:read", "messages:write",
|
||||
"children:read", "children:grades:read", "children:attendance:read",
|
||||
}
|
||||
|
||||
// Admin permissions
|
||||
var AdminPermissions = []string{
|
||||
"users:read", "users:write", "users:manage",
|
||||
"rbac:read", "rbac:write",
|
||||
"audit:read",
|
||||
"settings:read", "settings:write",
|
||||
"dsr:read", "dsr:process",
|
||||
}
|
||||
|
||||
// Employee roles
|
||||
var EmployeeRoles = map[string]bool{
|
||||
"admin": true,
|
||||
"schul_admin": true,
|
||||
"schulleitung": true,
|
||||
"pruefungsvorsitz": true,
|
||||
"klassenlehrer": true,
|
||||
"fachlehrer": true,
|
||||
"sekretariat": true,
|
||||
"erstkorrektor": true,
|
||||
"zweitkorrektor": true,
|
||||
"drittkorrektor": true,
|
||||
"teacher_assistant": true,
|
||||
"teacher": true,
|
||||
"lehrer": true,
|
||||
"data_protection_officer": true,
|
||||
}
|
||||
|
||||
// Customer roles
|
||||
var CustomerRoles = map[string]bool{
|
||||
"parent": true,
|
||||
"student": true,
|
||||
"user": true,
|
||||
"guardian": true,
|
||||
}
|
||||
|
||||
// RequireEmployee requires the user to be an employee
|
||||
func RequireEmployee() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
session := GetSession(c)
|
||||
if session == nil {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "unauthorized",
|
||||
"message": "Authentication required",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if !session.IsEmployee() {
|
||||
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
|
||||
"error": "forbidden",
|
||||
"message": "Employee access required",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// RequireCustomer requires the user to be a customer
|
||||
func RequireCustomer() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
session := GetSession(c)
|
||||
if session == nil {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "unauthorized",
|
||||
"message": "Authentication required",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if !session.IsCustomer() {
|
||||
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
|
||||
"error": "forbidden",
|
||||
"message": "Customer access required",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// RequireUserType requires a specific user type
|
||||
func RequireUserType(userType UserType) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
session := GetSession(c)
|
||||
if session == nil {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "unauthorized",
|
||||
"message": "Authentication required",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if session.UserType != userType {
|
||||
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
|
||||
"error": "forbidden",
|
||||
"message": "User type '" + string(userType) + "' required",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// RequirePermission requires a specific permission
|
||||
func RequirePermission(permission string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
session := GetSession(c)
|
||||
if session == nil {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "unauthorized",
|
||||
"message": "Authentication required",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if !session.HasPermission(permission) {
|
||||
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
|
||||
"error": "forbidden",
|
||||
"message": "Permission '" + permission + "' required",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// RequireAnyPermission requires at least one of the permissions
|
||||
func RequireAnyPermission(permissions ...string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
session := GetSession(c)
|
||||
if session == nil {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "unauthorized",
|
||||
"message": "Authentication required",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if !session.HasAnyPermission(permissions) {
|
||||
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
|
||||
"error": "forbidden",
|
||||
"message": "One of the required permissions is missing",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// RequireAllPermissions requires all specified permissions
|
||||
func RequireAllPermissions(permissions ...string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
session := GetSession(c)
|
||||
if session == nil {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "unauthorized",
|
||||
"message": "Authentication required",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if !session.HasAllPermissions(permissions) {
|
||||
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
|
||||
"error": "forbidden",
|
||||
"message": "Missing required permissions",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// RequireRole requires a specific role
|
||||
func RequireRole(role string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
session := GetSession(c)
|
||||
if session == nil {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "unauthorized",
|
||||
"message": "Authentication required",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if !session.HasRole(role) {
|
||||
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
|
||||
"error": "forbidden",
|
||||
"message": "Role '" + role + "' required",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// RequireAnyRole requires at least one of the roles
|
||||
func RequireAnyRole(roles ...string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
session := GetSession(c)
|
||||
if session == nil {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "unauthorized",
|
||||
"message": "Authentication required",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
for _, role := range roles {
|
||||
if session.HasRole(role) {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
|
||||
"error": "forbidden",
|
||||
"message": "One of the required roles is missing",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// RequireSameTenant ensures user can only access their tenant's data
|
||||
func RequireSameTenant(tenantIDParam string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
session := GetSession(c)
|
||||
if session == nil {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "unauthorized",
|
||||
"message": "Authentication required",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
requestTenantID := c.Param(tenantIDParam)
|
||||
if requestTenantID == "" {
|
||||
requestTenantID = c.Query(tenantIDParam)
|
||||
}
|
||||
|
||||
if requestTenantID != "" && session.TenantID != nil && *session.TenantID != requestTenantID {
|
||||
// Check if user is super admin
|
||||
if !session.HasRole("super_admin") {
|
||||
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
|
||||
"error": "forbidden",
|
||||
"message": "Access denied to this tenant",
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// DetermineUserType determines user type based on roles
|
||||
func DetermineUserType(roles []string) UserType {
|
||||
for _, role := range roles {
|
||||
if EmployeeRoles[role] {
|
||||
return UserTypeEmployee
|
||||
}
|
||||
}
|
||||
|
||||
for _, role := range roles {
|
||||
if CustomerRoles[role] {
|
||||
return UserTypeCustomer
|
||||
}
|
||||
}
|
||||
|
||||
return UserTypeCustomer
|
||||
}
|
||||
|
||||
// GetPermissionsForRoles returns permissions based on roles and user type
|
||||
func GetPermissionsForRoles(roles []string, userType UserType) []string {
|
||||
permSet := make(map[string]bool)
|
||||
|
||||
// Base permissions by user type
|
||||
if userType == UserTypeEmployee {
|
||||
for _, p := range EmployeePermissions {
|
||||
permSet[p] = true
|
||||
}
|
||||
} else {
|
||||
for _, p := range CustomerPermissions {
|
||||
permSet[p] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Admin permissions
|
||||
adminRoles := map[string]bool{
|
||||
"admin": true,
|
||||
"schul_admin": true,
|
||||
"super_admin": true,
|
||||
"data_protection_officer": true,
|
||||
}
|
||||
|
||||
for _, role := range roles {
|
||||
if adminRoles[role] {
|
||||
for _, p := range AdminPermissions {
|
||||
permSet[p] = true
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to slice
|
||||
permissions := make([]string, 0, len(permSet))
|
||||
for p := range permSet {
|
||||
permissions = append(permissions, p)
|
||||
}
|
||||
|
||||
return permissions
|
||||
}
|
||||
|
||||
// CheckResourceOwnership checks if user owns a resource or is admin
|
||||
func CheckResourceOwnership(session *Session, resourceUserID string, allowAdmin bool) bool {
|
||||
if session == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// User owns the resource
|
||||
if session.UserID == resourceUserID {
|
||||
return true
|
||||
}
|
||||
|
||||
// Admin can access all
|
||||
if allowAdmin && (session.HasRole("admin") || session.HasRole("super_admin")) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// IsSessionEmployee checks if current session belongs to an employee
|
||||
func IsSessionEmployee(c *gin.Context) bool {
|
||||
session := GetSession(c)
|
||||
if session == nil {
|
||||
return false
|
||||
}
|
||||
return session.IsEmployee()
|
||||
}
|
||||
|
||||
// IsSessionCustomer checks if current session belongs to a customer
|
||||
func IsSessionCustomer(c *gin.Context) bool {
|
||||
session := GetSession(c)
|
||||
if session == nil {
|
||||
return false
|
||||
}
|
||||
return session.IsCustomer()
|
||||
}
|
||||
|
||||
// HasSessionPermission checks if session has a permission
|
||||
func HasSessionPermission(c *gin.Context, permission string) bool {
|
||||
session := GetSession(c)
|
||||
if session == nil {
|
||||
return false
|
||||
}
|
||||
return session.HasPermission(permission)
|
||||
}
|
||||
|
||||
// HasSessionRole checks if session has a role
|
||||
func HasSessionRole(c *gin.Context, role string) bool {
|
||||
session := GetSession(c)
|
||||
if session == nil {
|
||||
return false
|
||||
}
|
||||
return session.HasRole(role)
|
||||
}
|
||||
196
consent-service/internal/session/session_middleware.go
Normal file
196
consent-service/internal/session/session_middleware.go
Normal file
@@ -0,0 +1,196 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
// SessionMiddleware extracts session from request and adds to context
|
||||
func SessionMiddleware(pgPool *pgxpool.Pool) gin.HandlerFunc {
|
||||
store := GetSessionStore(pgPool)
|
||||
|
||||
return func(c *gin.Context) {
|
||||
sessionID := extractSessionID(c)
|
||||
|
||||
if sessionID != "" {
|
||||
session, err := store.GetSession(c.Request.Context(), sessionID)
|
||||
if err == nil && session != nil {
|
||||
// Add session to context
|
||||
c.Set("session", session)
|
||||
c.Set("session_id", session.SessionID)
|
||||
c.Set("user_id", session.UserID)
|
||||
c.Set("email", session.Email)
|
||||
c.Set("user_type", string(session.UserType))
|
||||
c.Set("roles", session.Roles)
|
||||
c.Set("permissions", session.Permissions)
|
||||
if session.TenantID != nil {
|
||||
c.Set("tenant_id", *session.TenantID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// extractSessionID extracts session ID from request
|
||||
func extractSessionID(c *gin.Context) string {
|
||||
// Try Authorization header first
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if strings.HasPrefix(authHeader, "Bearer ") {
|
||||
return strings.TrimPrefix(authHeader, "Bearer ")
|
||||
}
|
||||
|
||||
// Try X-Session-ID header
|
||||
if sessionID := c.GetHeader("X-Session-ID"); sessionID != "" {
|
||||
return sessionID
|
||||
}
|
||||
|
||||
// Try cookie
|
||||
if cookie, err := c.Cookie("session_id"); err == nil {
|
||||
return cookie
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// RequireSession requires a valid session
|
||||
func RequireSession() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
session := GetSession(c)
|
||||
|
||||
if session == nil {
|
||||
// Check development bypass
|
||||
if os.Getenv("ENVIRONMENT") == "development" {
|
||||
// Set demo session
|
||||
demoSession := getDemoSession()
|
||||
c.Set("session", demoSession)
|
||||
c.Set("session_id", demoSession.SessionID)
|
||||
c.Set("user_id", demoSession.UserID)
|
||||
c.Set("email", demoSession.Email)
|
||||
c.Set("user_type", string(demoSession.UserType))
|
||||
c.Set("roles", demoSession.Roles)
|
||||
c.Set("permissions", demoSession.Permissions)
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "unauthorized",
|
||||
"message": "Authentication required",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// GetSession retrieves session from context
|
||||
func GetSession(c *gin.Context) *Session {
|
||||
if session, exists := c.Get("session"); exists {
|
||||
if s, ok := session.(*Session); ok {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSessionUserID retrieves user ID from session context
|
||||
func GetSessionUserID(c *gin.Context) (uuid.UUID, error) {
|
||||
userIDStr, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
return uuid.Nil, nil
|
||||
}
|
||||
|
||||
return uuid.Parse(userIDStr.(string))
|
||||
}
|
||||
|
||||
// GetSessionEmail retrieves email from session context
|
||||
func GetSessionEmail(c *gin.Context) string {
|
||||
email, exists := c.Get("email")
|
||||
if !exists {
|
||||
return ""
|
||||
}
|
||||
return email.(string)
|
||||
}
|
||||
|
||||
// GetSessionUserType retrieves user type from session context
|
||||
func GetSessionUserType(c *gin.Context) UserType {
|
||||
userType, exists := c.Get("user_type")
|
||||
if !exists {
|
||||
return UserTypeCustomer
|
||||
}
|
||||
return UserType(userType.(string))
|
||||
}
|
||||
|
||||
// GetSessionRoles retrieves roles from session context
|
||||
func GetSessionRoles(c *gin.Context) []string {
|
||||
roles, exists := c.Get("roles")
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
if r, ok := roles.([]string); ok {
|
||||
return r
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSessionPermissions retrieves permissions from session context
|
||||
func GetSessionPermissions(c *gin.Context) []string {
|
||||
perms, exists := c.Get("permissions")
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
if p, ok := perms.([]string); ok {
|
||||
return p
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSessionTenantID retrieves tenant ID from session context
|
||||
func GetSessionTenantID(c *gin.Context) *string {
|
||||
tenantID, exists := c.Get("tenant_id")
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
if t, ok := tenantID.(string); ok {
|
||||
return &t
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getDemoSession returns a demo session for development
|
||||
func getDemoSession() *Session {
|
||||
tenantID := "a0000000-0000-0000-0000-000000000001"
|
||||
ip := "127.0.0.1"
|
||||
ua := "Development"
|
||||
|
||||
return &Session{
|
||||
SessionID: "demo-session-id",
|
||||
UserID: "10000000-0000-0000-0000-000000000024",
|
||||
Email: "demo@breakpilot.app",
|
||||
UserType: UserTypeEmployee,
|
||||
Roles: []string{
|
||||
"admin", "schul_admin", "teacher",
|
||||
},
|
||||
Permissions: []string{
|
||||
"grades:read", "grades:write",
|
||||
"attendance:read", "attendance:write",
|
||||
"students:read", "students:write",
|
||||
"reports:generate", "consent:admin",
|
||||
"own_data:read", "users:manage",
|
||||
},
|
||||
TenantID: &tenantID,
|
||||
IPAddress: &ip,
|
||||
UserAgent: &ua,
|
||||
CreatedAt: time.Now().UTC(),
|
||||
LastActivityAt: time.Now().UTC(),
|
||||
}
|
||||
}
|
||||
463
consent-service/internal/session/session_store.go
Normal file
463
consent-service/internal/session/session_store.go
Normal file
@@ -0,0 +1,463 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/redis/go-redis/v9"
|
||||
)
|
||||
|
||||
// UserType distinguishes between internal employees and external customers
|
||||
type UserType string
|
||||
|
||||
const (
|
||||
UserTypeEmployee UserType = "employee"
|
||||
UserTypeCustomer UserType = "customer"
|
||||
)
|
||||
|
||||
// Session represents a user session with RBAC data
|
||||
type Session struct {
|
||||
SessionID string `json:"session_id"`
|
||||
UserID string `json:"user_id"`
|
||||
Email string `json:"email"`
|
||||
UserType UserType `json:"user_type"`
|
||||
Roles []string `json:"roles"`
|
||||
Permissions []string `json:"permissions"`
|
||||
TenantID *string `json:"tenant_id,omitempty"`
|
||||
IPAddress *string `json:"ip_address,omitempty"`
|
||||
UserAgent *string `json:"user_agent,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
LastActivityAt time.Time `json:"last_activity_at"`
|
||||
}
|
||||
|
||||
// HasPermission checks if session has a specific permission
|
||||
func (s *Session) HasPermission(permission string) bool {
|
||||
for _, p := range s.Permissions {
|
||||
if p == permission {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// HasAnyPermission checks if session has any of the specified permissions
|
||||
func (s *Session) HasAnyPermission(permissions []string) bool {
|
||||
for _, needed := range permissions {
|
||||
for _, has := range s.Permissions {
|
||||
if needed == has {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// HasAllPermissions checks if session has all specified permissions
|
||||
func (s *Session) HasAllPermissions(permissions []string) bool {
|
||||
for _, needed := range permissions {
|
||||
found := false
|
||||
for _, has := range s.Permissions {
|
||||
if needed == has {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// HasRole checks if session has a specific role
|
||||
func (s *Session) HasRole(role string) bool {
|
||||
for _, r := range s.Roles {
|
||||
if r == role {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IsEmployee checks if user is an employee (internal staff)
|
||||
func (s *Session) IsEmployee() bool {
|
||||
return s.UserType == UserTypeEmployee
|
||||
}
|
||||
|
||||
// IsCustomer checks if user is a customer (external user)
|
||||
func (s *Session) IsCustomer() bool {
|
||||
return s.UserType == UserTypeCustomer
|
||||
}
|
||||
|
||||
// SessionStore provides hybrid Valkey + PostgreSQL session storage
|
||||
type SessionStore struct {
|
||||
valkeyClient *redis.Client
|
||||
pgPool *pgxpool.Pool
|
||||
sessionTTL time.Duration
|
||||
valkeyEnabled bool
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewSessionStore creates a new session store
|
||||
func NewSessionStore(pgPool *pgxpool.Pool) *SessionStore {
|
||||
ttlHours := 24
|
||||
if ttlStr := os.Getenv("SESSION_TTL_HOURS"); ttlStr != "" {
|
||||
if val, err := strconv.Atoi(ttlStr); err == nil {
|
||||
ttlHours = val
|
||||
}
|
||||
}
|
||||
|
||||
store := &SessionStore{
|
||||
pgPool: pgPool,
|
||||
sessionTTL: time.Duration(ttlHours) * time.Hour,
|
||||
valkeyEnabled: false,
|
||||
}
|
||||
|
||||
// Try to connect to Valkey
|
||||
valkeyURL := os.Getenv("VALKEY_URL")
|
||||
if valkeyURL == "" {
|
||||
valkeyURL = "redis://localhost:6379"
|
||||
}
|
||||
|
||||
opt, err := redis.ParseURL(valkeyURL)
|
||||
if err == nil {
|
||||
store.valkeyClient = redis.NewClient(opt)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := store.valkeyClient.Ping(ctx).Err(); err == nil {
|
||||
store.valkeyEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
return store
|
||||
}
|
||||
|
||||
// Close closes all connections
|
||||
func (s *SessionStore) Close() {
|
||||
if s.valkeyClient != nil {
|
||||
s.valkeyClient.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// getValkeyKey returns the Valkey key for a session
|
||||
func (s *SessionStore) getValkeyKey(sessionID string) string {
|
||||
return fmt.Sprintf("session:%s", sessionID)
|
||||
}
|
||||
|
||||
// CreateSession creates a new session
|
||||
func (s *SessionStore) CreateSession(ctx context.Context, userID, email string, userType UserType, roles, permissions []string, tenantID, ipAddress, userAgent *string) (*Session, error) {
|
||||
session := &Session{
|
||||
SessionID: uuid.New().String(),
|
||||
UserID: userID,
|
||||
Email: email,
|
||||
UserType: userType,
|
||||
Roles: roles,
|
||||
Permissions: permissions,
|
||||
TenantID: tenantID,
|
||||
IPAddress: ipAddress,
|
||||
UserAgent: userAgent,
|
||||
CreatedAt: time.Now().UTC(),
|
||||
LastActivityAt: time.Now().UTC(),
|
||||
}
|
||||
|
||||
// Store in Valkey (primary cache)
|
||||
if s.valkeyEnabled {
|
||||
data, err := json.Marshal(session)
|
||||
if err == nil {
|
||||
key := s.getValkeyKey(session.SessionID)
|
||||
s.valkeyClient.SetEx(ctx, key, data, s.sessionTTL)
|
||||
}
|
||||
}
|
||||
|
||||
// Store in PostgreSQL (persistent + audit)
|
||||
if s.pgPool != nil {
|
||||
rolesJSON, _ := json.Marshal(roles)
|
||||
permsJSON, _ := json.Marshal(permissions)
|
||||
|
||||
expiresAt := time.Now().UTC().Add(s.sessionTTL)
|
||||
|
||||
_, err := s.pgPool.Exec(ctx, `
|
||||
INSERT INTO user_sessions (
|
||||
id, user_id, token_hash, email, user_type, roles,
|
||||
permissions, tenant_id, ip_address, user_agent,
|
||||
expires_at, created_at, last_activity_at
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
|
||||
`,
|
||||
session.SessionID,
|
||||
session.UserID,
|
||||
session.SessionID, // token_hash = session_id for session-based auth
|
||||
session.Email,
|
||||
string(session.UserType),
|
||||
rolesJSON,
|
||||
permsJSON,
|
||||
tenantID,
|
||||
ipAddress,
|
||||
userAgent,
|
||||
expiresAt,
|
||||
session.CreatedAt,
|
||||
session.LastActivityAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to store session in PostgreSQL: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return session, nil
|
||||
}
|
||||
|
||||
// GetSession retrieves a session by ID
|
||||
func (s *SessionStore) GetSession(ctx context.Context, sessionID string) (*Session, error) {
|
||||
// Try Valkey first
|
||||
if s.valkeyEnabled {
|
||||
key := s.getValkeyKey(sessionID)
|
||||
data, err := s.valkeyClient.Get(ctx, key).Bytes()
|
||||
if err == nil {
|
||||
var session Session
|
||||
if err := json.Unmarshal(data, &session); err == nil {
|
||||
// Update last activity
|
||||
go s.updateLastActivity(sessionID)
|
||||
return &session, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to PostgreSQL
|
||||
if s.pgPool != nil {
|
||||
var session Session
|
||||
var rolesJSON, permsJSON []byte
|
||||
var tenantID, ipAddress, userAgent *string
|
||||
|
||||
err := s.pgPool.QueryRow(ctx, `
|
||||
SELECT id, user_id, email, user_type, roles, permissions,
|
||||
tenant_id, ip_address, user_agent, created_at, last_activity_at
|
||||
FROM user_sessions
|
||||
WHERE id = $1
|
||||
AND revoked_at IS NULL
|
||||
AND expires_at > NOW()
|
||||
`, sessionID).Scan(
|
||||
&session.SessionID,
|
||||
&session.UserID,
|
||||
&session.Email,
|
||||
&session.UserType,
|
||||
&rolesJSON,
|
||||
&permsJSON,
|
||||
&tenantID,
|
||||
&ipAddress,
|
||||
&userAgent,
|
||||
&session.CreatedAt,
|
||||
&session.LastActivityAt,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.New("session not found or expired")
|
||||
}
|
||||
|
||||
json.Unmarshal(rolesJSON, &session.Roles)
|
||||
json.Unmarshal(permsJSON, &session.Permissions)
|
||||
session.TenantID = tenantID
|
||||
session.IPAddress = ipAddress
|
||||
session.UserAgent = userAgent
|
||||
|
||||
// Re-cache in Valkey
|
||||
if s.valkeyEnabled {
|
||||
data, _ := json.Marshal(session)
|
||||
key := s.getValkeyKey(sessionID)
|
||||
s.valkeyClient.SetEx(ctx, key, data, s.sessionTTL)
|
||||
}
|
||||
|
||||
return &session, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("session not found")
|
||||
}
|
||||
|
||||
// updateLastActivity updates the last activity timestamp
|
||||
func (s *SessionStore) updateLastActivity(sessionID string) {
|
||||
ctx := context.Background()
|
||||
now := time.Now().UTC()
|
||||
|
||||
// Update Valkey TTL
|
||||
if s.valkeyEnabled {
|
||||
key := s.getValkeyKey(sessionID)
|
||||
s.valkeyClient.Expire(ctx, key, s.sessionTTL)
|
||||
}
|
||||
|
||||
// Update PostgreSQL
|
||||
if s.pgPool != nil {
|
||||
s.pgPool.Exec(ctx, `
|
||||
UPDATE user_sessions
|
||||
SET last_activity_at = $1, expires_at = $2
|
||||
WHERE id = $3
|
||||
`, now, now.Add(s.sessionTTL), sessionID)
|
||||
}
|
||||
}
|
||||
|
||||
// RevokeSession revokes a session (logout)
|
||||
func (s *SessionStore) RevokeSession(ctx context.Context, sessionID string) error {
|
||||
// Remove from Valkey
|
||||
if s.valkeyEnabled {
|
||||
key := s.getValkeyKey(sessionID)
|
||||
s.valkeyClient.Del(ctx, key)
|
||||
}
|
||||
|
||||
// Mark as revoked in PostgreSQL
|
||||
if s.pgPool != nil {
|
||||
_, err := s.pgPool.Exec(ctx, `
|
||||
UPDATE user_sessions
|
||||
SET revoked_at = NOW()
|
||||
WHERE id = $1
|
||||
`, sessionID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to revoke session: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RevokeAllUserSessions revokes all sessions for a user
|
||||
func (s *SessionStore) RevokeAllUserSessions(ctx context.Context, userID string) (int, error) {
|
||||
if s.pgPool == nil {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// Get all session IDs
|
||||
rows, err := s.pgPool.Query(ctx, `
|
||||
SELECT id FROM user_sessions
|
||||
WHERE user_id = $1
|
||||
AND revoked_at IS NULL
|
||||
AND expires_at > NOW()
|
||||
`, userID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var sessionIDs []string
|
||||
for rows.Next() {
|
||||
var id string
|
||||
if err := rows.Scan(&id); err == nil {
|
||||
sessionIDs = append(sessionIDs, id)
|
||||
}
|
||||
}
|
||||
|
||||
// Revoke in PostgreSQL
|
||||
result, err := s.pgPool.Exec(ctx, `
|
||||
UPDATE user_sessions
|
||||
SET revoked_at = NOW()
|
||||
WHERE user_id = $1 AND revoked_at IS NULL
|
||||
`, userID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Remove from Valkey
|
||||
if s.valkeyEnabled {
|
||||
for _, sessionID := range sessionIDs {
|
||||
key := s.getValkeyKey(sessionID)
|
||||
s.valkeyClient.Del(ctx, key)
|
||||
}
|
||||
}
|
||||
|
||||
return int(result.RowsAffected()), nil
|
||||
}
|
||||
|
||||
// GetActiveSessions returns all active sessions for a user
|
||||
func (s *SessionStore) GetActiveSessions(ctx context.Context, userID string) ([]*Session, error) {
|
||||
if s.pgPool == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
rows, err := s.pgPool.Query(ctx, `
|
||||
SELECT id, user_id, email, user_type, roles, permissions,
|
||||
tenant_id, ip_address, user_agent, created_at, last_activity_at
|
||||
FROM user_sessions
|
||||
WHERE user_id = $1
|
||||
AND revoked_at IS NULL
|
||||
AND expires_at > NOW()
|
||||
ORDER BY last_activity_at DESC
|
||||
`, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var sessions []*Session
|
||||
for rows.Next() {
|
||||
var session Session
|
||||
var rolesJSON, permsJSON []byte
|
||||
var tenantID, ipAddress, userAgent *string
|
||||
|
||||
err := rows.Scan(
|
||||
&session.SessionID,
|
||||
&session.UserID,
|
||||
&session.Email,
|
||||
&session.UserType,
|
||||
&rolesJSON,
|
||||
&permsJSON,
|
||||
&tenantID,
|
||||
&ipAddress,
|
||||
&userAgent,
|
||||
&session.CreatedAt,
|
||||
&session.LastActivityAt,
|
||||
)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
json.Unmarshal(rolesJSON, &session.Roles)
|
||||
json.Unmarshal(permsJSON, &session.Permissions)
|
||||
session.TenantID = tenantID
|
||||
session.IPAddress = ipAddress
|
||||
session.UserAgent = userAgent
|
||||
|
||||
sessions = append(sessions, &session)
|
||||
}
|
||||
|
||||
return sessions, nil
|
||||
}
|
||||
|
||||
// CleanupExpiredSessions removes old expired sessions from PostgreSQL
|
||||
func (s *SessionStore) CleanupExpiredSessions(ctx context.Context) (int, error) {
|
||||
if s.pgPool == nil {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
result, err := s.pgPool.Exec(ctx, `
|
||||
DELETE FROM user_sessions
|
||||
WHERE expires_at < NOW() - INTERVAL '7 days'
|
||||
`)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return int(result.RowsAffected()), nil
|
||||
}
|
||||
|
||||
// Global session store instance
|
||||
var (
|
||||
globalStore *SessionStore
|
||||
globalStoreMu sync.Mutex
|
||||
globalStoreOnce sync.Once
|
||||
)
|
||||
|
||||
// GetSessionStore returns the global session store instance
|
||||
func GetSessionStore(pgPool *pgxpool.Pool) *SessionStore {
|
||||
globalStoreMu.Lock()
|
||||
defer globalStoreMu.Unlock()
|
||||
|
||||
if globalStore == nil {
|
||||
globalStore = NewSessionStore(pgPool)
|
||||
}
|
||||
|
||||
return globalStore
|
||||
}
|
||||
342
consent-service/internal/session/session_test.go
Normal file
342
consent-service/internal/session/session_test.go
Normal file
@@ -0,0 +1,342 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestSessionHasPermission(t *testing.T) {
|
||||
session := &Session{
|
||||
SessionID: "test-session-id",
|
||||
UserID: "test-user-id",
|
||||
Email: "test@example.com",
|
||||
UserType: UserTypeEmployee,
|
||||
Permissions: []string{"grades:read", "grades:write", "attendance:read"},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
permission string
|
||||
expected bool
|
||||
}{
|
||||
{"has grades:read", "grades:read", true},
|
||||
{"has grades:write", "grades:write", true},
|
||||
{"missing users:manage", "users:manage", false},
|
||||
{"empty permission", "", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := session.HasPermission(tt.permission)
|
||||
if result != tt.expected {
|
||||
t.Errorf("HasPermission(%q) = %v, want %v", tt.permission, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionHasAnyPermission(t *testing.T) {
|
||||
session := &Session{
|
||||
SessionID: "test-session-id",
|
||||
UserID: "test-user-id",
|
||||
Email: "test@example.com",
|
||||
UserType: UserTypeEmployee,
|
||||
Permissions: []string{"grades:read"},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
permissions []string
|
||||
expected bool
|
||||
}{
|
||||
{"has one of the permissions", []string{"grades:read", "grades:write"}, true},
|
||||
{"missing all permissions", []string{"users:manage", "audit:read"}, false},
|
||||
{"empty list", []string{}, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := session.HasAnyPermission(tt.permissions)
|
||||
if result != tt.expected {
|
||||
t.Errorf("HasAnyPermission(%v) = %v, want %v", tt.permissions, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionHasAllPermissions(t *testing.T) {
|
||||
session := &Session{
|
||||
SessionID: "test-session-id",
|
||||
UserID: "test-user-id",
|
||||
Email: "test@example.com",
|
||||
UserType: UserTypeEmployee,
|
||||
Permissions: []string{"grades:read", "grades:write", "attendance:read"},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
permissions []string
|
||||
expected bool
|
||||
}{
|
||||
{"has all permissions", []string{"grades:read", "grades:write"}, true},
|
||||
{"missing one permission", []string{"grades:read", "users:manage"}, false},
|
||||
{"empty list", []string{}, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := session.HasAllPermissions(tt.permissions)
|
||||
if result != tt.expected {
|
||||
t.Errorf("HasAllPermissions(%v) = %v, want %v", tt.permissions, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionHasRole(t *testing.T) {
|
||||
session := &Session{
|
||||
SessionID: "test-session-id",
|
||||
UserID: "test-user-id",
|
||||
Email: "test@example.com",
|
||||
UserType: UserTypeEmployee,
|
||||
Roles: []string{"teacher", "klassenlehrer"},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
role string
|
||||
expected bool
|
||||
}{
|
||||
{"has teacher role", "teacher", true},
|
||||
{"has klassenlehrer role", "klassenlehrer", true},
|
||||
{"missing admin role", "admin", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := session.HasRole(tt.role)
|
||||
if result != tt.expected {
|
||||
t.Errorf("HasRole(%q) = %v, want %v", tt.role, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionIsEmployee(t *testing.T) {
|
||||
employeeSession := &Session{
|
||||
SessionID: "test",
|
||||
UserID: "test",
|
||||
Email: "test@test.com",
|
||||
UserType: UserTypeEmployee,
|
||||
}
|
||||
|
||||
customerSession := &Session{
|
||||
SessionID: "test",
|
||||
UserID: "test",
|
||||
Email: "test@test.com",
|
||||
UserType: UserTypeCustomer,
|
||||
}
|
||||
|
||||
if !employeeSession.IsEmployee() {
|
||||
t.Error("Employee session should return true for IsEmployee()")
|
||||
}
|
||||
if employeeSession.IsCustomer() {
|
||||
t.Error("Employee session should return false for IsCustomer()")
|
||||
}
|
||||
if !customerSession.IsCustomer() {
|
||||
t.Error("Customer session should return true for IsCustomer()")
|
||||
}
|
||||
if customerSession.IsEmployee() {
|
||||
t.Error("Customer session should return false for IsEmployee()")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetermineUserType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
roles []string
|
||||
expected UserType
|
||||
}{
|
||||
{"teacher is employee", []string{"teacher"}, UserTypeEmployee},
|
||||
{"admin is employee", []string{"admin"}, UserTypeEmployee},
|
||||
{"klassenlehrer is employee", []string{"klassenlehrer"}, UserTypeEmployee},
|
||||
{"parent is customer", []string{"parent"}, UserTypeCustomer},
|
||||
{"student is customer", []string{"student"}, UserTypeCustomer},
|
||||
{"employee takes precedence", []string{"teacher", "parent"}, UserTypeEmployee},
|
||||
{"unknown role defaults to customer", []string{"unknown_role"}, UserTypeCustomer},
|
||||
{"empty roles defaults to customer", []string{}, UserTypeCustomer},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := DetermineUserType(tt.roles)
|
||||
if result != tt.expected {
|
||||
t.Errorf("DetermineUserType(%v) = %v, want %v", tt.roles, result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPermissionsForRoles(t *testing.T) {
|
||||
t.Run("employee gets employee permissions", func(t *testing.T) {
|
||||
permissions := GetPermissionsForRoles([]string{"teacher"}, UserTypeEmployee)
|
||||
|
||||
hasGradesRead := false
|
||||
for _, p := range permissions {
|
||||
if p == "grades:read" {
|
||||
hasGradesRead = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasGradesRead {
|
||||
t.Error("Employee should have grades:read permission")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("customer gets customer permissions", func(t *testing.T) {
|
||||
permissions := GetPermissionsForRoles([]string{"parent"}, UserTypeCustomer)
|
||||
|
||||
hasChildrenRead := false
|
||||
for _, p := range permissions {
|
||||
if p == "children:read" {
|
||||
hasChildrenRead = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasChildrenRead {
|
||||
t.Error("Customer should have children:read permission")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("admin gets admin permissions", func(t *testing.T) {
|
||||
permissions := GetPermissionsForRoles([]string{"admin"}, UserTypeEmployee)
|
||||
|
||||
hasUsersManage := false
|
||||
for _, p := range permissions {
|
||||
if p == "users:manage" {
|
||||
hasUsersManage = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasUsersManage {
|
||||
t.Error("Admin should have users:manage permission")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCheckResourceOwnership(t *testing.T) {
|
||||
userID := "user-123"
|
||||
adminSession := &Session{
|
||||
SessionID: "test",
|
||||
UserID: "admin-456",
|
||||
Email: "admin@test.com",
|
||||
UserType: UserTypeEmployee,
|
||||
Roles: []string{"admin"},
|
||||
}
|
||||
regularSession := &Session{
|
||||
SessionID: "test",
|
||||
UserID: userID,
|
||||
Email: "user@test.com",
|
||||
UserType: UserTypeEmployee,
|
||||
Roles: []string{"teacher"},
|
||||
}
|
||||
otherSession := &Session{
|
||||
SessionID: "test",
|
||||
UserID: "other-789",
|
||||
Email: "other@test.com",
|
||||
UserType: UserTypeEmployee,
|
||||
Roles: []string{"teacher"},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
session *Session
|
||||
resourceUID string
|
||||
allowAdmin bool
|
||||
expected bool
|
||||
}{
|
||||
{"owner can access", regularSession, userID, true, true},
|
||||
{"admin can access with allowAdmin", adminSession, userID, true, true},
|
||||
{"admin cannot access without allowAdmin", adminSession, userID, false, false},
|
||||
{"other user cannot access", otherSession, userID, true, false},
|
||||
{"nil session returns false", nil, userID, true, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := CheckResourceOwnership(tt.session, tt.resourceUID, tt.allowAdmin)
|
||||
if result != tt.expected {
|
||||
t.Errorf("CheckResourceOwnership() = %v, want %v", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmployeeRolesMap(t *testing.T) {
|
||||
expectedRoles := []string{
|
||||
"admin", "schul_admin", "teacher", "klassenlehrer",
|
||||
"fachlehrer", "sekretariat", "data_protection_officer",
|
||||
}
|
||||
|
||||
for _, role := range expectedRoles {
|
||||
if !EmployeeRoles[role] {
|
||||
t.Errorf("Expected employee role %q not found in EmployeeRoles map", role)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCustomerRolesMap(t *testing.T) {
|
||||
expectedRoles := []string{"parent", "student", "user"}
|
||||
|
||||
for _, role := range expectedRoles {
|
||||
if !CustomerRoles[role] {
|
||||
t.Errorf("Expected customer role %q not found in CustomerRoles map", role)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPermissionSlicesNotEmpty(t *testing.T) {
|
||||
if len(EmployeePermissions) == 0 {
|
||||
t.Error("EmployeePermissions should not be empty")
|
||||
}
|
||||
if len(CustomerPermissions) == 0 {
|
||||
t.Error("CustomerPermissions should not be empty")
|
||||
}
|
||||
if len(AdminPermissions) == 0 {
|
||||
t.Error("AdminPermissions should not be empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionTimestamps(t *testing.T) {
|
||||
now := time.Now().UTC()
|
||||
session := &Session{
|
||||
SessionID: "test",
|
||||
UserID: "test",
|
||||
Email: "test@test.com",
|
||||
UserType: UserTypeEmployee,
|
||||
CreatedAt: now,
|
||||
LastActivityAt: now,
|
||||
}
|
||||
|
||||
if session.CreatedAt.IsZero() {
|
||||
t.Error("CreatedAt should not be zero")
|
||||
}
|
||||
if session.LastActivityAt.IsZero() {
|
||||
t.Error("LastActivityAt should not be zero")
|
||||
}
|
||||
if session.CreatedAt.After(time.Now().UTC()) {
|
||||
t.Error("CreatedAt should not be in the future")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserTypeConstants(t *testing.T) {
|
||||
if UserTypeEmployee != "employee" {
|
||||
t.Errorf("UserTypeEmployee = %q, want %q", UserTypeEmployee, "employee")
|
||||
}
|
||||
if UserTypeCustomer != "customer" {
|
||||
t.Errorf("UserTypeCustomer = %q, want %q", UserTypeCustomer, "customer")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user