8000 feat: Add token-level control for public updates by MHNassar · Pull Request #1017 · dunglas/mercure · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

feat: Add token-level control for public updates #1017

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
27 changes: 27 additions & 0 deletions authorization.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"net/http"
"net/url"
"strings"

"github.com/golang-jwt/jwt/v5"
"go.uber.org/zap"
Expand Down Expand Up @@ -157,6 +158,32 @@ func canDispatch(s *TopicSelectorStore, topics, topicSelectors []string) bool {
return true
}

// canDispatchPublic checks if the payload allow public updates by examining the "allow_public_updates" field in the payload.
// It returns true if the field is set to true, or if the field is not present (for backward compatibility).
func canDispatchPublic(payload interface{}) bool {
payloadMap, ok := payload.(map[string]interface{})
if !ok {
return true
}

publicPublish, exists := payloadMap["allow_public_updates"]
if !exists {
return true
}

// Check boolean value
if isPublicPublish, ok := publicPublish.(bool); ok {
return isPublicPublish
}

// Check string value
if strValue, ok := publicPublish.(string); ok {
return strings.ToLower(strValue) != "false"
}

return true
}

func (h *Hub) httpAuthorizationError(w http.ResponseWriter, r *http.Request, err error) {
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
if c := h.logger.Check(zap.DebugLevel, "Topic selectors not matched, not provided or authorization error"); c != nil {
Expand Down
49 changes: 49 additions & 0 deletions authorization_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -724,3 +724,52 @@ func TestCanDispatch(t *testing.T) {
assert.False(t, canDispatch(tss, []string{"foo", "bar"}, []string{"baz"}))
assert.False(t, canDispatch(tss, []string{"foo", "bar"}, []string{"baz", "bat"}))
}

func TestCanDispatchPublic(t *testing.T) {
t.Parallel()

tests := []struct {
name string
payload interface{}
expected bool
}{
{
name: "allow_public_updates is true",
payload: map[string]interface{}{"allow_public_updates": true},
expected: true,
},
{
name: "allow_public_updates is true as a string",
payload: map[string]interface{}{"allow_public_updates": "true"},
expected: true,
},
{
name: "allow_public_updates is false",
payload: map[string]interface{}{"allow_public_updates": false},
expected: false,
},
{
name: "allow_public_updates is false as a string",
payload: map[string]interface{}{"allow_public_updates": "false"},
expected: false,
},
{
name: "allow_public_updates is missing",
payload: map[string]interface{}{},
expected: true,
},
{
name: "payload is not a map",
payload: "invalid payload",
expected: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
result := canDispatchPublic(tt.payload)
assert.Equal(t, tt.expected, result)
})
}
}
7 changes: 7 additions & 0 deletions publish.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@ func (h *Hub) PublishHandler(w http.ResponseWriter, r *http.Request) {
}
}

if !private && !canDispatchPublic(claims.Mercure.Payload) {
h.logger.Info("Unauthorized: token does not allow public updates, 'allow_public_updates' is set to false")
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)

return
}

u := &Update{
Topics: topics,
Private: private,
Expand Down
56 changes: 56 additions & 0 deletions publish_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -385,3 +385,59 @@ func FuzzPublish(f *testing.F) {
assert.Equal(t, id, string(body))
})
}

func TestPublishPublicEventWithAllowPublicUpdatesFalse(t *testing.T) {
t.Parallel()

hub := createDummy()

form := url.Values{}
form.Add("topic", "http://example.com/books/1")
form.Add("data", "foo")

req := httptest.NewRequest(http.MethodPost, defaultHubURL, strings.NewReader(form.Encode()))
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")

payload := map[string]interface{}{
"bar": "baz",
"allow_public_updates": false,
}

req.Header.Add("Authorization", bearerPrefix+createDummyAuthorizedJWTWithPayload(rolePublisher, []string{"foo"}, payload))

w := httptest.NewRecorder()
hub.PublishHandler(w, req)

resp := w.Result()
defer resp.Body.Close()

assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
}

func TestPublishPublicEventWithAllowPublicUpdatesTrue(t *testing.T) {
t.Parallel()

hub := createDummy()

form := url.Values{}
form.Add("topic", "http://example.com/books/1")
form.Add("data", "foo")

req := httptest.NewRequest(http.MethodPost, defaultHubURL, strings.NewReader(form.Encode()))
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")

payload := map[string]interface{}{
"bar": "baz",
"allow_public_updates": true,
}

req.Header.Add("Authorization", bearerPrefix+createDummyAuthorizedJWTWithPayload(rolePublisher, []string{"*"}, payload))

w := httptest.NewRecorder()
hub.PublishHandler(w, req)

resp := w.Result()
defer resp.Body.Close()

assert.Equal(t, http.StatusOK, resp.StatusCode)
}
Loading
0