Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3699f37b76 |
@@ -201,8 +201,8 @@ type Config struct {
|
|||||||
// link-local that disclose credentials or user data at a
|
// link-local that disclose credentials or user data at a
|
||||||
// provider-fixed, non-public address; it is not exhaustive of
|
// provider-fixed, non-public address; it is not exhaustive of
|
||||||
// every cloud's metadata address. See
|
// every cloud's metadata address. See
|
||||||
// alwaysBlockedNetworks for the authoritative list and why
|
// alwaysBlockedNetworks for the authoritative list and the
|
||||||
// each entry is on it.
|
// criterion it is built from.
|
||||||
AllowedEgressCIDRs []netip.Prefix
|
AllowedEgressCIDRs []netip.Prefix
|
||||||
|
|
||||||
params *ConfigParams
|
params *ConfigParams
|
||||||
|
|||||||
+19
-20
@@ -72,17 +72,16 @@ var blockedNetworks []*net.IPNet
|
|||||||
var blockedPublicNetworks []*net.IPNet
|
var blockedPublicNetworks []*net.IPNet
|
||||||
|
|
||||||
// alwaysBlockedNetworks are the ranges no configuration can
|
// alwaysBlockedNetworks are the ranges no configuration can
|
||||||
// open, so a supplied CIDR that covers one still leaves it
|
// open: the link-local blocks, the cloud instance metadata
|
||||||
// blocked. An entry is here for one of two reasons: it is a
|
// endpoints that live outside them, and the unspecified
|
||||||
// metadata endpoint (the link-local blocks and the cloud
|
// addresses. Reaching a metadata endpoint is credential or
|
||||||
// instance metadata endpoints that live outside them), or it is
|
// user-data theft rather than delivery to an internal service,
|
||||||
// an unspecified address. Reaching a metadata endpoint is
|
// so a supplied CIDR that covers such an address still leaves it
|
||||||
// credential or user-data theft rather than delivery to an
|
// blocked.
|
||||||
// internal service.
|
|
||||||
//
|
//
|
||||||
// Inclusion criterion for metadata endpoints — one belongs here
|
// Inclusion criterion — an address belongs here only if BOTH
|
||||||
// only if BOTH hold, and every metadata entry below satisfies
|
// hold, and every entry below but the unspecified addresses
|
||||||
// both:
|
// satisfies both:
|
||||||
//
|
//
|
||||||
// 1. It is a fixed address assigned by the provider, or a
|
// 1. It is a fixed address assigned by the provider, or a
|
||||||
// range reserved by IANA — never one the operator chose.
|
// range reserved by IANA — never one the operator chose.
|
||||||
@@ -93,8 +92,8 @@ var blockedPublicNetworks []*net.IPNet
|
|||||||
// not cheaply rotated.
|
// not cheaply rotated.
|
||||||
//
|
//
|
||||||
// Both halves are load-bearing, so use them to refuse a
|
// Both halves are load-bearing, so use them to refuse a
|
||||||
// metadata candidate and say why. An endpoint disclosing only
|
// candidate and say why. An endpoint disclosing only the
|
||||||
// the operator's own inventory (instance id, region, disks, NICs)
|
// operator's own inventory (instance id, region, disks, NICs)
|
||||||
// fails (2): letting a delivery target reach the operator's own
|
// fails (2): letting a delivery target reach the operator's own
|
||||||
// infrastructure is the feature ALLOWED_EGRESS_CIDRS exists to
|
// infrastructure is the feature ALLOWED_EGRESS_CIDRS exists to
|
||||||
// provide. But (2) is not "IAM credentials only" either —
|
// provide. But (2) is not "IAM credentials only" either —
|
||||||
@@ -115,14 +114,14 @@ var blockedPublicNetworks []*net.IPNet
|
|||||||
// This is a criterion, not an enumeration of every metadata
|
// This is a criterion, not an enumeration of every metadata
|
||||||
// address in existence.
|
// address in existence.
|
||||||
//
|
//
|
||||||
// The unspecified addresses 0.0.0.0 and :: are here for a
|
// The unspecified addresses 0.0.0.0 and :: fail (2) and are here
|
||||||
// separate reason: they disclose nothing, but no host can have
|
// anyway. No host can have either, and on Linux a connection to
|
||||||
// either, and on Linux a connection to one reaches this host's
|
// one reaches this host's own loopback. Listing them means an
|
||||||
// own loopback. Listing them means an allowlist reaches loopback
|
// allowlist reaches loopback only through an entry that covers a
|
||||||
// only through an entry that covers a loopback address
|
// loopback address (127.0.0.0/8, ::1/128, 0.0.0.0/0), never
|
||||||
// (127.0.0.0/8, ::1/128, 0.0.0.0/0), never through one that
|
// through one that covers only 0.0.0.0 or :: (0.0.0.0/8, for
|
||||||
// covers only 0.0.0.0 or :: (0.0.0.0/8, for example). Nothing
|
// example). Nothing else lives at either address, so refusing
|
||||||
// else lives at either address, so refusing them costs nothing.
|
// them costs nothing.
|
||||||
//
|
//
|
||||||
// Every entry is either already in blockedNetworks — this list is
|
// Every entry is either already in blockedNetworks — this list is
|
||||||
// what makes it unconditional — or an alternate encoding of
|
// what makes it unconditional — or an alternate encoding of
|
||||||
|
|||||||
@@ -333,9 +333,7 @@ func (h *Handlers) HandleLogout() http.HandlerFunc {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
http.Redirect(
|
// Redirect to login page
|
||||||
w, r, withNotice("/pages/login", signedOut),
|
http.Redirect(w, r, "/pages/login", http.StatusSeeOther)
|
||||||
http.StatusSeeOther,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,37 +11,72 @@ import (
|
|||||||
"sneak.berlin/go/webhooker/internal/delivery"
|
"sneak.berlin/go/webhooker/internal/delivery"
|
||||||
)
|
)
|
||||||
|
|
||||||
// The outcomes of a replay POST, as the notice codes its redirect
|
// replayOutcomeParam is the query parameter the replay POST redirects
|
||||||
// carries. noticeFor holds the line each one shows.
|
// with and the event log page reads its banner from.
|
||||||
|
const replayOutcomeParam = "replay"
|
||||||
|
|
||||||
|
// replayOutcomeCode is the outcome of a replay POST. The redirect
|
||||||
|
// carries one of these fixed codes rather than a message, so nothing a
|
||||||
|
// client submits can reach the rendered page through it.
|
||||||
|
type replayOutcomeCode string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// replayQueued reports that a new delivery was created and handed
|
// replayQueued reports that a new delivery was created and handed
|
||||||
// to the delivery engine.
|
// to the delivery engine.
|
||||||
replayQueued noticeCode = "replay-queued"
|
replayQueued replayOutcomeCode = "queued"
|
||||||
|
|
||||||
// replayTargetDeleted reports a target that once existed and has
|
// replayTargetDeleted reports a target that once existed and has
|
||||||
// since been deleted. Deletes are soft and deliveries carry no
|
// since been deleted. Deletes are soft and deliveries carry no
|
||||||
// foreign key to the target row, so the history survives its
|
// foreign key to the target row, so the history survives its
|
||||||
// target and this is the ordinary case for an old event.
|
// target and this is the ordinary case for an old event.
|
||||||
replayTargetDeleted noticeCode = "replay-target-deleted"
|
replayTargetDeleted replayOutcomeCode = "target-deleted"
|
||||||
|
|
||||||
// replayTargetMissing reports a target id that names no row at
|
// replayTargetMissing reports a target id that names no row at
|
||||||
// all, deleted or otherwise.
|
// all, deleted or otherwise.
|
||||||
replayTargetMissing noticeCode = "replay-target-missing"
|
replayTargetMissing replayOutcomeCode = "target-missing"
|
||||||
|
|
||||||
// replayTargetInactive reports a target the operator has
|
// replayTargetInactive reports a target the operator has
|
||||||
// deactivated. A deactivated target receives no new deliveries, so
|
// deactivated. A deactivated target receives no new deliveries, so
|
||||||
// a replay to it would be a delivery they switched off.
|
// a replay to it would be a delivery they switched off.
|
||||||
replayTargetInactive noticeCode = "replay-target-inactive"
|
replayTargetInactive replayOutcomeCode = "target-inactive"
|
||||||
|
|
||||||
// replayNotTerminal reports a delivery the engine has not finished
|
// replayNotTerminal reports a delivery the engine has not finished
|
||||||
// with.
|
// with.
|
||||||
replayNotTerminal noticeCode = "replay-not-terminal"
|
replayNotTerminal replayOutcomeCode = "not-terminal"
|
||||||
|
|
||||||
// replayInFlight reports that an earlier replay of this event to
|
// replayInFlight reports that an earlier replay of this event to
|
||||||
// this target is still running.
|
// this target is still running.
|
||||||
replayInFlight noticeCode = "replay-in-flight"
|
replayInFlight replayOutcomeCode = "in-flight"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// replayOutcome returns the banner the event log page shows for an
|
||||||
|
// outcome code, and whether the replay was queued. An unrecognised
|
||||||
|
// code yields no banner.
|
||||||
|
func replayOutcome(code string) (string, bool) {
|
||||||
|
switch replayOutcomeCode(code) {
|
||||||
|
case replayQueued:
|
||||||
|
return "Replay queued: a new delivery was created against " +
|
||||||
|
"the target's current configuration.", true
|
||||||
|
case replayTargetDeleted:
|
||||||
|
return "Not replayed: the target this delivery was for has " +
|
||||||
|
"been deleted. Recreate the target, then replay.", false
|
||||||
|
case replayTargetMissing:
|
||||||
|
return "Not replayed: the target this delivery was for no " +
|
||||||
|
"longer exists.", false
|
||||||
|
case replayTargetInactive:
|
||||||
|
return "Not replayed: the target this delivery was for is " +
|
||||||
|
"deactivated. Activate it, then replay.", false
|
||||||
|
case replayNotTerminal:
|
||||||
|
return "Not replayed: this delivery has not finished yet.",
|
||||||
|
false
|
||||||
|
case replayInFlight:
|
||||||
|
return "Not replayed: a delivery of this event to this " +
|
||||||
|
"target is already in flight.", false
|
||||||
|
default:
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// HandleDeliveryReplay re-sends a finished delivery's event to its
|
// HandleDeliveryReplay re-sends a finished delivery's event to its
|
||||||
// target.
|
// target.
|
||||||
//
|
//
|
||||||
@@ -105,14 +140,14 @@ func (h *Handlers) replayDelivery(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !original.Status.Terminal() {
|
if !original.Status.Terminal() {
|
||||||
redirectToEventLog(w, r, webhook, replayNotTerminal)
|
h.finishReplay(w, r, webhook, replayNotTerminal)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
target, code := h.replayTarget(webhook.ID, original.TargetID)
|
target, code := h.replayTarget(webhook.ID, original.TargetID)
|
||||||
if target == nil {
|
if target == nil {
|
||||||
redirectToEventLog(w, r, webhook, code)
|
h.finishReplay(w, r, webhook, code)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -165,7 +200,7 @@ func (h *Handlers) queueReplay(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if inFlight > 0 {
|
if inFlight > 0 {
|
||||||
redirectToEventLog(w, r, webhook, replayInFlight)
|
h.finishReplay(w, r, webhook, replayInFlight)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -203,7 +238,7 @@ func (h *Handlers) queueReplay(
|
|||||||
"delivery_id", task.DeliveryID,
|
"delivery_id", task.DeliveryID,
|
||||||
)
|
)
|
||||||
|
|
||||||
redirectToEventLog(w, r, webhook, replayQueued)
|
h.finishReplay(w, r, webhook, replayQueued)
|
||||||
}
|
}
|
||||||
|
|
||||||
// replayTarget loads the delivery's target as it stands now.
|
// replayTarget loads the delivery's target as it stands now.
|
||||||
@@ -216,7 +251,7 @@ func (h *Handlers) queueReplay(
|
|||||||
// with the returned code saying why.
|
// with the returned code saying why.
|
||||||
func (h *Handlers) replayTarget(
|
func (h *Handlers) replayTarget(
|
||||||
webhookID, targetID string,
|
webhookID, targetID string,
|
||||||
) (*database.Target, noticeCode) {
|
) (*database.Target, replayOutcomeCode) {
|
||||||
var target database.Target
|
var target database.Target
|
||||||
|
|
||||||
err := h.db.DB().Unscoped().Where(
|
err := h.db.DB().Unscoped().Where(
|
||||||
@@ -326,16 +361,17 @@ func replayBody(body string) *string {
|
|||||||
return &body
|
return &body
|
||||||
}
|
}
|
||||||
|
|
||||||
// redirectToEventLog redirects a replay or resubmit back to the event
|
// finishReplay redirects back to the event log the replay was
|
||||||
// log it was triggered from, carrying the outcome as its notice and
|
// triggered from, carrying the outcome code the page turns into a
|
||||||
// the page number the form submitted.
|
// banner and the page number the form submitted.
|
||||||
func redirectToEventLog(
|
func (h *Handlers) finishReplay(
|
||||||
w http.ResponseWriter,
|
w http.ResponseWriter,
|
||||||
r *http.Request,
|
r *http.Request,
|
||||||
webhook database.Webhook,
|
webhook database.Webhook,
|
||||||
code noticeCode,
|
code replayOutcomeCode,
|
||||||
) {
|
) {
|
||||||
dest := withNotice("/hook/"+webhook.ID+"/events", code)
|
dest := "/hook/" + webhook.ID + "/events?" +
|
||||||
|
replayOutcomeParam + "=" + string(code)
|
||||||
|
|
||||||
// The page is read from the form rather than the query string:
|
// The page is read from the form rather than the query string:
|
||||||
// this is a POST, and its query string is what logs and Referer
|
// this is a POST, and its query string is what logs and Referer
|
||||||
|
|||||||
@@ -212,7 +212,7 @@ func TestHandleDeliveryReplay_AppendsDeliveryAndLeavesOriginal(
|
|||||||
require.Equal(t, http.StatusSeeOther, w.Code)
|
require.Equal(t, http.StatusSeeOther, w.Code)
|
||||||
assert.Equal(
|
assert.Equal(
|
||||||
t,
|
t,
|
||||||
"/hook/"+wh.ID+"/events?notice=replay-queued",
|
"/hook/"+wh.ID+"/events?replay=queued",
|
||||||
w.Header().Get("Location"),
|
w.Header().Get("Location"),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -362,7 +362,7 @@ func TestHandleDeliveryReplay_RefusesDeletedTarget(t *testing.T) {
|
|||||||
require.Equal(t, http.StatusSeeOther, w.Code)
|
require.Equal(t, http.StatusSeeOther, w.Code)
|
||||||
assert.Equal(
|
assert.Equal(
|
||||||
t,
|
t,
|
||||||
"/hook/"+wh.ID+"/events?notice=replay-target-deleted",
|
"/hook/"+wh.ID+"/events?replay=target-deleted",
|
||||||
w.Header().Get("Location"),
|
w.Header().Get("Location"),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -390,7 +390,7 @@ func TestHandleDeliveryReplay_RefusesDeletedTarget(t *testing.T) {
|
|||||||
require.Equal(t, http.StatusSeeOther, missing.Code)
|
require.Equal(t, http.StatusSeeOther, missing.Code)
|
||||||
assert.Equal(
|
assert.Equal(
|
||||||
t,
|
t,
|
||||||
"/hook/"+wh.ID+"/events?notice=replay-target-missing",
|
"/hook/"+wh.ID+"/events?replay=target-missing",
|
||||||
missing.Header().Get("Location"),
|
missing.Header().Get("Location"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -431,7 +431,7 @@ func TestHandleDeliveryReplay_RefusesWhileEarlierReplayInFlight(
|
|||||||
require.Equal(t, http.StatusSeeOther, first.Code)
|
require.Equal(t, http.StatusSeeOther, first.Code)
|
||||||
require.Equal(
|
require.Equal(
|
||||||
t,
|
t,
|
||||||
"/hook/"+wh.ID+"/events?notice=replay-queued",
|
"/hook/"+wh.ID+"/events?replay=queued",
|
||||||
first.Header().Get("Location"),
|
first.Header().Get("Location"),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -439,7 +439,7 @@ func TestHandleDeliveryReplay_RefusesWhileEarlierReplayInFlight(
|
|||||||
require.Equal(t, http.StatusSeeOther, second.Code)
|
require.Equal(t, http.StatusSeeOther, second.Code)
|
||||||
assert.Equal(
|
assert.Equal(
|
||||||
t,
|
t,
|
||||||
"/hook/"+wh.ID+"/events?notice=replay-in-flight",
|
"/hook/"+wh.ID+"/events?replay=in-flight",
|
||||||
second.Header().Get("Location"),
|
second.Header().Get("Location"),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -465,7 +465,7 @@ func TestHandleDeliveryReplay_RefusesWhileEarlierReplayInFlight(
|
|||||||
require.Equal(t, http.StatusSeeOther, pending.Code)
|
require.Equal(t, http.StatusSeeOther, pending.Code)
|
||||||
assert.Equal(
|
assert.Equal(
|
||||||
t,
|
t,
|
||||||
"/hook/"+wh.ID+"/events?notice=replay-not-terminal",
|
"/hook/"+wh.ID+"/events?replay=not-terminal",
|
||||||
pending.Header().Get("Location"),
|
pending.Header().Get("Location"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -509,7 +509,7 @@ func TestHandleSourceLogs_RendersReplayControlAndBanner(t *testing.T) {
|
|||||||
assert.Contains(t, body, ">Replay<")
|
assert.Contains(t, body, ">Replay<")
|
||||||
|
|
||||||
refused := renderSourceLogsPageWithQuery(
|
refused := renderSourceLogsPageWithQuery(
|
||||||
t, h, sess, wh.ID, "?notice=replay-target-deleted",
|
t, h, sess, wh.ID, "?replay=target-deleted",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert.Contains(t, refused, "alert-error")
|
assert.Contains(t, refused, "alert-error")
|
||||||
@@ -517,7 +517,7 @@ func TestHandleSourceLogs_RendersReplayControlAndBanner(t *testing.T) {
|
|||||||
|
|
||||||
// An outcome code nobody issued renders no banner at all.
|
// An outcome code nobody issued renders no banner at all.
|
||||||
unknown := renderSourceLogsPageWithQuery(
|
unknown := renderSourceLogsPageWithQuery(
|
||||||
t, h, sess, wh.ID, "?notice=made-up",
|
t, h, sess, wh.ID, "?replay=made-up",
|
||||||
)
|
)
|
||||||
|
|
||||||
assert.NotContains(t, unknown, "alert-error")
|
assert.NotContains(t, unknown, "alert-error")
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package handlers
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
"github.com/go-chi/chi"
|
"github.com/go-chi/chi"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@@ -10,19 +11,43 @@ import (
|
|||||||
"sneak.berlin/go/webhooker/internal/database"
|
"sneak.berlin/go/webhooker/internal/database"
|
||||||
)
|
)
|
||||||
|
|
||||||
// The outcomes of a resubmit POST, as the notice codes its redirect
|
// resubmitOutcomeParam is the query parameter the resubmit POST
|
||||||
// carries. noticeFor holds the line each one shows.
|
// redirects with and the event log page reads its banner from.
|
||||||
|
const resubmitOutcomeParam = "resubmit"
|
||||||
|
|
||||||
|
// resubmitOutcomeCode is the outcome of a resubmit POST. The redirect
|
||||||
|
// carries one of these fixed codes rather than a message, so nothing a
|
||||||
|
// client submits can reach the rendered page through it.
|
||||||
|
type resubmitOutcomeCode string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// resubmitQueued reports that a new event was stored and its
|
// resubmitQueued reports that a new event was stored and its
|
||||||
// deliveries handed to the delivery engine.
|
// deliveries handed to the delivery engine.
|
||||||
resubmitQueued noticeCode = "resubmit-queued"
|
resubmitQueued resubmitOutcomeCode = "queued"
|
||||||
|
|
||||||
// resubmitNoTargets reports a source with no active targets. The
|
// resubmitNoTargets reports a source with no active targets. The
|
||||||
// new event is stored either way, exactly as a received event
|
// new event is stored either way, exactly as a received event
|
||||||
// with no targets is.
|
// with no targets is.
|
||||||
resubmitNoTargets noticeCode = "resubmit-no-targets"
|
resubmitNoTargets resubmitOutcomeCode = "no-targets"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// resubmitOutcome returns the banner the event log page shows for an
|
||||||
|
// outcome code, and whether the resubmit was queued. An unrecognised
|
||||||
|
// code yields no banner.
|
||||||
|
func resubmitOutcome(code string) (string, bool) {
|
||||||
|
switch resubmitOutcomeCode(code) {
|
||||||
|
case resubmitQueued:
|
||||||
|
return "Resubmitted: a new event was created from the stored " +
|
||||||
|
"one and queued to every active target.", true
|
||||||
|
case resubmitNoTargets:
|
||||||
|
return "Resubmitted: a new event was created, but this " +
|
||||||
|
"source has no active targets, so nothing was queued.",
|
||||||
|
true
|
||||||
|
default:
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// resubmitSource is the stored event a resubmit copies. Its body is
|
// resubmitSource is the stored event a resubmit copies. Its body is
|
||||||
// read as bytes rather than as a string so the copy is byte-identical
|
// read as bytes rather than as a string so the copy is byte-identical
|
||||||
// to what was received, whatever the payload's encoding.
|
// to what was received, whatever the payload's encoding.
|
||||||
@@ -220,5 +245,29 @@ func (h *Handlers) queueResubmit(
|
|||||||
code = resubmitNoTargets
|
code = resubmitNoTargets
|
||||||
}
|
}
|
||||||
|
|
||||||
redirectToEventLog(w, r, webhook, code)
|
h.finishResubmit(w, r, webhook, code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// finishResubmit redirects back to the event log the resubmit was
|
||||||
|
// triggered from, carrying the outcome code the page turns into a
|
||||||
|
// banner and the page number the form submitted.
|
||||||
|
func (h *Handlers) finishResubmit(
|
||||||
|
w http.ResponseWriter,
|
||||||
|
r *http.Request,
|
||||||
|
webhook database.Webhook,
|
||||||
|
code resubmitOutcomeCode,
|
||||||
|
) {
|
||||||
|
dest := "/hook/" + webhook.ID + "/events?" +
|
||||||
|
resubmitOutcomeParam + "=" + string(code)
|
||||||
|
|
||||||
|
// The page is read from the form rather than the query string:
|
||||||
|
// this is a POST, and its query string is what logs and Referer
|
||||||
|
// headers record.
|
||||||
|
if page := pageOrFirst(
|
||||||
|
r.PostFormValue("page"),
|
||||||
|
); page > 1 {
|
||||||
|
dest += "&page=" + strconv.Itoa(page)
|
||||||
|
}
|
||||||
|
|
||||||
|
http.Redirect(w, r, dest, http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ func TestHandleEventResubmit_DeliversToTargetCreatedAfterTheEvent(
|
|||||||
require.Equal(t, http.StatusSeeOther, w.Code)
|
require.Equal(t, http.StatusSeeOther, w.Code)
|
||||||
assert.Equal(
|
assert.Equal(
|
||||||
t,
|
t,
|
||||||
"/hook/"+wh.ID+"/events?notice=resubmit-queued",
|
"/hook/"+wh.ID+"/events?resubmit=queued",
|
||||||
w.Header().Get("Location"),
|
w.Header().Get("Location"),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -282,7 +282,7 @@ func TestHandleEventResubmit_IsRepeatable(t *testing.T) {
|
|||||||
require.Equal(t, http.StatusSeeOther, w.Code)
|
require.Equal(t, http.StatusSeeOther, w.Code)
|
||||||
assert.Equal(
|
assert.Equal(
|
||||||
t,
|
t,
|
||||||
"/hook/"+wh.ID+"/events?notice=resubmit-queued",
|
"/hook/"+wh.ID+"/events?resubmit=queued",
|
||||||
w.Header().Get("Location"),
|
w.Header().Get("Location"),
|
||||||
"a resubmit must not be refused while an earlier "+
|
"a resubmit must not be refused while an earlier "+
|
||||||
"one is in flight",
|
"one is in flight",
|
||||||
@@ -436,7 +436,7 @@ func TestHandleEventResubmit_SkipsInactiveTarget(t *testing.T) {
|
|||||||
require.Equal(t, http.StatusSeeOther, w.Code)
|
require.Equal(t, http.StatusSeeOther, w.Code)
|
||||||
assert.Equal(
|
assert.Equal(
|
||||||
t,
|
t,
|
||||||
"/hook/"+wh.ID+"/events?notice=resubmit-queued",
|
"/hook/"+wh.ID+"/events?resubmit=queued",
|
||||||
w.Header().Get("Location"),
|
w.Header().Get("Location"),
|
||||||
"an inactive target is skipped, not an error",
|
"an inactive target is skipped, not an error",
|
||||||
)
|
)
|
||||||
@@ -482,7 +482,7 @@ func TestHandleEventResubmit_NoActiveTargetsStillStoresEvent(
|
|||||||
require.Equal(t, http.StatusSeeOther, w.Code)
|
require.Equal(t, http.StatusSeeOther, w.Code)
|
||||||
assert.Equal(
|
assert.Equal(
|
||||||
t,
|
t,
|
||||||
"/hook/"+wh.ID+"/events?notice=resubmit-no-targets",
|
"/hook/"+wh.ID+"/events?resubmit=no-targets",
|
||||||
w.Header().Get("Location"),
|
w.Header().Get("Location"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -97,10 +97,10 @@ type Handlers struct {
|
|||||||
|
|
||||||
// parsePageTemplate parses a page-specific template set from the
|
// parsePageTemplate parses a page-specific template set from the
|
||||||
// embedded FS. Each page template is combined with the shared
|
// embedded FS. Each page template is combined with the shared
|
||||||
// base, htmlheader, navbar and notice templates, and with any further
|
// base, htmlheader, and navbar templates, and with any further files
|
||||||
// files the page includes. The page file must be listed first so that
|
// the page includes. The page file must be listed first so that its
|
||||||
// its root action ({{template "base" .}}) becomes the template set's
|
// root action ({{template "base" .}}) becomes the template set's entry
|
||||||
// entry point.
|
// point.
|
||||||
func parsePageTemplate(
|
func parsePageTemplate(
|
||||||
pageFile string, included ...string,
|
pageFile string, included ...string,
|
||||||
) *template.Template {
|
) *template.Template {
|
||||||
@@ -109,7 +109,6 @@ func parsePageTemplate(
|
|||||||
"base.html",
|
"base.html",
|
||||||
"htmlheader.html",
|
"htmlheader.html",
|
||||||
"navbar.html",
|
"navbar.html",
|
||||||
"notice.html",
|
|
||||||
}, included...)
|
}, included...)
|
||||||
|
|
||||||
return template.Must(
|
return template.Must(
|
||||||
@@ -210,13 +209,11 @@ func (s *Handlers) renderError(
|
|||||||
// served outside the routes where NoCache runs.
|
// served outside the routes where NoCache runs.
|
||||||
w.Header().Set("Cache-Control", "no-store")
|
w.Header().Set("Cache-Control", "no-store")
|
||||||
|
|
||||||
// No notice: one would say an action worked above a page saying
|
|
||||||
// the request failed.
|
|
||||||
data := s.pageData(r, map[string]any{
|
data := s.pageData(r, map[string]any{
|
||||||
"Status": status,
|
"Status": status,
|
||||||
"StatusText": http.StatusText(status),
|
"StatusText": http.StatusText(status),
|
||||||
"Message": errorPageText(status),
|
"Message": errorPageText(status),
|
||||||
}, nil)
|
})
|
||||||
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
|
|
||||||
@@ -270,7 +267,6 @@ type templateDataWrapper struct {
|
|||||||
User *UserInfo
|
User *UserInfo
|
||||||
CSRFToken string
|
CSRFToken string
|
||||||
Version string
|
Version string
|
||||||
Notice *notice
|
|
||||||
Data any
|
Data any
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,15 +311,12 @@ func (s *Handlers) renderTemplate(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s.executeTemplate(w, r, tmpl, s.pageData(r, data, noticeFor(r)))
|
s.executeTemplate(w, r, tmpl, s.pageData(r, data))
|
||||||
}
|
}
|
||||||
|
|
||||||
// pageData adds the fields the shared layout renders to a page's own
|
// pageData adds the fields the shared layout renders to a page's own
|
||||||
// data. The layout shows the notice, when there is one, above the
|
// data.
|
||||||
// page.
|
func (s *Handlers) pageData(r *http.Request, data any) any {
|
||||||
func (s *Handlers) pageData(
|
|
||||||
r *http.Request, data any, pageNotice *notice,
|
|
||||||
) any {
|
|
||||||
userInfo := s.getUserInfo(r)
|
userInfo := s.getUserInfo(r)
|
||||||
csrfToken := middleware.CSRFToken(r)
|
csrfToken := middleware.CSRFToken(r)
|
||||||
|
|
||||||
@@ -337,7 +330,6 @@ func (s *Handlers) pageData(
|
|||||||
m["User"] = userInfo
|
m["User"] = userInfo
|
||||||
m["CSRFToken"] = csrfToken
|
m["CSRFToken"] = csrfToken
|
||||||
m["Version"] = version
|
m["Version"] = version
|
||||||
m["Notice"] = pageNotice
|
|
||||||
|
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
@@ -346,7 +338,6 @@ func (s *Handlers) pageData(
|
|||||||
User: userInfo,
|
User: userInfo,
|
||||||
CSRFToken: csrfToken,
|
CSRFToken: csrfToken,
|
||||||
Version: version,
|
Version: version,
|
||||||
Notice: pageNotice,
|
|
||||||
Data: data,
|
Data: data,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,109 +0,0 @@
|
|||||||
package handlers
|
|
||||||
|
|
||||||
import "net/http"
|
|
||||||
|
|
||||||
// noticeParam is the query parameter an action's redirect carries its
|
|
||||||
// notice code in.
|
|
||||||
const noticeParam = "notice"
|
|
||||||
|
|
||||||
// noticeCode names one of the fixed lines noticeFor knows. An action
|
|
||||||
// redirects with the code rather than the line, so nothing a client
|
|
||||||
// puts in the URL reaches the page: a code noticeFor does not know
|
|
||||||
// shows nothing.
|
|
||||||
type noticeCode string
|
|
||||||
|
|
||||||
// The codes of the actions on the webhook pages and of signing out.
|
|
||||||
// Replay's codes, with the reasons a replay can be refused, and
|
|
||||||
// resubmit's codes are defined beside those actions.
|
|
||||||
const (
|
|
||||||
webhookCreated noticeCode = "webhook-created"
|
|
||||||
webhookSaved noticeCode = "webhook-saved"
|
|
||||||
webhookDeleted noticeCode = "webhook-deleted"
|
|
||||||
entrypointAdded noticeCode = "entrypoint-added"
|
|
||||||
entrypointDeleted noticeCode = "entrypoint-deleted"
|
|
||||||
entrypointActivated noticeCode = "entrypoint-activated"
|
|
||||||
entrypointDeactivated noticeCode = "entrypoint-deactivated"
|
|
||||||
targetAdded noticeCode = "target-added"
|
|
||||||
targetSaved noticeCode = "target-saved"
|
|
||||||
targetDeleted noticeCode = "target-deleted"
|
|
||||||
targetActivated noticeCode = "target-activated"
|
|
||||||
targetDeactivated noticeCode = "target-deactivated"
|
|
||||||
signedOut noticeCode = "signed-out"
|
|
||||||
)
|
|
||||||
|
|
||||||
// notice is the line templates/notice.html shows above a page to say
|
|
||||||
// what an action did.
|
|
||||||
type notice struct {
|
|
||||||
Text string
|
|
||||||
|
|
||||||
// Failed shows the line as an error: the action was refused.
|
|
||||||
Failed bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// noticeFor returns the notice the request's URL names, or nil when it
|
|
||||||
// names none or an unknown code.
|
|
||||||
func noticeFor(r *http.Request) *notice {
|
|
||||||
n, ok := map[noticeCode]notice{
|
|
||||||
webhookCreated: {Text: "Webhook created."},
|
|
||||||
webhookSaved: {Text: "Webhook saved."},
|
|
||||||
webhookDeleted: {Text: "Webhook deleted."},
|
|
||||||
entrypointAdded: {Text: "Entrypoint added."},
|
|
||||||
entrypointDeleted: {Text: "Entrypoint deleted."},
|
|
||||||
entrypointActivated: {Text: "Entrypoint activated."},
|
|
||||||
entrypointDeactivated: {Text: "Entrypoint deactivated."},
|
|
||||||
targetAdded: {Text: "Target added."},
|
|
||||||
targetSaved: {Text: "Target saved."},
|
|
||||||
targetDeleted: {Text: "Target deleted."},
|
|
||||||
targetActivated: {Text: "Target activated."},
|
|
||||||
targetDeactivated: {Text: "Target deactivated."},
|
|
||||||
signedOut: {Text: "Signed out."},
|
|
||||||
|
|
||||||
replayQueued: {
|
|
||||||
Text: "Replay queued: a new delivery was created " +
|
|
||||||
"against the target's current configuration.",
|
|
||||||
},
|
|
||||||
replayTargetDeleted: {
|
|
||||||
Text: "Not replayed: the target this delivery was for " +
|
|
||||||
"has been deleted. Recreate the target, then replay.",
|
|
||||||
Failed: true,
|
|
||||||
},
|
|
||||||
replayTargetMissing: {
|
|
||||||
Text: "Not replayed: the target this delivery was for " +
|
|
||||||
"no longer exists.",
|
|
||||||
Failed: true,
|
|
||||||
},
|
|
||||||
replayTargetInactive: {
|
|
||||||
Text: "Not replayed: the target this delivery was for " +
|
|
||||||
"is deactivated. Activate it, then replay.",
|
|
||||||
Failed: true,
|
|
||||||
},
|
|
||||||
replayNotTerminal: {
|
|
||||||
Text: "Not replayed: this delivery has not finished yet.",
|
|
||||||
Failed: true,
|
|
||||||
},
|
|
||||||
replayInFlight: {
|
|
||||||
Text: "Not replayed: a delivery of this event to this " +
|
|
||||||
"target is already in flight.",
|
|
||||||
Failed: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
resubmitQueued: {
|
|
||||||
Text: "Resubmitted: a new event was created from the " +
|
|
||||||
"stored one and queued to every active target.",
|
|
||||||
},
|
|
||||||
resubmitNoTargets: {
|
|
||||||
Text: "Resubmitted: a new event was created, but this " +
|
|
||||||
"source has no active targets, so nothing was queued.",
|
|
||||||
},
|
|
||||||
}[noticeCode(r.URL.Query().Get(noticeParam))]
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return &n
|
|
||||||
}
|
|
||||||
|
|
||||||
// withNotice returns path with code added as its notice.
|
|
||||||
func withNotice(path string, code noticeCode) string {
|
|
||||||
return path + "?" + noticeParam + "=" + string(code)
|
|
||||||
}
|
|
||||||
@@ -411,9 +411,7 @@ func TestHandleSourceDelete_RemovesConfigAndEventDatabase(
|
|||||||
h.HandleSourceDelete().ServeHTTP(w, req)
|
h.HandleSourceDelete().ServeHTTP(w, req)
|
||||||
|
|
||||||
require.Equal(t, http.StatusSeeOther, w.Code)
|
require.Equal(t, http.StatusSeeOther, w.Code)
|
||||||
assert.Equal(
|
assert.Equal(t, "/hooks", w.Header().Get("Location"))
|
||||||
t, "/hooks?notice=webhook-deleted", w.Header().Get("Location"),
|
|
||||||
)
|
|
||||||
|
|
||||||
assert.Equal(
|
assert.Equal(
|
||||||
t, int64(0),
|
t, int64(0),
|
||||||
|
|||||||
@@ -322,8 +322,7 @@ func (h *Handlers) createWebhookWithEntrypoint(
|
|||||||
)
|
)
|
||||||
|
|
||||||
http.Redirect(
|
http.Redirect(
|
||||||
w, r, withNotice("/hook/"+webhook.ID, webhookCreated),
|
w, r, "/hook/"+webhook.ID, http.StatusSeeOther,
|
||||||
http.StatusSeeOther,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -580,8 +579,7 @@ func (h *Handlers) applyWebhookEdit(
|
|||||||
}
|
}
|
||||||
|
|
||||||
http.Redirect(
|
http.Redirect(
|
||||||
w, r, withNotice("/hook/"+webhook.ID, webhookSaved),
|
w, r, "/hook/"+webhook.ID, http.StatusSeeOther,
|
||||||
http.StatusSeeOther,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -664,9 +662,7 @@ func (h *Handlers) deleteWebhookResources(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
http.Redirect(
|
http.Redirect(w, r, "/hooks", http.StatusSeeOther)
|
||||||
w, r, withNotice("/hooks", webhookDeleted), http.StatusSeeOther,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// commitWebhookDeletion soft-deletes a webhook's entrypoints,
|
// commitWebhookDeletion soft-deletes a webhook's entrypoints,
|
||||||
@@ -845,16 +841,31 @@ func (h *Handlers) HandleSourceLogs() http.HandlerFunc {
|
|||||||
totalPages++
|
totalPages++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The banner a replay or resubmit POST redirected back
|
||||||
|
// with. The message comes from a fixed set keyed by the
|
||||||
|
// outcome code, never from the query string itself.
|
||||||
|
replayMsg, replayOK := replayOutcome(
|
||||||
|
r.URL.Query().Get(replayOutcomeParam),
|
||||||
|
)
|
||||||
|
|
||||||
|
resubmitMsg, resubmitOK := resubmitOutcome(
|
||||||
|
r.URL.Query().Get(resubmitOutcomeParam),
|
||||||
|
)
|
||||||
|
|
||||||
data := map[string]any{
|
data := map[string]any{
|
||||||
tmplKeyWebhook: &webhook,
|
tmplKeyWebhook: &webhook,
|
||||||
"Events": evts,
|
"Events": evts,
|
||||||
"Page": page,
|
"ReplayMessage": replayMsg,
|
||||||
"TotalPages": totalPages,
|
"ReplayQueued": replayOK,
|
||||||
"TotalEvents": total,
|
"ResubmitMessage": resubmitMsg,
|
||||||
"HasPrev": page > 1,
|
"ResubmitQueued": resubmitOK,
|
||||||
"HasNext": page < totalPages,
|
"Page": page,
|
||||||
"PrevPage": page - 1,
|
"TotalPages": totalPages,
|
||||||
"NextPage": page + 1,
|
"TotalEvents": total,
|
||||||
|
"HasPrev": page > 1,
|
||||||
|
"HasNext": page < totalPages,
|
||||||
|
"PrevPage": page - 1,
|
||||||
|
"NextPage": page + 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
h.renderTemplate(w, r, "source_logs.html", data)
|
h.renderTemplate(w, r, "source_logs.html", data)
|
||||||
@@ -1243,8 +1254,7 @@ func (h *Handlers) HandleEntrypointCreate() http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
http.Redirect(
|
http.Redirect(
|
||||||
w, r, withNotice("/hook/"+webhook.ID, entrypointAdded),
|
w, r, "/hook/"+webhook.ID, http.StatusSeeOther,
|
||||||
http.StatusSeeOther,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1355,8 +1365,7 @@ func (h *Handlers) processTargetCreate(
|
|||||||
}
|
}
|
||||||
|
|
||||||
http.Redirect(
|
http.Redirect(
|
||||||
w, r, withNotice("/hook/"+webhook.ID, targetAdded),
|
w, r, "/hook/"+webhook.ID, http.StatusSeeOther,
|
||||||
http.StatusSeeOther,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1561,8 +1570,8 @@ func (h *Handlers) validateTargetURL(
|
|||||||
|
|
||||||
// Only a private or reserved address's refusal says how
|
// Only a private or reserved address's refusal says how
|
||||||
// to allow it. Other refusals never do: link-local, the
|
// to allow it. Other refusals never do: link-local, the
|
||||||
// unspecified addresses and the unconditional metadata
|
// unspecified addresses and the other unconditional
|
||||||
// addresses cannot be opened, and the default
|
// metadata addresses cannot be opened, and the default
|
||||||
// blocklist's public addresses, which listing does open,
|
// blocklist's public addresses, which listing does open,
|
||||||
// hand out credentials.
|
// hand out credentials.
|
||||||
if errors.Is(err, delivery.ErrBlockedPrivateOrReservedIP) {
|
if errors.Is(err, delivery.ErrBlockedPrivateOrReservedIP) {
|
||||||
@@ -1635,7 +1644,6 @@ func (h *Handlers) HandleEntrypointDelete() http.HandlerFunc {
|
|||||||
"entrypointID", &database.Entrypoint{},
|
"entrypointID", &database.Entrypoint{},
|
||||||
"failed to delete entrypoint",
|
"failed to delete entrypoint",
|
||||||
nil,
|
nil,
|
||||||
entrypointDeleted,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1648,21 +1656,18 @@ func (h *Handlers) HandleTargetDelete() http.HandlerFunc {
|
|||||||
"targetID", &database.Target{},
|
"targetID", &database.Target{},
|
||||||
"failed to delete target",
|
"failed to delete target",
|
||||||
h.evictArchiveWriterIfUnused,
|
h.evictArchiveWriterIfUnused,
|
||||||
targetDeleted,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// deleteChildResource returns a handler that deletes a child
|
// deleteChildResource returns a handler that deletes a child
|
||||||
// resource (entrypoint or target) belonging to a webhook. The
|
// resource (entrypoint or target) belonging to a webhook. The
|
||||||
// optional afterDelete hook runs with the webhook's id once the
|
// optional afterDelete hook runs with the webhook's id once the
|
||||||
// delete has succeeded, before the redirect, which carries done as
|
// delete has succeeded, before the redirect.
|
||||||
// its notice.
|
|
||||||
func (h *Handlers) deleteChildResource(
|
func (h *Handlers) deleteChildResource(
|
||||||
idParam string,
|
idParam string,
|
||||||
model any,
|
model any,
|
||||||
errMsg string,
|
errMsg string,
|
||||||
afterDelete func(webhookID string),
|
afterDelete func(webhookID string),
|
||||||
done noticeCode,
|
|
||||||
) http.HandlerFunc {
|
) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
userID, ok := h.getUserID(r)
|
userID, ok := h.getUserID(r)
|
||||||
@@ -1704,7 +1709,7 @@ func (h *Handlers) deleteChildResource(
|
|||||||
|
|
||||||
http.Redirect(
|
http.Redirect(
|
||||||
w, r,
|
w, r,
|
||||||
withNotice("/hook/"+webhook.ID, done),
|
"/hook/"+webhook.ID,
|
||||||
http.StatusSeeOther,
|
http.StatusSeeOther,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1715,7 +1720,7 @@ func (h *Handlers) deleteChildResource(
|
|||||||
func (h *Handlers) HandleEntrypointToggle() http.HandlerFunc {
|
func (h *Handlers) HandleEntrypointToggle() http.HandlerFunc {
|
||||||
return h.toggleChildResource(
|
return h.toggleChildResource(
|
||||||
"entrypointID",
|
"entrypointID",
|
||||||
func(webhookID, childID string) (bool, error) {
|
func(webhookID, childID string) error {
|
||||||
var ep database.Entrypoint
|
var ep database.Entrypoint
|
||||||
|
|
||||||
err := h.db.DB().Where(
|
err := h.db.DB().Where(
|
||||||
@@ -1723,15 +1728,14 @@ func (h *Handlers) HandleEntrypointToggle() http.HandlerFunc {
|
|||||||
childID, webhookID,
|
childID, webhookID,
|
||||||
).First(&ep).Error
|
).First(&ep).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
ep.Active = !ep.Active
|
ep.Active = !ep.Active
|
||||||
|
|
||||||
return ep.Active, h.db.DB().Save(&ep).Error
|
return h.db.DB().Save(&ep).Error
|
||||||
},
|
},
|
||||||
"failed to toggle entrypoint",
|
"failed to toggle entrypoint",
|
||||||
entrypointActivated, entrypointDeactivated,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1739,7 +1743,7 @@ func (h *Handlers) HandleEntrypointToggle() http.HandlerFunc {
|
|||||||
func (h *Handlers) HandleTargetToggle() http.HandlerFunc {
|
func (h *Handlers) HandleTargetToggle() http.HandlerFunc {
|
||||||
return h.toggleChildResource(
|
return h.toggleChildResource(
|
||||||
"targetID",
|
"targetID",
|
||||||
func(webhookID, childID string) (bool, error) {
|
func(webhookID, childID string) error {
|
||||||
var tgt database.Target
|
var tgt database.Target
|
||||||
|
|
||||||
err := h.db.DB().Where(
|
err := h.db.DB().Where(
|
||||||
@@ -1747,27 +1751,23 @@ func (h *Handlers) HandleTargetToggle() http.HandlerFunc {
|
|||||||
childID, webhookID,
|
childID, webhookID,
|
||||||
).First(&tgt).Error
|
).First(&tgt).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
tgt.Active = !tgt.Active
|
tgt.Active = !tgt.Active
|
||||||
|
|
||||||
return tgt.Active, h.db.DB().Save(&tgt).Error
|
return h.db.DB().Save(&tgt).Error
|
||||||
},
|
},
|
||||||
"failed to toggle target",
|
"failed to toggle target",
|
||||||
targetActivated, targetDeactivated,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// toggleChildResource returns a handler that toggles the active
|
// toggleChildResource returns a handler that toggles the active
|
||||||
// state of a child resource belonging to a webhook. toggleFn returns
|
// state of a child resource belonging to a webhook.
|
||||||
// the new state, and the redirect carries activated or deactivated as
|
|
||||||
// its notice to match.
|
|
||||||
func (h *Handlers) toggleChildResource(
|
func (h *Handlers) toggleChildResource(
|
||||||
idParam string,
|
idParam string,
|
||||||
toggleFn func(webhookID, childID string) (bool, error),
|
toggleFn func(webhookID, childID string) error,
|
||||||
errMsg string,
|
errMsg string,
|
||||||
activated, deactivated noticeCode,
|
|
||||||
) http.HandlerFunc {
|
) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
userID, ok := h.getUserID(r)
|
userID, ok := h.getUserID(r)
|
||||||
@@ -1793,21 +1793,16 @@ func (h *Handlers) toggleChildResource(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
active, err := toggleFn(webhook.ID, childID)
|
err = toggleFn(webhook.ID, childID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.serverError(w, r, errMsg, err)
|
h.serverError(w, r, errMsg, err)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
done := deactivated
|
|
||||||
if active {
|
|
||||||
done = activated
|
|
||||||
}
|
|
||||||
|
|
||||||
http.Redirect(
|
http.Redirect(
|
||||||
w, r,
|
w, r,
|
||||||
withNotice("/hook/"+webhook.ID, done),
|
"/hook/"+webhook.ID,
|
||||||
http.StatusSeeOther,
|
http.StatusSeeOther,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -161,8 +161,7 @@ func (h *Handlers) applyTargetEdit(
|
|||||||
}
|
}
|
||||||
|
|
||||||
http.Redirect(
|
http.Redirect(
|
||||||
w, r, withNotice("/hook/"+webhook.ID, targetSaved),
|
w, r, "/hook/"+webhook.ID, http.StatusSeeOther,
|
||||||
http.StatusSeeOther,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -83,22 +83,6 @@ func TestErrorPage_DeletedTarget(t *testing.T) {
|
|||||||
assertErrorPage(t, w, http.StatusNotFound, backToWebhooks)
|
assertErrorPage(t, w, http.StatusNotFound, backToWebhooks)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestErrorPage_ShowsNoNotice pins that a notice code in the URL of a
|
|
||||||
// page that fails is not shown above the error.
|
|
||||||
func TestErrorPage_ShowsNoNotice(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
env := newTestEnv(t)
|
|
||||||
|
|
||||||
userID, _ := env.seedUser(t, "owner", "somepassword")
|
|
||||||
cookies := env.authCookies(t, userID, "owner")
|
|
||||||
|
|
||||||
w := env.get("/hook/no-such-webhook?notice=webhook-saved", cookies)
|
|
||||||
|
|
||||||
assertErrorPage(t, w, http.StatusNotFound, backToWebhooks)
|
|
||||||
assert.NotContains(t, w.Body.String(), "Webhook saved.")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestErrorPage_UnknownPath(t *testing.T) {
|
func TestErrorPage_UnknownPath(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@@ -263,24 +263,6 @@ func (e *testEnv) urlFrom(
|
|||||||
return html.UnescapeString(match[1])
|
return html.UnescapeString(match[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
// requireNotice requires w to redirect to dest carrying the notice
|
|
||||||
// code, then renders that page and requires it to show text.
|
|
||||||
func (e *testEnv) requireNotice(
|
|
||||||
t *testing.T,
|
|
||||||
w *httptest.ResponseRecorder,
|
|
||||||
dest, code, text string,
|
|
||||||
cookies []*http.Cookie,
|
|
||||||
) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
require.Equal(t, http.StatusSeeOther, w.Code)
|
|
||||||
require.Equal(t, dest+"?notice="+code, w.Header().Get("Location"))
|
|
||||||
|
|
||||||
page := e.get(w.Header().Get("Location"), cookies)
|
|
||||||
require.Equal(t, http.StatusOK, page.Code)
|
|
||||||
assert.Contains(t, page.Body.String(), text)
|
|
||||||
}
|
|
||||||
|
|
||||||
// authCookies forges an authenticated session for the given user.
|
// authCookies forges an authenticated session for the given user.
|
||||||
func (e *testEnv) authCookies(
|
func (e *testEnv) authCookies(
|
||||||
t *testing.T,
|
t *testing.T,
|
||||||
@@ -759,31 +741,6 @@ func TestPagesLogin_ReturnsToTheRequestedPage(t *testing.T) {
|
|||||||
assert.Equal(t, asked, w.Header().Get("Location"))
|
assert.Equal(t, asked, w.Header().Get("Location"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestPagesLogout_SaysSignedOut signs out with the navbar's form and
|
|
||||||
// lands on the sign-in page, which says so.
|
|
||||||
func TestPagesLogout_SaysSignedOut(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
env := newTestEnv(t)
|
|
||||||
|
|
||||||
userID, _ := env.seedUser(t, "leaver", "somepassword")
|
|
||||||
token, cookies := env.csrfFrom(
|
|
||||||
t, "/hooks", env.authCookies(t, userID, "leaver"),
|
|
||||||
)
|
|
||||||
|
|
||||||
form := url.Values{}
|
|
||||||
form.Set("csrf_token", token)
|
|
||||||
|
|
||||||
w := env.post(
|
|
||||||
env.urlFrom(t, "/hooks", `action="(/pages/logout)"`, cookies),
|
|
||||||
form, cookies,
|
|
||||||
)
|
|
||||||
|
|
||||||
// The sign-in page is requested without the session cookie, which
|
|
||||||
// the logout told the browser to delete.
|
|
||||||
env.requireNotice(t, w, "/pages/login", "signed-out", "Signed out.", nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- /user/{username} group ---
|
// --- /user/{username} group ---
|
||||||
|
|
||||||
// TestPasswordChange_OversizeBody_RejectedAndPasswordUnchanged
|
// TestPasswordChange_OversizeBody_RejectedAndPasswordUnchanged
|
||||||
@@ -901,9 +858,9 @@ func TestHooks_ListAndNewWebhookForm(t *testing.T) {
|
|||||||
require.NoError(t,
|
require.NoError(t,
|
||||||
env.db.DB().Where("name = ?", "created").First(&created).Error,
|
env.db.DB().Where("name = ?", "created").First(&created).Error,
|
||||||
)
|
)
|
||||||
env.requireNotice(
|
assert.Equal(
|
||||||
t, w, "/hook/"+created.ID, "webhook-created", "Webhook created.",
|
t, "/hook/"+created.ID, w.Header().Get("Location"),
|
||||||
cookies,
|
"creating a webhook should redirect to its page",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -934,7 +891,8 @@ func TestHook_EditFormAndDelete(t *testing.T) {
|
|||||||
env.urlFrom(t, editPage, `action="(/hook/[^/"]+/edit)"`, cookies),
|
env.urlFrom(t, editPage, `action="(/hook/[^/"]+/edit)"`, cookies),
|
||||||
form, cookies,
|
form, cookies,
|
||||||
)
|
)
|
||||||
env.requireNotice(t, w, page, "webhook-saved", "Webhook saved.", cookies)
|
require.Equal(t, http.StatusSeeOther, w.Code)
|
||||||
|
assert.Equal(t, page, w.Header().Get("Location"))
|
||||||
|
|
||||||
var edited database.Webhook
|
var edited database.Webhook
|
||||||
|
|
||||||
@@ -948,17 +906,16 @@ func TestHook_EditFormAndDelete(t *testing.T) {
|
|||||||
env.urlFrom(t, page, `action="(/hook/[^/"]+/delete)"`, cookies),
|
env.urlFrom(t, page, `action="(/hook/[^/"]+/delete)"`, cookies),
|
||||||
form, cookies,
|
form, cookies,
|
||||||
)
|
)
|
||||||
env.requireNotice(
|
require.Equal(t, http.StatusSeeOther, w.Code)
|
||||||
t, w, "/hooks", "webhook-deleted", "Webhook deleted.", cookies,
|
assert.Equal(t, "/hooks", w.Header().Get("Location"))
|
||||||
)
|
|
||||||
assert.Equal(
|
assert.Equal(
|
||||||
t, http.StatusNotFound, env.get(page, cookies).Code,
|
t, http.StatusNotFound, env.get(page, cookies).Code,
|
||||||
"a deleted webhook's page should be gone",
|
"a deleted webhook's page should be gone",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestHook_EntrypointActions adds, deactivates, activates and deletes
|
// TestHook_EntrypointActions adds, deactivates and deletes an
|
||||||
// an entrypoint with the forms on the webhook page, each submitted to
|
// entrypoint with the forms on the webhook page, each submitted to
|
||||||
// the action and with the token the page rendered.
|
// the action and with the token the page rendered.
|
||||||
func TestHook_EntrypointActions(t *testing.T) {
|
func TestHook_EntrypointActions(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
@@ -976,19 +933,16 @@ func TestHook_EntrypointActions(t *testing.T) {
|
|||||||
form.Set("csrf_token", token)
|
form.Set("csrf_token", token)
|
||||||
|
|
||||||
// submit posts the webhook page's form whose action pattern
|
// submit posts the webhook page's form whose action pattern
|
||||||
// captures, and requires the redirect back to that page with the
|
// captures, and requires the redirect back to that page.
|
||||||
// notice code, and the page to show text.
|
submit := func(pattern string) {
|
||||||
submit := func(pattern, code, text string) {
|
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
w := env.post(env.urlFrom(t, page, pattern, cookies), form, cookies)
|
w := env.post(env.urlFrom(t, page, pattern, cookies), form, cookies)
|
||||||
env.requireNotice(t, w, page, code, text, cookies)
|
require.Equal(t, http.StatusSeeOther, w.Code)
|
||||||
|
require.Equal(t, page, w.Header().Get("Location"))
|
||||||
}
|
}
|
||||||
|
|
||||||
toggle := `action="(/hook/[^/"]+/entrypoints/[^/"]+/toggle)"`
|
submit(`action="(/hook/[^/"]+/entrypoints)"`)
|
||||||
|
|
||||||
submit(`action="(/hook/[^/"]+/entrypoints)"`,
|
|
||||||
"entrypoint-added", "Entrypoint added.")
|
|
||||||
|
|
||||||
var added database.Entrypoint
|
var added database.Entrypoint
|
||||||
|
|
||||||
@@ -997,7 +951,7 @@ func TestHook_EntrypointActions(t *testing.T) {
|
|||||||
)
|
)
|
||||||
require.True(t, added.Active)
|
require.True(t, added.Active)
|
||||||
|
|
||||||
submit(toggle, "entrypoint-deactivated", "Entrypoint deactivated.")
|
submit(`action="(/hook/[^/"]+/entrypoints/[^/"]+/toggle)"`)
|
||||||
|
|
||||||
var toggled database.Entrypoint
|
var toggled database.Entrypoint
|
||||||
|
|
||||||
@@ -1006,10 +960,7 @@ func TestHook_EntrypointActions(t *testing.T) {
|
|||||||
)
|
)
|
||||||
assert.False(t, toggled.Active, "the toggle should deactivate it")
|
assert.False(t, toggled.Active, "the toggle should deactivate it")
|
||||||
|
|
||||||
submit(toggle, "entrypoint-activated", "Entrypoint activated.")
|
submit(`action="(/hook/[^/"]+/entrypoints/[^/"]+/delete)"`)
|
||||||
|
|
||||||
submit(`action="(/hook/[^/"]+/entrypoints/[^/"]+/delete)"`,
|
|
||||||
"entrypoint-deleted", "Entrypoint deleted.")
|
|
||||||
|
|
||||||
var left int64
|
var left int64
|
||||||
|
|
||||||
@@ -1020,8 +971,8 @@ func TestHook_EntrypointActions(t *testing.T) {
|
|||||||
|
|
||||||
// TestHook_TargetActions adds a target with the form on the webhook
|
// TestHook_TargetActions adds a target with the form on the webhook
|
||||||
// page, follows its Edit link to the target edit form and submits
|
// page, follows its Edit link to the target edit form and submits
|
||||||
// it, then deactivates, activates and deletes it, every URL and token
|
// it, then deactivates and deletes it, every URL and token taken from
|
||||||
// taken from the rendered pages.
|
// the rendered pages.
|
||||||
func TestHook_TargetActions(t *testing.T) {
|
func TestHook_TargetActions(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -1036,29 +987,27 @@ func TestHook_TargetActions(t *testing.T) {
|
|||||||
|
|
||||||
// submit posts form, with the token, to the action pattern
|
// submit posts form, with the token, to the action pattern
|
||||||
// captures on the page at from, and requires the redirect back to
|
// captures on the page at from, and requires the redirect back to
|
||||||
// the webhook page with the notice code, and that page to show
|
// the webhook page.
|
||||||
// text.
|
submit := func(from, pattern string, form url.Values) {
|
||||||
submit := func(from, pattern string, form url.Values, code, text string) {
|
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
form.Set("csrf_token", token)
|
form.Set("csrf_token", token)
|
||||||
|
|
||||||
w := env.post(env.urlFrom(t, from, pattern, cookies), form, cookies)
|
w := env.post(env.urlFrom(t, from, pattern, cookies), form, cookies)
|
||||||
env.requireNotice(t, w, page, code, text, cookies)
|
require.Equal(t, http.StatusSeeOther, w.Code)
|
||||||
|
require.Equal(t, page, w.Header().Get("Location"))
|
||||||
}
|
}
|
||||||
|
|
||||||
toggle := `action="(/hook/[^/"]+/targets/[^/"]+/toggle)"`
|
|
||||||
|
|
||||||
submit(page, `action="(/hook/[^/"]+/targets)"`, url.Values{
|
submit(page, `action="(/hook/[^/"]+/targets)"`, url.Values{
|
||||||
"name": {"added"},
|
"name": {"added"},
|
||||||
"type": {string(database.TargetTypeLog)},
|
"type": {string(database.TargetTypeLog)},
|
||||||
}, "target-added", "Target added.")
|
})
|
||||||
|
|
||||||
editPage := env.urlFrom(
|
editPage := env.urlFrom(
|
||||||
t, page, `href="(/hook/[^/"]+/targets/[^/"]+/edit)"`, cookies,
|
t, page, `href="(/hook/[^/"]+/targets/[^/"]+/edit)"`, cookies,
|
||||||
)
|
)
|
||||||
submit(editPage, `action="(/hook/[^/"]+/targets/[^/"]+/edit)"`,
|
submit(editPage, `action="(/hook/[^/"]+/targets/[^/"]+/edit)"`,
|
||||||
url.Values{"name": {"renamed"}}, "target-saved", "Target saved.")
|
url.Values{"name": {"renamed"}})
|
||||||
|
|
||||||
var edited database.Target
|
var edited database.Target
|
||||||
|
|
||||||
@@ -1068,8 +1017,8 @@ func TestHook_TargetActions(t *testing.T) {
|
|||||||
assert.Equal(t, "renamed", edited.Name)
|
assert.Equal(t, "renamed", edited.Name)
|
||||||
require.True(t, edited.Active)
|
require.True(t, edited.Active)
|
||||||
|
|
||||||
submit(page, toggle, url.Values{},
|
submit(page, `action="(/hook/[^/"]+/targets/[^/"]+/toggle)"`,
|
||||||
"target-deactivated", "Target deactivated.")
|
url.Values{})
|
||||||
|
|
||||||
var toggled database.Target
|
var toggled database.Target
|
||||||
|
|
||||||
@@ -1078,11 +1027,8 @@ func TestHook_TargetActions(t *testing.T) {
|
|||||||
)
|
)
|
||||||
assert.False(t, toggled.Active, "the toggle should deactivate it")
|
assert.False(t, toggled.Active, "the toggle should deactivate it")
|
||||||
|
|
||||||
submit(page, toggle, url.Values{},
|
|
||||||
"target-activated", "Target activated.")
|
|
||||||
|
|
||||||
submit(page, `action="(/hook/[^/"]+/targets/[^/"]+/delete)"`,
|
submit(page, `action="(/hook/[^/"]+/targets/[^/"]+/delete)"`,
|
||||||
url.Values{}, "target-deleted", "Target deleted.")
|
url.Values{})
|
||||||
|
|
||||||
var left int64
|
var left int64
|
||||||
|
|
||||||
@@ -1118,9 +1064,9 @@ func TestHook_ResubmitFromEventLog(t *testing.T) {
|
|||||||
env.urlFrom(t, logsPath, `action="(/hook/[^"]+/resubmit)"`, cookies),
|
env.urlFrom(t, logsPath, `action="(/hook/[^"]+/resubmit)"`, cookies),
|
||||||
form, cookies,
|
form, cookies,
|
||||||
)
|
)
|
||||||
env.requireNotice(
|
require.Equal(t, http.StatusSeeOther, w.Code)
|
||||||
t, w, logsPath, "resubmit-no-targets",
|
assert.Equal(
|
||||||
"this source has no active targets", cookies,
|
t, logsPath+"?resubmit=no-targets", w.Header().Get("Location"),
|
||||||
)
|
)
|
||||||
|
|
||||||
webhookDB, err := env.dbMgr.GetDB(wh.ID)
|
webhookDB, err := env.dbMgr.GetDB(wh.ID)
|
||||||
@@ -1356,8 +1302,10 @@ func TestDeliveryReplay_PostOnlyAndCSRFProtected(t *testing.T) {
|
|||||||
html.UnescapeString(action[1]), form, cookies,
|
html.UnescapeString(action[1]), form, cookies,
|
||||||
)
|
)
|
||||||
|
|
||||||
env.requireNotice(
|
require.Equal(t, http.StatusSeeOther, w.Code)
|
||||||
t, w, logsPath, "replay-queued", "Replay queued:", cookies,
|
assert.Equal(
|
||||||
|
t, logsPath+"?replay=queued",
|
||||||
|
w.Header().Get("Location"),
|
||||||
)
|
)
|
||||||
assert.Equal(
|
assert.Equal(
|
||||||
t, int64(2), env.countDeliveries(t, wh.ID),
|
t, int64(2), env.countDeliveries(t, wh.ID),
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
<body class="bg-gray-50 min-h-screen flex flex-col">
|
<body class="bg-gray-50 min-h-screen flex flex-col">
|
||||||
<div class="flex-grow">
|
<div class="flex-grow">
|
||||||
{{template "navbar" .}}
|
{{template "navbar" .}}
|
||||||
{{template "notice" .}}
|
|
||||||
{{block "content" .}}{{end}}
|
{{block "content" .}}{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{template "footer" .}}
|
{{template "footer" .}}
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
{{define "notice"}}
|
|
||||||
{{with .Notice}}
|
|
||||||
<div class="max-w-6xl mx-auto px-6 pt-4">
|
|
||||||
<div class="{{if .Failed}}alert-error{{else}}alert-success{{end}}">{{.Text}}</div>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
{{end}}
|
|
||||||
@@ -12,6 +12,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{if .ReplayMessage}}
|
||||||
|
<div class="{{if .ReplayQueued}}alert-success{{else}}alert-error{{end}}">{{.ReplayMessage}}</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if .ResubmitMessage}}
|
||||||
|
<div class="{{if .ResubmitQueued}}alert-success{{else}}alert-error{{end}}">{{.ResubmitMessage}}</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="divide-y divide-gray-100">
|
<div class="divide-y divide-gray-100">
|
||||||
{{range .Events}}
|
{{range .Events}}
|
||||||
|
|||||||
Reference in New Issue
Block a user