package permissions import ( "context" "database/sql" ) // Checker verifies user permissions against the database. type Checker struct { db *sql.DB } // NewChecker creates a new Checker backed by the given database connection. func NewChecker(db *sql.DB) *Checker { return &Checker{db: db} } // GetUserPermissions returns the effective (OR'd) permissions for a user in a server. // It aggregates all permissions from the roles assigned to the user, plus the // @everyone role for that server. func (c *Checker) GetUserPermissions(ctx context.Context, serverID string, userID string) (int64, error) { rows, err := c.db.QueryContext(ctx, ` SELECT r.permissions FROM roles r INNER JOIN member_roles mr ON mr.role_id = r.id WHERE mr.user_id = $1 AND mr.server_id = $2 `, userID, serverID) if err != nil { return 0, err } defer rows.Close() var effective int64 found := false for rows.Next() { var perm int64 if err := rows.Scan(&perm); err != nil { return 0, err } effective |= perm found = true } if err := rows.Err(); err != nil { return 0, err } // Also include the @everyone role permissions for this server. var everyonePerm int64 err = c.db.QueryRowContext(ctx, ` SELECT permissions FROM roles WHERE server_id = $1 AND is_default = TRUE LIMIT 1 `, serverID).Scan(&everyonePerm) if err != nil && err != sql.ErrNoRows { return 0, err } effective |= everyonePerm if everyonePerm != 0 { found = true } if !found { return 0, nil } return effective, nil } // CheckPermission reports whether the user has all the required permission bits // in the given server. ADMINISTRATOR bypasses all checks. func (c *Checker) CheckPermission(ctx context.Context, serverID string, userID string, required int64) (bool, error) { // Check if the user is the server owner. var ownerID string err := c.db.QueryRowContext(ctx, `SELECT owner_id FROM servers WHERE id = $1`, serverID).Scan(&ownerID) if err == nil && ownerID == userID { return true, nil } perms, err := c.GetUserPermissions(ctx, serverID, userID) if err != nil { return false, err } // Administrator bypasses everything. if Has(perms, ADMINISTRATOR) { return true, nil } return Has(perms, required), nil } // CheckChannelPermission reports whether the user has the required permission bits // for a specific channel. It layers channel-level overrides on top of server role // permissions. ADMINISTRATOR bypasses all checks. func (c *Checker) CheckChannelPermission(ctx context.Context, serverID, userID, channelID string, required int64) (bool, error) { // Check if the user is the server owner. var ownerID string err := c.db.QueryRowContext(ctx, `SELECT owner_id FROM servers WHERE id = $1`, serverID).Scan(&ownerID) if err == nil && ownerID == userID { return true, nil } perms, err := c.GetUserPermissions(ctx, serverID, userID) if err != nil { return false, err } // Administrator bypasses everything. if Has(perms, ADMINISTRATOR) { return true, nil } // Layer channel overrides: apply deny first (subtract), then allow (add). // Role-based overrides from all user roles, plus the @everyone role, then user-specific override. rows, err := c.db.QueryContext(ctx, ` SELECT co.allow_bitflags, co.deny_bitflags FROM channel_overrides co WHERE co.channel_id = $1 AND co.target_type = 'role' AND ( co.target_id IN (SELECT mr.role_id FROM member_roles mr WHERE mr.user_id = $2 AND mr.server_id = $3) OR co.target_id = (SELECT id FROM roles WHERE server_id = $3 AND is_default = TRUE LIMIT 1) ) `, channelID, userID, serverID) if err != nil { return false, err } var allow, deny int64 if rows != nil { for rows.Next() { var a, d int64 if err := rows.Scan(&a, &d); err != nil { rows.Close() return false, err } allow |= a deny |= d } rows.Close() } // User-specific override. var ua, ud int64 err = c.db.QueryRowContext(ctx, ` SELECT allow_bitflags, deny_bitflags FROM channel_overrides WHERE channel_id = $1 AND target_type = 'user' AND target_id = $2 `, channelID, userID).Scan(&ua, &ud) if err != nil && err != sql.ErrNoRows { return false, err } allow |= ua deny |= ud // Apply deny first, then allow. perms = (perms &^ deny) | allow return Has(perms, required), nil }