SMQ-2758 - Add option Auth to call webhook for only for certain authz (#2763)

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>
This commit is contained in:
Steve Munene
2025-03-21 01:10:32 +03:00
committed by GitHub
parent 3b675a7ab3
commit ddb3f9ba6d
6 changed files with 87 additions and 15 deletions
+1
View File
@@ -102,6 +102,7 @@ The service is configured using the environment variables presented in the follo
| SMQ_AUTH_CALLOUT_CA_CERT | Path to CA certificate file | "" |
| SMQ_AUTH_CALLOUT_CERT | Path to client certificate file | "" |
| SMQ_AUTH_CALLOUT_KEY | Path to client key file | "" |
| SMQ_AUTH_CALLOUT_INVOKE_PERMISSIONS | Invoke callout if the authorization permission matches any of the given permissions. | "" |
## Deployment
+23 -7
View File
@@ -17,9 +17,10 @@ import (
)
type callback struct {
httpClient *http.Client
urls []string
method string
httpClient *http.Client
urls []string
method string
allowedPermission map[string]struct{}
}
// CallBack send auth request to an external service.
@@ -30,7 +31,7 @@ type CallBack interface {
}
// NewCallback creates a new instance of CallBack.
func NewCallback(httpClient *http.Client, method string, urls []string) (CallBack, error) {
func NewCallback(httpClient *http.Client, method string, urls []string, permissions []string) (CallBack, error) {
if httpClient == nil {
httpClient = http.DefaultClient
}
@@ -38,10 +39,16 @@ func NewCallback(httpClient *http.Client, method string, urls []string) (CallBac
return nil, fmt.Errorf("unsupported auth callback method: %s", method)
}
allowedPermission := make(map[string]struct{})
for _, permission := range permissions {
allowedPermission[permission] = struct{}{}
}
return &callback{
httpClient: httpClient,
urls: urls,
method: method,
httpClient: httpClient,
urls: urls,
method: method,
allowedPermission: allowedPermission,
}, nil
}
@@ -50,6 +57,15 @@ func (c *callback) Authorize(ctx context.Context, pr policies.Policy) error {
return nil
}
// Check if the permission is in the allowed list
// Otherwise, only call webhook if the permission is in the map
if len(c.allowedPermission) > 0 {
_, exists := c.allowedPermission[pr.Permission]
if !exists {
return nil
}
}
payload := map[string]string{
"domain": pr.Domain,
"subject": pr.Subject,
+59 -7
View File
@@ -71,7 +71,7 @@ func TestCallback_Authorize(t *testing.T) {
}))
defer ts.Close()
cb, err := auth.NewCallback(http.DefaultClient, tc.method, []string{ts.URL})
cb, err := auth.NewCallback(http.DefaultClient, tc.method, []string{ts.URL}, []string{})
assert.NoError(t, err)
err = cb.Authorize(context.Background(), policy)
@@ -96,21 +96,21 @@ func TestCallback_MultipleURLs(t *testing.T) {
}))
defer ts2.Close()
cb, err := auth.NewCallback(http.DefaultClient, http.MethodPost, []string{ts1.URL, ts2.URL})
cb, err := auth.NewCallback(http.DefaultClient, http.MethodPost, []string{ts1.URL, ts2.URL}, []string{})
assert.NoError(t, err)
err = cb.Authorize(context.Background(), policies.Policy{})
assert.NoError(t, err)
}
func TestCallback_InvalidURL(t *testing.T) {
cb, err := auth.NewCallback(http.DefaultClient, http.MethodPost, []string{"http://invalid-url"})
cb, err := auth.NewCallback(http.DefaultClient, http.MethodPost, []string{"http://invalid-url"}, []string{})
assert.NoError(t, err)
err = cb.Authorize(context.Background(), policies.Policy{})
assert.Error(t, err)
}
func TestCallback_InvalidMethod(t *testing.T) {
_, err := auth.NewCallback(http.DefaultClient, "invalid-method", []string{"http://example.com"})
_, err := auth.NewCallback(http.DefaultClient, "invalid-method", []string{"http://example.com"}, []string{})
assert.Error(t, err)
}
@@ -123,21 +123,73 @@ func TestCallback_CancelledContext(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel()
cb, err := auth.NewCallback(http.DefaultClient, http.MethodPost, []string{ts.URL})
cb, err := auth.NewCallback(http.DefaultClient, http.MethodPost, []string{ts.URL}, []string{})
assert.NoError(t, err)
err = cb.Authorize(ctx, policies.Policy{})
assert.Error(t, err)
}
func TestNewCallback_NilClient(t *testing.T) {
cb, err := auth.NewCallback(nil, http.MethodPost, []string{"test"})
cb, err := auth.NewCallback(nil, http.MethodPost, []string{"test"}, []string{})
assert.NoError(t, err)
assert.NotNil(t, cb)
}
func TestCallback_NoURL(t *testing.T) {
cb, err := auth.NewCallback(http.DefaultClient, http.MethodPost, []string{})
cb, err := auth.NewCallback(http.DefaultClient, http.MethodPost, []string{}, []string{})
assert.NoError(t, err)
err = cb.Authorize(context.Background(), policies.Policy{})
assert.NoError(t, err)
}
func TestCallback_PermissionFiltering(t *testing.T) {
webhookCalled := false
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
webhookCalled = true
w.WriteHeader(http.StatusOK)
}))
defer ts.Close()
t.Run("allowed permission", func(t *testing.T) {
webhookCalled = false
allowedPermissions := []string{"create_client", "delete_channel"}
cb, err := auth.NewCallback(http.DefaultClient, http.MethodPost, []string{ts.URL}, allowedPermissions)
assert.NoError(t, err)
err = cb.Authorize(context.Background(), policies.Policy{
Permission: "create_client",
})
assert.NoError(t, err)
assert.True(t, webhookCalled, "webhook should be called for allowed permission")
})
t.Run("non-allowed permission", func(t *testing.T) {
webhookCalled = false
allowedPermissions := []string{"create_client", "delete_channel"}
cb, err := auth.NewCallback(http.DefaultClient, http.MethodPost, []string{ts.URL}, allowedPermissions)
assert.NoError(t, err)
err = cb.Authorize(context.Background(), policies.Policy{
Permission: "read_channel",
})
assert.NoError(t, err)
assert.False(t, webhookCalled, "webhook should not be called for non-allowed permission")
})
t.Run("empty allowed permissions", func(t *testing.T) {
webhookCalled = false
allowedPermissions := []string{}
cb, err := auth.NewCallback(http.DefaultClient, http.MethodPost, []string{ts.URL}, allowedPermissions)
assert.NoError(t, err)
err = cb.Authorize(context.Background(), policies.Policy{
Permission: "any_permission",
})
assert.NoError(t, err)
assert.True(t, webhookCalled, "webhook should be called when allowed permissions list is empty")
})
}
+2 -1
View File
@@ -87,6 +87,7 @@ type config struct {
AuthCalloutCACert string `env:"SMQ_AUTH_CALLOUT_CA_CERT" envDefault:""`
AuthCalloutCert string `env:"SMQ_AUTH_CALLOUT_CERT" envDefault:""`
AuthCalloutKey string `env:"SMQ_AUTH_CALLOUT_KEY" envDefault:""`
AuthCalloutPermissions []string `env:"SMQ_AUTH_CALLOUT_INVOKE_PERMISSIONS" envDefault:"" envSeparator:","`
}
func main() {
@@ -284,7 +285,7 @@ func newService(db *sqlx.DB, tracer trace.Tracer, cfg config, dbConfig pgclient.
},
Timeout: cfg.AuthCalloutTimeout,
}
callback, err := auth.NewCallback(httpClient, cfg.AuthCalloutMethod, cfg.AuthCalloutURLs)
callback, err := auth.NewCallback(httpClient, cfg.AuthCalloutMethod, cfg.AuthCalloutURLs, cfg.AuthCalloutPermissions)
if err != nil {
return nil, err
}
+1
View File
@@ -109,6 +109,7 @@ SMQ_AUTH_CALLOUT_TIMEOUT="10s"
SMQ_AUTH_CALLOUT_CA_CERT=""
SMQ_AUTH_CALLOUT_CERT=""
SMQ_AUTH_CALLOUT_KEY=""
SMQ_AUTH_CALLOUT_INVOKE_PERMISSIONS=""
#### Auth Client Config
SMQ_AUTH_URL=auth:9001
+1
View File
@@ -149,6 +149,7 @@ services:
SMQ_AUTH_CALLOUT_CA_CERT: ${SMQ_AUTH_CALLOUT_CA_CERT}
SMQ_AUTH_CALLOUT_CERT: ${SMQ_AUTH_CALLOUT_CERT}
SMQ_AUTH_CALLOUT_KEY: ${SMQ_AUTH_CALLOUT_KEY}
SMQ_AUTH_CALLOUT_INVOKE_PERMISSIONS: ${SMQ_AUTH_CALLOUT_INVOKE_PERMISSIONS}
ports:
- ${SMQ_AUTH_HTTP_PORT}:${SMQ_AUTH_HTTP_PORT}
- ${SMQ_AUTH_GRPC_PORT}:${SMQ_AUTH_GRPC_PORT}