fix: wire permission checks into message/channel handlers
- Add CheckChannelPermission to permissions.Checker (layers channel overrides on top of role permissions) - Refactor requireChannelAccess to accept required permission bitmap - Message Create checks SEND_MESSAGES; List/Search check VIEW_CHANNEL - BulkDelete uses Checker.CheckPermission for MANAGE_MESSAGES - Channel handler List filters out channels user cannot view - Remove dead checkPermission method from message handler - Fix frontend avatar upload: parse response URL, call updateProfile
This commit is contained in:
@@ -26,15 +26,17 @@ type Handler struct {
|
||||
mentions *MentionHandler
|
||||
pushHandler *push.Handler
|
||||
logger *slog.Logger
|
||||
checker *permissions.Checker
|
||||
}
|
||||
|
||||
func NewHandler(db *sql.DB, hub *gateway.Hub, pushHandler *push.Handler, logger *slog.Logger) *Handler {
|
||||
func NewHandler(db *sql.DB, hub *gateway.Hub, pushHandler *push.Handler, logger *slog.Logger, checker *permissions.Checker) *Handler {
|
||||
return &Handler{
|
||||
db: db,
|
||||
hub: hub,
|
||||
mentions: NewMentionHandler(db, pushHandler, logger),
|
||||
pushHandler: pushHandler,
|
||||
logger: logger,
|
||||
checker: checker,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,14 +61,12 @@ type bulkDeleteResponse struct {
|
||||
// and messages must be within the last 14 days.
|
||||
func (h *Handler) BulkDelete(w http.ResponseWriter, r *http.Request) {
|
||||
channelID := chi.URLParam(r, "channelID")
|
||||
userID, serverID, ok := h.requireChannelAccess(w, r, channelID)
|
||||
userID, serverID, ok := h.requireChannelAccess(w, r, channelID, 0)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// ponytail: reuse existing auth structure; requireChannelAccess already verifies membership.
|
||||
// Permission check uses the server's permission checker via the caller's middleware chain.
|
||||
allowed, err := h.checkPermission(r.Context(), serverID, userID, permissions.MANAGE_MESSAGES)
|
||||
allowed, err := h.checker.CheckPermission(r.Context(), serverID, userID, permissions.MANAGE_MESSAGES)
|
||||
if err != nil {
|
||||
http.Error(w, `{"error":"server error"}`, http.StatusInternalServerError)
|
||||
return
|
||||
@@ -126,37 +126,6 @@ func (h *Handler) BulkDelete(w http.ResponseWriter, r *http.Request) {
|
||||
json.NewEncoder(w).Encode(bulkDeleteResponse{Deleted: int(deleted)})
|
||||
}
|
||||
|
||||
// checkPermission is a helper wrapper around a permission checker lookup.
|
||||
func (h *Handler) checkPermission(ctx context.Context, serverID, userID string, perm int64) (bool, error) {
|
||||
// ponytail: keep it minimal; caller already has serverID. If no checker is wired, fall back to true only for admins.
|
||||
rows, err := h.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 false, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var effective int64
|
||||
for rows.Next() {
|
||||
var p int64
|
||||
if err := rows.Scan(&p); err != nil {
|
||||
return false, err
|
||||
}
|
||||
effective |= p
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
var everyone int64
|
||||
_ = h.db.QueryRowContext(ctx, `
|
||||
SELECT permissions FROM roles WHERE server_id = $1 AND is_default = TRUE LIMIT 1
|
||||
`, serverID).Scan(&everyone)
|
||||
effective |= everyone
|
||||
return permissions.Has(effective, permissions.ADMINISTRATOR) || permissions.Has(effective, perm), nil
|
||||
}
|
||||
|
||||
type embedResponse struct {
|
||||
ID string `json:"id"`
|
||||
URL string `json:"url"`
|
||||
@@ -208,7 +177,7 @@ type createMessageRequest struct {
|
||||
// @Router /channels/{channelID}/messages [post]
|
||||
func (h *Handler) Create(w http.ResponseWriter, r *http.Request) {
|
||||
channelID := chi.URLParam(r, "channelID")
|
||||
userID, serverID, ok := h.requireChannelAccess(w, r, channelID)
|
||||
userID, serverID, ok := h.requireChannelAccess(w, r, channelID, permissions.SEND_MESSAGES)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
@@ -494,7 +463,7 @@ func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
// @Router /channels/{channelID}/messages [get]
|
||||
func (h *Handler) List(w http.ResponseWriter, r *http.Request) {
|
||||
channelID := chi.URLParam(r, "channelID")
|
||||
userID, _, ok := h.requireChannelAccess(w, r, channelID)
|
||||
userID, _, ok := h.requireChannelAccess(w, r, channelID, permissions.VIEW_CHANNEL)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
@@ -599,7 +568,7 @@ func (h *Handler) attachEmbeds(ctx context.Context, messages []messageResponse)
|
||||
// Search searches messages in a channel using PostgreSQL full-text search.
|
||||
func (h *Handler) Search(w http.ResponseWriter, r *http.Request) {
|
||||
channelID := chi.URLParam(r, "channelID")
|
||||
_, _, ok := h.requireChannelAccess(w, r, channelID)
|
||||
_, _, ok := h.requireChannelAccess(w, r, channelID, permissions.VIEW_CHANNEL)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
@@ -661,8 +630,9 @@ func (h *Handler) Search(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// requireChannelAccess checks if user is authenticated and is a member of the channel's server.
|
||||
// Returns (userID, serverID, ok).
|
||||
func (h *Handler) requireChannelAccess(w http.ResponseWriter, r *http.Request, channelID string) (string, string, bool) {
|
||||
// Returns (userID, serverID, ok). If required is non-zero, also checks the permission against
|
||||
// channel-level overrides.
|
||||
func (h *Handler) requireChannelAccess(w http.ResponseWriter, r *http.Request, channelID string, required int64) (string, string, bool) {
|
||||
userID, ok := middleware.UserIDFromContext(r.Context())
|
||||
if !ok {
|
||||
http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
|
||||
@@ -682,5 +652,19 @@ func (h *Handler) requireChannelAccess(w http.ResponseWriter, r *http.Request, c
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
// Check channel-specific permission if a bitmap was provided.
|
||||
if required != 0 {
|
||||
allowed, checkErr := h.checker.CheckChannelPermission(r.Context(), serverID, userID, channelID, required)
|
||||
if checkErr != nil {
|
||||
h.logger.Error("permission check failed", "error", checkErr)
|
||||
http.Error(w, `{"error":"server error"}`, http.StatusInternalServerError)
|
||||
return "", "", false
|
||||
}
|
||||
if !allowed {
|
||||
http.Error(w, `{"error":"missing permissions"}`, http.StatusForbidden)
|
||||
return "", "", false
|
||||
}
|
||||
}
|
||||
|
||||
return userID, serverID, true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user