Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,12 @@ jobs:
- name: D-0815 forgejo and file-processing pass the whole gate
run: bash tests/conformance/d0815-test.sh

- name: D-0973 the tarball build prunes the BuildKit cache on CI only
run: bash tests/conformance/d0973-test.sh

- name: D-0821 workflows manifest port matches the port it listens on
run: bash tests/conformance/d0821-test.sh

- name: Released CLI v1.4.12 oracle
run: bash scripts/conformance/v14-oracle.sh --fixtures

Expand Down
6 changes: 3 additions & 3 deletions free/audit-log/internal/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import (
// RegisterRoutes mounts all audit-log endpoints on the given router.
//
// - secret is the value of PLUGIN_INTERNAL_SECRET; all /events and
// /admin/events endpoints require a matching X-Plugin-Secret header, except
// /admin/events which accepts either X-Plugin-Secret or X-Hasura-Admin-Secret.
// /admin/events endpoints require a matching X-Internal-Token header, except
// /admin/events which accepts either X-Internal-Token or X-Hasura-Admin-Secret.
// - adminSecret is the value of HASURA_GRAPHQL_ADMIN_SECRET; when non-empty
// GET /admin/events also accepts requests carrying this header.
func RegisterRoutes(r chi.Router, pool *pgxpool.Pool, secret, adminSecret string) {
Expand Down Expand Up @@ -70,4 +70,4 @@ func handleHealth(pool *pgxpool.Pool) http.HandlerFunc {

// handleIngest handles POST /events.
// This endpoint is for internal use only (e.g. other plugins, the CLI, Admin).
// All requests must include a matching X-Plugin-Secret header.
// All requests must include a matching X-Internal-Token header.
9 changes: 4 additions & 5 deletions free/audit-log/internal/handlers_admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ func handleAdminList(pool *pgxpool.Pool, secret, adminSecret string) http.Handle
return func(w http.ResponseWriter, r *http.Request) {
if !authorizeAdminRequest(r, secret, adminSecret) {
sdk.Respond(w, http.StatusUnauthorized, map[string]string{
"error": "missing or invalid authentication header; provide X-Plugin-Secret or X-Hasura-Admin-Secret",
"error": "missing or invalid authentication header; provide X-Internal-Token or X-Hasura-Admin-Secret",
})
return
}
Expand Down Expand Up @@ -64,9 +64,8 @@ func handleAdminList(pool *pgxpool.Pool, secret, adminSecret string) http.Handle
// Size-cap exception: single-responsibility HTTP route handler — 108L of request decode + validate + DB op + response encode; splitting adds indirection without cohesion gain.
func handleExport(pool *pgxpool.Pool, secret string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
provided := r.Header.Get("X-Plugin-Secret")
if provided == "" || provided != secret {
sdk.Respond(w, http.StatusUnauthorized, map[string]string{"error": "missing or invalid X-Plugin-Secret"})
if !hasInternalSecret(r, secret) {
sdk.Respond(w, http.StatusUnauthorized, map[string]string{"error": "missing or invalid X-Internal-Token"})
return
}

Expand Down Expand Up @@ -172,5 +171,5 @@ func handleExport(pool *pgxpool.Pool, secret string) http.HandlerFunc {
}

// authorizeAdminRequest returns true when the request carries a valid
// X-Plugin-Secret or (when adminSecret is non-empty) a valid
// X-Internal-Token or (when adminSecret is non-empty) a valid
// X-Hasura-Admin-Secret header.
2 changes: 1 addition & 1 deletion free/audit-log/internal/handlers_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
)

func authorizeAdminRequest(r *http.Request, secret, adminSecret string) bool {
if ps := r.Header.Get("X-Plugin-Secret"); ps != "" && ps == secret {
if hasInternalSecret(r, secret) {
return true
}
if adminSecret != "" {
Expand Down
9 changes: 4 additions & 5 deletions free/audit-log/internal/handlers_ingest.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@ import (
func handleIngest(pool *pgxpool.Pool, secret string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Always authenticate internal callers.
provided := r.Header.Get("X-Plugin-Secret")
if provided == "" || provided != secret {
sdk.Respond(w, http.StatusUnauthorized, map[string]string{"error": "missing or invalid X-Plugin-Secret"})
if !hasInternalSecret(r, secret) {
sdk.Respond(w, http.StatusUnauthorized, map[string]string{"error": "missing or invalid X-Internal-Token"})
return
}

Expand Down Expand Up @@ -89,7 +88,7 @@ func handleIngest(pool *pgxpool.Pool, secret string) http.HandlerFunc {
CreatedAt: time.Now().UTC(),
}

// Prefer real client IP when X-Plugin-Secret is set (trusted call from
// Prefer real client IP when the internal secret header is set (trusted call from
// within the stack) or fall back to the remote address.
if event.IPAddress == "" {
event.IPAddress = realIP(r)
Expand All @@ -111,7 +110,7 @@ func handleIngest(pool *pgxpool.Pool, secret string) http.HandlerFunc {
}

// handleList handles GET /events.
// All requests must include a matching X-Plugin-Secret header.
// All requests must include a matching X-Internal-Token header.
//
// Supported query parameters:
//
Expand Down
14 changes: 6 additions & 8 deletions free/audit-log/internal/handlers_list_get.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@ import (

func handleList(pool *pgxpool.Pool, secret string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
provided := r.Header.Get("X-Plugin-Secret")
if provided == "" || provided != secret {
sdk.Respond(w, http.StatusUnauthorized, map[string]string{"error": "missing or invalid X-Plugin-Secret"})
if !hasInternalSecret(r, secret) {
sdk.Respond(w, http.StatusUnauthorized, map[string]string{"error": "missing or invalid X-Internal-Token"})
return
}

Expand Down Expand Up @@ -46,12 +45,11 @@ func handleList(pool *pgxpool.Pool, secret string) http.HandlerFunc {
}

// handleGet handles GET /events/{id}.
// All requests must include a matching X-Plugin-Secret header.
// All requests must include a matching X-Internal-Token header.
func handleGet(pool *pgxpool.Pool, secret string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
provided := r.Header.Get("X-Plugin-Secret")
if provided == "" || provided != secret {
sdk.Respond(w, http.StatusUnauthorized, map[string]string{"error": "missing or invalid X-Plugin-Secret"})
if !hasInternalSecret(r, secret) {
sdk.Respond(w, http.StatusUnauthorized, map[string]string{"error": "missing or invalid X-Internal-Token"})
return
}

Expand All @@ -76,5 +74,5 @@ func handleGet(pool *pgxpool.Pool, secret string) http.HandlerFunc {

// handleAdminList handles GET /admin/events.
// This is the Admin-facing variant of GET /events. It accepts either
// X-Plugin-Secret (plugin internal callers) or X-Hasura-Admin-Secret (Admin
// X-Internal-Token (plugin internal callers) or X-Hasura-Admin-Secret (Admin
// UI via Hasura). Supports the same filter query parameters as GET /events.
2 changes: 1 addition & 1 deletion free/audit-log/internal/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
)

// TestAuthorizeAdminRequest_PluginSecret verifies that a valid X-Plugin-Secret
// header grants access.
// header (legacy name, still read) grants access.
func TestAuthorizeAdminRequest_PluginSecret(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "/", nil)
r.Header.Set("X-Plugin-Secret", "my-secret")
Expand Down
37 changes: 37 additions & 0 deletions free/audit-log/internal/internal_auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package internal

import (
"crypto/sha256"
"crypto/subtle"
"net/http"
)

// InternalSecretHeader is the one header that carries PLUGIN_INTERNAL_SECRET on inter-plugin calls (D-0198).
const InternalSecretHeader = "X-Internal-Token"

// legacySecretHeaders are the older names for the same check, still read so callers that have not moved to
// InternalSecretHeader keep working. Remove them once every caller sends the canonical name.
var legacySecretHeaders = []string{"X-Plugin-Secret", "X-Internal-Secret", "X-Internal-Key"}

// presentedSecret returns the secret a request carries: the canonical header, else the first legacy header set.
func presentedSecret(r *http.Request) string {
if v := r.Header.Get(InternalSecretHeader); v != "" {
return v
}
for _, h := range legacySecretHeaders {
if v := r.Header.Get(h); v != "" {
return v
}
}
return ""
}

// hasInternalSecret reports whether the request carries a non-empty secret equal to want, compared in constant time.
func hasInternalSecret(r *http.Request, want string) bool {
got := presentedSecret(r)
if got == "" || want == "" {
return false
}
g, w := sha256.Sum256([]byte(got)), sha256.Sum256([]byte(want))
return subtle.ConstantTimeCompare(g[:], w[:]) == 1
}
46 changes: 46 additions & 0 deletions free/audit-log/internal/internal_auth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package internal

import (
"net/http"
"net/http/httptest"
"testing"
)

// TestHasInternalSecret proves the canonical header is accepted, every legacy name is still read (D-0198), and a
// missing, empty or wrong secret is refused, including when the configured secret is empty.
func TestHasInternalSecret(t *testing.T) {
cases := []struct {
name string
headers map[string]string
want string
ok bool
}{
{"canonical", map[string]string{"X-Internal-Token": "s3"}, "s3", true},
{"legacy X-Plugin-Secret", map[string]string{"X-Plugin-Secret": "s3"}, "s3", true},
{"legacy X-Internal-Secret", map[string]string{"X-Internal-Secret": "s3"}, "s3", true},
{"legacy X-Internal-Key", map[string]string{"X-Internal-Key": "s3"}, "s3", true},
{"canonical wins over a legacy header", map[string]string{"X-Internal-Token": "s3", "X-Plugin-Secret": "other"}, "s3", true},
{"wrong canonical is not rescued by a legacy header", map[string]string{"X-Internal-Token": "bad", "X-Plugin-Secret": "s3"}, "s3", false},
{"wrong value", map[string]string{"X-Internal-Token": "bad"}, "s3", false},
{"no header", nil, "s3", false},
{"empty configured secret never matches", map[string]string{"X-Internal-Token": ""}, "", false},
}
for _, c := range cases {
r := httptest.NewRequest(http.MethodGet, "/", nil)
for k, v := range c.headers {
r.Header.Set(k, v)
}
if got := hasInternalSecret(r, c.want); got != c.ok {
t.Errorf("%s: hasInternalSecret = %v, want %v", c.name, got, c.ok)
}
}
}

// TestAuthorizeAdminRequest_CanonicalHeader proves the admin gate accepts X-Internal-Token.
func TestAuthorizeAdminRequest_CanonicalHeader(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "/", nil)
r.Header.Set("X-Internal-Token", "my-secret")
if !authorizeAdminRequest(r, "my-secret", "") {
t.Error("expected authorizeAdminRequest to accept X-Internal-Token")
}
}
2 changes: 1 addition & 1 deletion free/audit-log/nginx/audit-log.conf
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ server {
set $upstream http://audit-log:3308;

# POST /events is an internal ingestion endpoint.
# It is protected by X-Plugin-Secret at the application layer.
# It is protected by X-Internal-Token at the application layer.
# At the nginx layer we additionally restrict it to requests originating
# from the internal Docker network (127.0.0.0/8 and 172.16.0.0/12).
location = /events {
Expand Down
4 changes: 2 additions & 2 deletions free/linkedin/internal/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -312,11 +312,11 @@ func (h *Handlers) HandleTools(w http.ResponseWriter, _ *http.Request) {
// =============================================================================

// InternalAuthMiddleware rejects requests that do not carry the correct
// X-Internal-Token header.
// X-Internal-Token header (or a legacy name, see presentedSecret).
func InternalAuthMiddleware(secret string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("X-Internal-Token")
token := presentedSecret(r)
if token == "" {
http.Error(w, "X-Internal-Token required", http.StatusUnauthorized)
return
Expand Down
20 changes: 20 additions & 0 deletions free/linkedin/internal/internal_auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package internal

import "net/http"

// legacySecretHeaders are the older names for the X-Internal-Token check (D-0198), still read so callers that have
// not moved to the canonical name keep working. Remove them once every caller sends X-Internal-Token.
var legacySecretHeaders = []string{"X-Plugin-Secret", "X-Internal-Secret", "X-Internal-Key"}

// presentedSecret returns the secret a request carries: X-Internal-Token, else the first legacy header set.
func presentedSecret(r *http.Request) string {
if v := r.Header.Get("X-Internal-Token"); v != "" {
return v
}
for _, h := range legacySecretHeaders {
if v := r.Header.Get(h); v != "" {
return v
}
}
return ""
}
37 changes: 37 additions & 0 deletions free/linkedin/internal/internal_auth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package internal

import (
"net/http"
"net/http/httptest"
"testing"
)

// TestInternalAuth_HeaderNames proves X-Internal-Token and every legacy name carry the same secret (D-0198), a
// wrong value under any name is refused, and a legacy header never overrides a present canonical one.
func TestInternalAuth_HeaderNames(t *testing.T) {
cases := []struct {
name string
headers map[string]string
want int
}{
{"canonical", map[string]string{"X-Internal-Token": "s3"}, 200},
{"legacy X-Plugin-Secret", map[string]string{"X-Plugin-Secret": "s3"}, 200},
{"legacy X-Internal-Secret", map[string]string{"X-Internal-Secret": "s3"}, 200},
{"legacy X-Internal-Key", map[string]string{"X-Internal-Key": "s3"}, 200},
{"wrong canonical is not rescued by a right legacy header", map[string]string{"X-Internal-Token": "bad", "X-Plugin-Secret": "s3"}, 403},
{"wrong legacy", map[string]string{"X-Plugin-Secret": "bad"}, 403},
{"no header", nil, 401},
}
for _, c := range cases {
h := InternalAuthMiddleware("s3")(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}))
req := httptest.NewRequest(http.MethodGet, "/", nil)
for k, v := range c.headers {
req.Header.Set(k, v)
}
rr := httptest.NewRecorder()
h.ServeHTTP(rr, req)
if rr.Code != c.want {
t.Errorf("%s: code %d, want %d", c.name, rr.Code, c.want)
}
}
}
37 changes: 37 additions & 0 deletions free/post/cmd/internal_auth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package main

import (
"net/http"
"net/http/httptest"
"testing"
)

// TestInternalAuth_HeaderNames proves X-Internal-Token and every legacy name carry the same secret (D-0198), a
// wrong value under any name is refused, and a legacy header never overrides a present canonical one.
func TestInternalAuth_HeaderNames(t *testing.T) {
cases := []struct {
name string
headers map[string]string
want int
}{
{"canonical", map[string]string{"X-Internal-Token": "s3"}, 200},
{"legacy X-Plugin-Secret", map[string]string{"X-Plugin-Secret": "s3"}, 200},
{"legacy X-Internal-Secret", map[string]string{"X-Internal-Secret": "s3"}, 200},
{"legacy X-Internal-Key", map[string]string{"X-Internal-Key": "s3"}, 200},
{"wrong canonical is not rescued by a right legacy header", map[string]string{"X-Internal-Token": "bad", "X-Plugin-Secret": "s3"}, 403},
{"wrong legacy", map[string]string{"X-Plugin-Secret": "bad"}, 403},
{"no header", nil, 401},
}
for _, c := range cases {
h := internalAuthMiddleware("s3")(http.HandlerFunc(func(http.ResponseWriter, *http.Request) {}))
req := httptest.NewRequest(http.MethodGet, "/", nil)
for k, v := range c.headers {
req.Header.Set(k, v)
}
rr := httptest.NewRecorder()
h.ServeHTTP(rr, req)
if rr.Code != c.want {
t.Errorf("%s: code %d, want %d", c.name, rr.Code, c.want)
}
}
}
21 changes: 19 additions & 2 deletions free/post/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,29 @@ func main() {
log.Println("nself-post stopped")
}

// legacySecretHeaders are the older names for the X-Internal-Token check (D-0198), still read so callers that have
// not moved to the canonical name keep working. Remove them once every caller sends X-Internal-Token.
var legacySecretHeaders = []string{"X-Plugin-Secret", "X-Internal-Secret", "X-Internal-Key"}

// presentedSecret returns the secret a request carries: X-Internal-Token, else the first legacy header set.
func presentedSecret(r *http.Request) string {
if v := r.Header.Get("X-Internal-Token"); v != "" {
return v
}
for _, h := range legacySecretHeaders {
if v := r.Header.Get(h); v != "" {
return v
}
}
return ""
}

// internalAuthMiddleware rejects requests that do not present a valid
// X-Internal-Token header matching the configured secret.
// X-Internal-Token header (or a legacy name, see presentedSecret) matching the configured secret.
func internalAuthMiddleware(secret string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("X-Internal-Token")
token := presentedSecret(r)
if token == "" {
http.Error(w, "X-Internal-Token required", http.StatusUnauthorized)
return
Expand Down
Loading
Loading