-
Notifications
You must be signed in to change notification settings - Fork 672
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
SMQ - 2724 - Add Auth Callout #2731
Merged
+578
−123
Merged
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
17600f2
chore: update go modules
rodneyosodo 982941c
feat(auth-callout): Enable auth to make external requests for differe…
rodneyosodo f48bd47
fix: add content type to header
rodneyosodo 71b61b4
fix: don't use go routines if there is one URL
rodneyosodo b39ec62
style: add copyright license
rodneyosodo 44cffec
style: fix ineffectual assignment to err and add period at the end
rodneyosodo 7f51841
fix: remove content type from GET request
rodneyosodo d8623e6
fix(auth-callout): only support GET and POST http methods
rodneyosodo 0813030
fix(auth-callout): use one url and others as fallabcks
rodneyosodo f2bdfd3
fix(auth-callout): use sdk error
rodneyosodo f663082
fix(auth-callout): not pass method as function paramter
rodneyosodo 8cf805a
chore: update go modules
rodneyosodo 425f408
feat(auth-callout): Add request timeout
rodneyosodo cdceed7
feat(auth-callout): Add support for custom certificates for HTTS requ…
rodneyosodo ff4fbff
chore: update go modules
rodneyosodo 17cf207
style: fix linter
rodneyosodo 8fe5350
docs(auth-callback): Add comment showing why we use first positive re…
rodneyosodo 2e7f687
test(pats): use globally defined errors
rodneyosodo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
// Copyright (c) Abstract Machines | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package auth | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"net/http" | ||
"net/url" | ||
|
||
"github.com/absmach/supermq/pkg/errors" | ||
svcerr "github.com/absmach/supermq/pkg/errors/service" | ||
"github.com/absmach/supermq/pkg/policies" | ||
) | ||
|
||
type callback struct { | ||
httpClient *http.Client | ||
urls []string | ||
method string | ||
} | ||
|
||
// CallBack send auth request to an external service. | ||
// | ||
//go:generate mockery --name CallBack --output=./mocks --filename callback.go --quiet --note "Copyright (c) Abstract Machines" | ||
type CallBack interface { | ||
Authorize(ctx context.Context, pr policies.Policy) error | ||
} | ||
|
||
// NewCallback creates a new instance of CallBack. | ||
func NewCallback(httpClient *http.Client, method string, urls []string) (CallBack, error) { | ||
if httpClient == nil { | ||
httpClient = http.DefaultClient | ||
} | ||
if method != http.MethodPost && method != http.MethodGet { | ||
return nil, fmt.Errorf("unsupported auth callback method: %s", method) | ||
} | ||
|
||
return &callback{ | ||
httpClient: httpClient, | ||
urls: urls, | ||
method: method, | ||
}, nil | ||
} | ||
|
||
func (c *callback) Authorize(ctx context.Context, pr policies.Policy) error { | ||
if len(c.urls) == 0 { | ||
return nil | ||
} | ||
|
||
payload := map[string]string{ | ||
"domain": pr.Domain, | ||
"subject": pr.Subject, | ||
"subject_type": pr.SubjectType, | ||
"subject_kind": pr.SubjectKind, | ||
"subject_relation": pr.SubjectRelation, | ||
"object": pr.Object, | ||
"object_type": pr.ObjectType, | ||
"object_kind": pr.ObjectKind, | ||
"relation": pr.Relation, | ||
"permission": pr.Permission, | ||
} | ||
|
||
var err error | ||
// We use a single URL at a time and others as fallbacks | ||
// the first positive result returned by a callback in the chain is considered to be final | ||
for i := range c.urls { | ||
if err = c.makeRequest(ctx, c.urls[i], payload); err == nil { | ||
return nil | ||
} | ||
} | ||
|
||
return err | ||
} | ||
|
||
func (c *callback) makeRequest(ctx context.Context, urlStr string, params map[string]string) error { | ||
var req *http.Request | ||
var err error | ||
|
||
switch c.method { | ||
case http.MethodGet: | ||
query := url.Values{} | ||
for key, value := range params { | ||
query.Set(key, value) | ||
} | ||
req, err = http.NewRequestWithContext(ctx, c.method, urlStr+"?"+query.Encode(), nil) | ||
case http.MethodPost: | ||
data, jsonErr := json.Marshal(params) | ||
if jsonErr != nil { | ||
return jsonErr | ||
} | ||
req, err = http.NewRequestWithContext(ctx, c.method, urlStr, bytes.NewReader(data)) | ||
req.Header.Set("Content-Type", "application/json") | ||
} | ||
|
||
if err != nil { | ||
return err | ||
} | ||
|
||
resp, err := c.httpClient.Do(req) | ||
if err != nil { | ||
return err | ||
} | ||
defer resp.Body.Close() | ||
|
||
if resp.StatusCode != http.StatusOK { | ||
return errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, resp.StatusCode) | ||
} | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
// Copyright (c) Abstract Machines | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package auth_test | ||
|
||
import ( | ||
"context" | ||
"net/http" | ||
"net/http/httptest" | ||
"testing" | ||
|
||
"github.com/absmach/supermq/auth" | ||
"github.com/absmach/supermq/pkg/errors" | ||
svcerr "github.com/absmach/supermq/pkg/errors/service" | ||
"github.com/absmach/supermq/pkg/policies" | ||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestCallback_Authorize(t *testing.T) { | ||
policy := policies.Policy{ | ||
Domain: "test-domain", | ||
Subject: "test-subject", | ||
SubjectType: "user", | ||
SubjectKind: "individual", | ||
SubjectRelation: "owner", | ||
Object: "test-object", | ||
ObjectType: "message", | ||
ObjectKind: "event", | ||
Relation: "publish", | ||
Permission: "allow", | ||
} | ||
|
||
cases := []struct { | ||
desc string | ||
method string | ||
respStatus int | ||
expectError bool | ||
}{ | ||
{ | ||
desc: "successful GET authorization", | ||
method: http.MethodGet, | ||
respStatus: http.StatusOK, | ||
expectError: false, | ||
}, | ||
{ | ||
desc: "successful POST authorization", | ||
method: http.MethodPost, | ||
respStatus: http.StatusOK, | ||
expectError: false, | ||
}, | ||
{ | ||
desc: "failed authorization", | ||
method: http.MethodPost, | ||
respStatus: http.StatusForbidden, | ||
expectError: true, | ||
}, | ||
} | ||
|
||
for _, tc := range cases { | ||
t.Run(tc.desc, func(t *testing.T) { | ||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
assert.Equal(t, tc.method, r.Method) | ||
|
||
if tc.method == http.MethodGet { | ||
query := r.URL.Query() | ||
assert.Equal(t, policy.Domain, query.Get("domain")) | ||
assert.Equal(t, policy.Subject, query.Get("subject")) | ||
} | ||
|
||
w.WriteHeader(tc.respStatus) | ||
})) | ||
defer ts.Close() | ||
|
||
cb, err := auth.NewCallback(http.DefaultClient, tc.method, []string{ts.URL}) | ||
assert.NoError(t, err) | ||
err = cb.Authorize(context.Background(), policy) | ||
|
||
if tc.expectError { | ||
assert.Error(t, err) | ||
assert.True(t, errors.Contains(err, svcerr.ErrAuthorization), "expected authorization error") | ||
} else { | ||
assert.NoError(t, err) | ||
} | ||
}) | ||
} | ||
} | ||
|
||
func TestCallback_MultipleURLs(t *testing.T) { | ||
ts1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
w.WriteHeader(http.StatusOK) | ||
})) | ||
defer ts1.Close() | ||
|
||
ts2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
w.WriteHeader(http.StatusOK) | ||
})) | ||
defer ts2.Close() | ||
|
||
cb, err := auth.NewCallback(http.DefaultClient, http.MethodPost, []string{ts1.URL, ts2.URL}) | ||
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"}) | ||
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"}) | ||
assert.Error(t, err) | ||
} | ||
|
||
func TestCallback_CancelledContext(t *testing.T) { | ||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
w.WriteHeader(http.StatusOK) | ||
})) | ||
defer ts.Close() | ||
|
||
ctx, cancel := context.WithCancel(context.Background()) | ||
cancel() | ||
|
||
cb, err := auth.NewCallback(http.DefaultClient, http.MethodPost, []string{ts.URL}) | ||
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"}) | ||
assert.NoError(t, err) | ||
assert.NotNil(t, cb) | ||
} | ||
|
||
func TestCallback_NoURL(t *testing.T) { | ||
cb, err := auth.NewCallback(http.DefaultClient, http.MethodPost, []string{}) | ||
assert.NoError(t, err) | ||
err = cb.Authorize(context.Background(), policies.Policy{}) | ||
assert.NoError(t, err) | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add an explanation comment here.