Files
authz_client/client_test.go
T
argoyle 01f30cfd2b
authz_client / test (push) Successful in 58s
Unbound Release / Check Preconditions (push) Successful in 22s
Unbound Release / Create Tag (push) Skipped
authz_client / vulnerabilities (push) Successful in 47s
Unbound Release / Generate Changelog and Handle PR (push) Successful in 32s
Unbound Release / Create Release (push) Successful in 24s
Release / release (push) Successful in 58s
pre-commit / pre-commit (push) Successful in 2m17s
fix: keep existing privileges when User.Added is processed late (#331)
2026-09-16 05:19:03 +00:00

733 lines
19 KiB
Go

package client
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"sort"
"sync"
"testing"
goamqp "codeberg.org/messaging/go-messaging-amqp"
spec "codeberg.org/messaging/messaging"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPrivilegeHandler_Process_InvalidType(t *testing.T) {
handler := New(WithBaseURL("base"))
err := handler.Process("abc")
assert.EqualError(t, err, "unexpected event type: 'string'")
}
func TestPrivilegeHandler_Process_PrivilegeRemoved(t *testing.T) {
handler := New(WithBaseURL("base"))
err := handler.Process(&PrivilegeAdded{
Email: "jim@example.org",
CompanyID: "abc-123",
Privilege: PrivilegeAdmin,
})
assert.NoError(t, err)
companies := handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
return privileges.Admin
})
assert.Equal(t, []string{"abc-123"}, companies)
err = handler.Process(&PrivilegeRemoved{
Email: "jim@example.org",
CompanyID: "abc-123",
Privilege: PrivilegeAdmin,
})
assert.NoError(t, err)
companies = handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
return privileges.Admin
})
assert.Empty(t, companies)
}
func TestPrivilegeHandler_Process_UserAdded_And_UserRemoved(t *testing.T) {
handler := New(WithBaseURL("base"))
err := handler.Process(&UserAdded{
Email: "jim@example.org",
CompanyID: "abc-123",
})
assert.NoError(t, err)
err = handler.Process(&UserAdded{
Email: "jim@example.org",
CompanyID: "abc-456",
})
assert.NoError(t, err)
companies := handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
return true
})
sort.Strings(companies)
assert.Equal(t, []string{"abc-123", "abc-456"}, companies)
err = handler.Process(&UserRemoved{
Email: "jim@example.org",
CompanyID: "abc-123",
})
assert.NoError(t, err)
err = handler.Process(&UserRemoved{
Email: "jim@example.org",
CompanyID: "abc-456",
})
assert.NoError(t, err)
companies = handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
return true
})
assert.Empty(t, companies)
}
func TestPrivilegeHandler_Process_UserAdded_Keeps_Existing_Privileges(t *testing.T) {
// Each routing key has its own transient queue, so Privilege.Added can be
// processed before the User.Added published before it. The privilege must
// survive either order: nothing re-reads it until the next Fetch at start.
userAdded := &UserAdded{Email: "jim@example.org", CompanyID: "abc-123"}
privilegeAdded := &PrivilegeAdded{Email: "jim@example.org", CompanyID: "abc-123", Privilege: PrivilegeAdmin}
for name, order := range map[string][]any{
"user added first": {userAdded, privilegeAdded},
"privilege added first": {privilegeAdded, userAdded},
} {
t.Run(name, func(t *testing.T) {
handler := New(WithBaseURL("base"))
for _, event := range order {
require.NoError(t, handler.Process(event))
}
assert.True(t, handler.IsAllowed("jim@example.org", "abc-123", func(privileges CompanyPrivileges) bool {
return privileges.Admin
}))
})
}
}
func TestPrivilegeHandler_Process_UserAdded_After_UserRemoved_Starts_Empty(t *testing.T) {
handler := New(WithBaseURL("base"))
assert.NoError(t, handler.Process(&UserAdded{Email: "jim@example.org", CompanyID: "abc-123"}))
assert.NoError(t, handler.Process(&PrivilegeAdded{
Email: "jim@example.org",
CompanyID: "abc-123",
Privilege: PrivilegeAdmin,
}))
assert.NoError(t, handler.Process(&UserRemoved{Email: "jim@example.org", CompanyID: "abc-123"}))
assert.NoError(t, handler.Process(&UserAdded{Email: "jim@example.org", CompanyID: "abc-123"}))
// Membership is back, without the privileges the removal took away.
assert.Equal(t, []string{"abc-123"}, handler.CompaniesByUser("jim@example.org", func(CompanyPrivileges) bool {
return true
}))
assert.False(t, handler.IsAllowed("jim@example.org", "abc-123", func(privileges CompanyPrivileges) bool {
return privileges.Admin
}))
}
func TestPrivilegeHandler_GetCompanies_Email_Not_Found(t *testing.T) {
handler := New(WithBaseURL("base"))
companies := handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
return true
})
assert.Empty(t, companies)
}
func TestPrivilegeHandler_GetCompanies_No_Companies_Found(t *testing.T) {
handler := New(WithBaseURL("base"))
err := handler.Process(&UserAdded{
Email: "jim@example.org",
CompanyID: "abc-123",
})
assert.NoError(t, err)
companies := handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
return privileges.Admin
})
assert.Empty(t, companies)
companies = handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
return true
})
assert.Equal(t, []string{"abc-123"}, companies)
err = handler.Process(&UserRemoved{
Email: "jim@example.org",
CompanyID: "abc-123",
})
assert.NoError(t, err)
companies = handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
return true
})
assert.Empty(t, companies)
}
func TestPrivilegeHandler_GetCompanies_Company_With_Company_Access_Found(t *testing.T) {
handler := New(WithBaseURL("base"))
err := handler.Process(&PrivilegeAdded{
Email: "jim@example.org",
CompanyID: "abc-123",
Privilege: PrivilegeCompany,
})
assert.NoError(t, err)
companies := handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
return privileges.Company
})
assert.Equal(t, []string{"abc-123"}, companies)
}
func TestPrivilegeHandler_GetCompanies_Company_With_Admin_Access_Found(t *testing.T) {
handler := New(WithBaseURL("base"))
err := handler.Process(&PrivilegeAdded{
Email: "jim@example.org",
CompanyID: "abc-123",
Privilege: PrivilegeConsumer,
})
assert.NoError(t, err)
companies := handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
return privileges.Consumer
})
assert.Equal(t, []string{"abc-123"}, companies)
}
func TestPrivilegeHandler_IsAllowed_Return_False_If_No_Privileges(t *testing.T) {
handler := New(WithBaseURL("base"))
result := handler.IsAllowed("jim@example.org", "abc-123", func(privileges CompanyPrivileges) bool {
return privileges.Company
})
assert.False(t, result)
}
func TestPrivilegeHandler_IsAllowed_Return_True_If_Privilege_Exists(t *testing.T) {
handler := New(WithBaseURL("base"))
_ = handler.Process(&PrivilegeAdded{
Email: "jim@example.org",
CompanyID: "abc-123",
Privilege: PrivilegeTime,
})
result := handler.IsAllowed("jim@example.org", "abc-123", func(privileges CompanyPrivileges) bool {
return privileges.Time
})
assert.True(t, result)
_ = handler.Process(&PrivilegeAdded{
Email: "jim@example.org",
CompanyID: "abc-123",
Privilege: PrivilegeInvoicing,
})
result = handler.IsAllowed("jim@example.org", "abc-123", func(privileges CompanyPrivileges) bool {
return privileges.Invoicing
})
assert.True(t, result)
_ = handler.Process(&PrivilegeAdded{
Email: "jim@example.org",
CompanyID: "abc-123",
Privilege: PrivilegeAccounting,
})
result = handler.IsAllowed("jim@example.org", "abc-123", func(privileges CompanyPrivileges) bool {
return privileges.Accounting
})
assert.True(t, result)
_ = handler.Process(&PrivilegeAdded{
Email: "jim@example.org",
CompanyID: "abc-123",
Privilege: PrivilegeSupplier,
})
result = handler.IsAllowed("jim@example.org", "abc-123", func(privileges CompanyPrivileges) bool {
return privileges.Supplier
})
assert.True(t, result)
_ = handler.Process(&PrivilegeAdded{
Email: "jim@example.org",
CompanyID: "abc-123",
Privilege: PrivilegeSalary,
})
result = handler.IsAllowed("jim@example.org", "abc-123", func(privileges CompanyPrivileges) bool {
return privileges.Salary
})
assert.True(t, result)
}
func TestPrivilegeHandler_Fetch_Sends_Authorization_Header_When_APIKey_Set(t *testing.T) {
var receivedAuth string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
receivedAuth = r.Header.Get("Authorization")
_, _ = w.Write([]byte("{}"))
}))
defer server.Close()
handler := New(
WithBaseURL(server.URL),
WithAPIKey("my-secret-key"),
)
err := handler.Fetch()
assert.NoError(t, err)
assert.Equal(t, "Bearer my-secret-key", receivedAuth)
}
func TestPrivilegeHandler_Fetch_No_Authorization_Header_Without_APIKey(t *testing.T) {
var receivedAuth string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
receivedAuth = r.Header.Get("Authorization")
_, _ = w.Write([]byte("{}"))
}))
defer server.Close()
handler := New(WithBaseURL(server.URL))
err := handler.Fetch()
assert.NoError(t, err)
assert.Empty(t, receivedAuth)
}
func TestPrivilegeHandler_Fetch_Error_Response(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(500)
}))
baseURL := server.Listener.Addr().String()
handler := New(WithBaseURL(fmt.Sprintf("http://%s", baseURL)))
server.Close()
err := handler.Fetch()
assert.EqualError(t, err, fmt.Sprintf("Get \"http://%s/authz\": dial tcp %s: connect: connection refused", baseURL, baseURL))
}
func TestPrivilegeHandler_Fetch_Error_Unreadable_Body(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Length", "1")
}))
defer server.Close()
baseURL := server.Listener.Addr().String()
handler := New(WithBaseURL(fmt.Sprintf("http://%s", baseURL)))
err := handler.Fetch()
assert.EqualError(t, err, "unexpected EOF")
}
func TestPrivilegeHandler_Fetch_Error_Broken_JSON(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("{abc"))
}))
defer server.Close()
baseURL := server.Listener.Addr().String()
handler := New(WithBaseURL(fmt.Sprintf("http://%s", baseURL)))
err := handler.Fetch()
assert.EqualError(t, err, "invalid character 'a' looking for beginning of object key string")
}
func TestPrivilegeHandler_Fetch_Valid(t *testing.T) {
privileges := `
{
"jim@example.org": {
"00010203-0405-4607-8809-0a0b0c0d0e0f": {
"admin": false,
"company": true,
"consumer": false,
"time": true,
"invoicing": true,
"accounting": false,
"supplier": false,
"salary": true
}
}
}`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(privileges))
}))
defer server.Close()
baseURL := server.Listener.Addr().String()
handler := New(WithBaseURL(fmt.Sprintf("http://%s", baseURL)))
err := handler.Fetch()
assert.NoError(t, err)
expectedPrivileges := map[string]map[string]*CompanyPrivileges{
"jim@example.org": {
"00010203-0405-4607-8809-0a0b0c0d0e0f": {
Admin: false,
Company: true,
Consumer: false,
Time: true,
Invoicing: true,
Accounting: false,
Supplier: false,
Salary: true,
},
},
}
assert.Equal(t, expectedPrivileges, handler.privileges)
}
func TestPrivilegeHandler_Fetch_Concurrent_Fetches(t *testing.T) {
privileges := `
{
"jim@example.org": {
"00010203-0405-4607-8809-0a0b0c0d0e0f": {
"admin": false,
"company": true,
"consumer": false,
"time": true,
"invoicing": true,
"accounting": false,
"supplier": false,
"salary": true
}
}
}`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(privileges))
}))
defer server.Close()
baseURL := server.Listener.Addr().String()
handler := New(WithBaseURL(fmt.Sprintf("http://%s", baseURL)))
// Run multiple Fetch calls concurrently to test thread-safety
var wg sync.WaitGroup
errors := make(chan error, 10)
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
if err := handler.Fetch(); err != nil {
errors <- err
}
}()
}
wg.Wait()
close(errors)
// Check no errors occurred
for err := range errors {
assert.NoError(t, err)
}
// Verify privileges were set correctly
expectedPrivileges := map[string]map[string]*CompanyPrivileges{
"jim@example.org": {
"00010203-0405-4607-8809-0a0b0c0d0e0f": {
Admin: false,
Company: true,
Consumer: false,
Time: true,
Invoicing: true,
Accounting: false,
Supplier: false,
Salary: true,
},
},
}
assert.Equal(t, expectedPrivileges, handler.privileges)
}
func TestPrivilegeHandler_Concurrent_Fetch_And_Read(t *testing.T) {
privileges := `
{
"jim@example.org": {
"abc-123": {
"admin": true,
"company": true,
"consumer": false,
"time": false,
"invoicing": false,
"accounting": false,
"supplier": false,
"salary": false
}
}
}`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(privileges))
}))
defer server.Close()
baseURL := server.Listener.Addr().String()
handler := New(WithBaseURL(fmt.Sprintf("http://%s", baseURL)))
var wg sync.WaitGroup
errors := make(chan error, 100)
// Start multiple Fetch operations
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
if err := handler.Fetch(); err != nil {
errors <- err
}
}()
}
// Concurrently read privileges while Fetch is running
for i := 0; i < 50; i++ {
wg.Add(1)
go func() {
defer wg.Done()
_ = handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
return privileges.Admin
})
}()
}
// Concurrently check privileges while Fetch is running
for i := 0; i < 50; i++ {
wg.Add(1)
go func() {
defer wg.Done()
_ = handler.IsAllowed("jim@example.org", "abc-123", func(privileges CompanyPrivileges) bool {
return privileges.Admin
})
}()
}
wg.Wait()
close(errors)
// Check no errors occurred
for err := range errors {
assert.NoError(t, err)
}
// Verify privileges are correct after all concurrent operations
companies := handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
return privileges.Admin
})
assert.Equal(t, []string{"abc-123"}, companies)
isAllowed := handler.IsAllowed("jim@example.org", "abc-123", func(privileges CompanyPrivileges) bool {
return privileges.Admin && privileges.Company
})
assert.True(t, isAllowed)
}
func TestPrivilegeHandler_Concurrent_Process_And_Read(t *testing.T) {
handler := New(WithBaseURL("base"))
var wg sync.WaitGroup
// Concurrently add privileges via Process
for i := 0; i < 100; i++ {
wg.Add(1)
companyID := fmt.Sprintf("company-%d", i%10)
go func(id string) {
defer wg.Done()
_ = handler.Process(&PrivilegeAdded{
Email: "jim@example.org",
CompanyID: id,
Privilege: PrivilegeAdmin,
})
}(companyID)
}
// Concurrently read privileges while Process is running
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
defer wg.Done()
_ = handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
return privileges.Admin
})
}()
}
wg.Wait()
// Verify all companies were added
companies := handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
return privileges.Admin
})
sort.Strings(companies)
expected := make([]string, 10)
for i := 0; i < 10; i++ {
expected[i] = fmt.Sprintf("company-%d", i)
}
sort.Strings(expected)
assert.Equal(t, expected, companies)
}
func TestPrivilegeHandler_Concurrent_Multiple_Operations(t *testing.T) {
privileges := `
{
"jim@example.org": {
"initial-company": {
"admin": true,
"company": true,
"consumer": false,
"time": false,
"invoicing": false,
"accounting": false,
"supplier": false,
"salary": false
}
}
}`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(privileges))
}))
defer server.Close()
baseURL := server.Listener.Addr().String()
handler := New(WithBaseURL(fmt.Sprintf("http://%s", baseURL)))
var wg sync.WaitGroup
// Fetch
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
_ = handler.Fetch()
}()
}
// Process PrivilegeAdded
for i := 0; i < 20; i++ {
wg.Add(1)
go func(idx int) {
defer wg.Done()
_ = handler.Process(&PrivilegeAdded{
Email: "jane@example.org",
CompanyID: fmt.Sprintf("company-%d", idx%5),
Privilege: PrivilegeCompany,
})
}(i)
}
// CompaniesByUser reads
for i := 0; i < 50; i++ {
wg.Add(1)
email := "jim@example.org"
if i%2 == 0 {
email = "jane@example.org"
}
go func(e string) {
defer wg.Done()
_ = handler.CompaniesByUser(e, func(privileges CompanyPrivileges) bool {
return privileges.Admin || privileges.Company
})
}(email)
}
// IsAllowed reads
for i := 0; i < 50; i++ {
wg.Add(1)
go func() {
defer wg.Done()
_ = handler.IsAllowed("jim@example.org", "initial-company", func(privileges CompanyPrivileges) bool {
return privileges.Admin
})
}()
}
wg.Wait()
// Verify final state is consistent
jimCompanies := handler.CompaniesByUser("jim@example.org", func(privileges CompanyPrivileges) bool {
return privileges.Admin
})
assert.Contains(t, jimCompanies, "initial-company")
janeCompanies := handler.CompaniesByUser("jane@example.org", func(privileges CompanyPrivileges) bool {
return privileges.Company
})
sort.Strings(janeCompanies)
expectedJane := []string{"company-0", "company-1", "company-2", "company-3", "company-4"}
assert.Equal(t, expectedJane, janeCompanies)
}
func TestPrivilegeHandler_Setup(t *testing.T) {
handler := New(WithBaseURL("base"))
topology, err := goamqp.CollectTopology("some-service", handler.Setup()...)
assert.NoError(t, err)
wiring := map[string]string{}
for _, e := range topology.Endpoints {
assert.Equal(t, spec.DirectionConsume, e.Direction)
assert.True(t, e.Ephemeral, "%s must be a per-replica consumer", e.RoutingKey)
wiring[e.RoutingKey] = e.MessageType
}
// A key wired to the wrong type could turn a revocation into a grant.
assert.Equal(t, map[string]string{
"User.Added": "client.UserAdded",
"User.Removed": "client.UserRemoved",
"Privilege.Added": "client.PrivilegeAdded",
"Privilege.Removed": "client.PrivilegeRemoved",
}, wiring)
}
func TestPrivilegeHandler_process(t *testing.T) {
ctx := context.Background()
handler := New(WithBaseURL("base"))
admin := func(p CompanyPrivileges) bool { return p.Admin }
assert.NoError(t, process[UserAdded](handler)(ctx, spec.ConsumableEvent[UserAdded]{
Payload: UserAdded{Email: "jim@example.org", CompanyID: "abc-123"},
}))
assert.False(t, handler.IsAllowed("jim@example.org", "abc-123", admin))
assert.NoError(t, process[PrivilegeAdded](handler)(ctx, spec.ConsumableEvent[PrivilegeAdded]{
Payload: PrivilegeAdded{Email: "jim@example.org", CompanyID: "abc-123", Privilege: PrivilegeAdmin},
}))
assert.True(t, handler.IsAllowed("jim@example.org", "abc-123", admin))
assert.NoError(t, process[PrivilegeRemoved](handler)(ctx, spec.ConsumableEvent[PrivilegeRemoved]{
Payload: PrivilegeRemoved{Email: "jim@example.org", CompanyID: "abc-123", Privilege: PrivilegeAdmin},
}))
assert.False(t, handler.IsAllowed("jim@example.org", "abc-123", admin))
assert.NoError(t, process[UserRemoved](handler)(ctx, spec.ConsumableEvent[UserRemoved]{
Payload: UserRemoved{Email: "jim@example.org", CompanyID: "abc-123"},
}))
assert.Empty(t, handler.CompaniesByUser("jim@example.org", func(CompanyPrivileges) bool { return true }))
}