* Move the server daemons into their own package With this change the internal/server package now is dedicated to the server domain (workers and controllers) and not to the server daemons. The hope is that this will make it easier to keep the daemons decoupled from the representation of the servers in the database.
344 lines
9.1 KiB
Go
344 lines
9.1 KiB
Go
package controller
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/hashicorp/boundary/internal/daemon/controller/handlers"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestAuthenticationHandler(t *testing.T) {
|
|
c := NewTestController(t, &TestControllerOpts{
|
|
DisableAuthorizationFailures: true,
|
|
DefaultPasswordAuthMethodId: "ampw_1234567890",
|
|
DefaultLoginName: "admin",
|
|
DefaultPassword: "password123",
|
|
})
|
|
defer c.Shutdown()
|
|
|
|
request := map[string]interface{}{
|
|
"attributes": map[string]interface{}{
|
|
"login_name": "admin",
|
|
"password": "password123",
|
|
},
|
|
}
|
|
// No "type" defined means "token" type
|
|
b, err := json.Marshal(request)
|
|
require.NoError(t, err)
|
|
|
|
resp, err := http.Post(fmt.Sprintf("%s/v1/auth-methods/ampw_1234567890:authenticate", c.ApiAddrs()[0]), "application/json", bytes.NewReader(b))
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode, "Got response: %v", resp)
|
|
|
|
b, err = ioutil.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
body := make(map[string]interface{})
|
|
require.NoError(t, json.Unmarshal(b, &body))
|
|
|
|
require.Contains(t, body, "attributes")
|
|
attrs := body["attributes"].(map[string]interface{})
|
|
pubId, tok := attrs["id"].(string), attrs["token"].(string)
|
|
assert.NotEmpty(t, pubId)
|
|
assert.NotEmpty(t, tok)
|
|
assert.Truef(t, strings.HasPrefix(tok, pubId), "Token: %q, Id: %q", tok, pubId)
|
|
|
|
// Set the token type to cookie and make sure the body does not contain the token anymore.
|
|
request["type"] = "cookie"
|
|
b, err = json.Marshal(request)
|
|
require.NoError(t, err)
|
|
resp, err = http.Post(fmt.Sprintf("%s/v1/auth-methods/ampw_1234567890:authenticate", c.ApiAddrs()[0]), "application/json", bytes.NewReader(b))
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode, "Got response: %v", resp)
|
|
|
|
b, err = ioutil.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
body = make(map[string]interface{})
|
|
require.NoError(t, json.Unmarshal(b, &body))
|
|
|
|
attrs = body["attributes"].(map[string]interface{})
|
|
|
|
require.Contains(t, attrs, "id")
|
|
require.Contains(t, attrs, "auth_method_id")
|
|
require.Contains(t, attrs, "user_id")
|
|
require.NotContains(t, attrs, "token")
|
|
|
|
cookies := make(map[string]*http.Cookie)
|
|
for _, c := range resp.Cookies() {
|
|
cookies[c.Name] = c
|
|
}
|
|
require.Contains(t, cookies, handlers.HttpOnlyCookieName)
|
|
require.Contains(t, cookies, handlers.JsVisibleCookieName)
|
|
assert.NotEmpty(t, cookies[handlers.HttpOnlyCookieName].Value)
|
|
assert.NotEmpty(t, cookies[handlers.JsVisibleCookieName].Value)
|
|
assert.True(t, cookies[handlers.HttpOnlyCookieName].HttpOnly)
|
|
assert.False(t, cookies[handlers.JsVisibleCookieName].HttpOnly)
|
|
tok = cookies[handlers.JsVisibleCookieName].Value
|
|
|
|
pubId = attrs["id"].(string)
|
|
assert.NotEmpty(t, pubId)
|
|
assert.Truef(t, strings.HasPrefix(tok, pubId), "Token: %q, Id: %q", tok, pubId)
|
|
}
|
|
|
|
func TestHandleImplementedPaths(t *testing.T) {
|
|
c := NewTestController(t, &TestControllerOpts{
|
|
DisableAuthorizationFailures: true,
|
|
})
|
|
defer c.Shutdown()
|
|
|
|
for verb, paths := range map[string][]string{
|
|
"GET": {
|
|
"v1/accounts",
|
|
"v1/accounts/someid",
|
|
"v1/auth-methods",
|
|
"v1/auth-methods/someid",
|
|
"v1/auth-methods/someid:authenticate:callback",
|
|
"v1/auth-tokens",
|
|
"v1/auth-tokens/someid",
|
|
"v1/credential-stores",
|
|
"v1/credential-stores/someid",
|
|
"v1/groups",
|
|
"v1/groups/someid",
|
|
"v1/host-catalogs",
|
|
"v1/host-catalogs/someid",
|
|
"v1/host-sets",
|
|
"v1/host-sets/someid",
|
|
"v1/hosts",
|
|
"v1/hosts/someid",
|
|
"v1/roles",
|
|
"v1/roles/someid",
|
|
"400_v1/sc\u200Bopes",
|
|
"200_v1/scopes",
|
|
"v1/scopes/someid",
|
|
"v1/sessions",
|
|
"v1/sessions/someid",
|
|
"v1/targets",
|
|
"v1/targets/someid",
|
|
"v1/users",
|
|
"v1/users/someid",
|
|
},
|
|
"POST": {
|
|
// Creation end points
|
|
"v1/accounts",
|
|
"v1/auth-methods",
|
|
"v1/credential-stores",
|
|
"v1/groups",
|
|
"v1/host-catalogs",
|
|
"v1/host-sets",
|
|
"v1/hosts",
|
|
"v1/roles",
|
|
"v1/scopes",
|
|
"v1/targets",
|
|
"v1/users",
|
|
|
|
// custom methods
|
|
"v1/accounts/someid:set-password",
|
|
"v1/accounts/someid:change-password",
|
|
"v1/auth-methods/someid:authenticate",
|
|
"v1/groups/someid:add-members",
|
|
"v1/groups/someid:set-members",
|
|
"v1/groups/someid:remove-members",
|
|
"v1/host-sets/someid:add-hosts",
|
|
"v1/host-sets/someid:remove-hosts",
|
|
"v1/host-sets/someid:set-hosts",
|
|
"v1/roles/someid:add-grants",
|
|
"v1/roles/someid:set-grants",
|
|
"v1/roles/someid:remove-grants",
|
|
"v1/roles/someid:add-principals",
|
|
"v1/roles/someid:set-principals",
|
|
"v1/roles/someid:remove-principals",
|
|
"v1/sessions/someid:cancel",
|
|
"v1/targets/someid:authorize-session",
|
|
"v1/targets/someid:add-host-sets",
|
|
"v1/targets/someid:set-host-sets",
|
|
"v1/targets/someid:remove-host-sets",
|
|
"v1/users/someid:add-accounts",
|
|
"v1/users/someid:set-accounts",
|
|
"v1/users/someid:remove-accounts",
|
|
},
|
|
"DELETE": {
|
|
"v1/accounts/someid",
|
|
"v1/auth-methods/someid",
|
|
"v1/auth-tokens/someid",
|
|
"v1/credential-stores/someid",
|
|
"v1/groups/someid",
|
|
"v1/host-catalogs/someid",
|
|
"v1/host-sets/someid",
|
|
"v1/hosts/someid",
|
|
"v1/roles/someid",
|
|
"v1/scopes/someid",
|
|
"v1/targets/someid",
|
|
"v1/users/someid",
|
|
},
|
|
"PATCH": {
|
|
"v1/accounts/someid",
|
|
"v1/auth-methods/someid",
|
|
"v1/credential-stores/someid",
|
|
"v1/groups/someid",
|
|
"v1/host-catalogs/someid",
|
|
"v1/host-sets/someid",
|
|
"v1/hosts/someid",
|
|
"v1/roles/someid",
|
|
"v1/scopes/someid",
|
|
"v1/targets/someid",
|
|
"v1/users/someid",
|
|
},
|
|
} {
|
|
for _, p := range paths {
|
|
t.Run(fmt.Sprintf("%s/%s", verb, p), func(t *testing.T) {
|
|
var expCode int
|
|
if !strings.HasPrefix(p, "v1/") {
|
|
sp := strings.Split(p, "_")
|
|
require.Len(t, sp, 2)
|
|
switch sp[0] {
|
|
case "400":
|
|
expCode = http.StatusBadRequest
|
|
case "200":
|
|
expCode = http.StatusOK
|
|
}
|
|
p = sp[1]
|
|
}
|
|
|
|
url := fmt.Sprintf("%s/%s", c.ApiAddrs()[0], p)
|
|
req, err := http.NewRequest(verb, url, nil)
|
|
require.NoError(t, err)
|
|
resp, err := http.DefaultClient.Do(req)
|
|
require.NoError(t, err)
|
|
if expCode != 0 {
|
|
assert.Equal(t, expCode, resp.StatusCode)
|
|
}
|
|
assert.NotEqualf(t, resp.StatusCode, http.StatusNotFound, "Got response %v, wanted not 404", resp.StatusCode)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCallbackInterceptor(t *testing.T) {
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
require.NoError(t, err)
|
|
defer func() {
|
|
// Ignore errors as a normal shutdown will also close the listener when
|
|
// the server Shutdown is called. This is just in case.
|
|
_ = listener.Close()
|
|
}()
|
|
|
|
noopHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Body == nil {
|
|
w.WriteHeader(http.StatusOK)
|
|
return
|
|
}
|
|
var buf bytes.Buffer
|
|
read, err := buf.ReadFrom(r.Body)
|
|
require.NoError(t, err)
|
|
written, err := w.Write(buf.Bytes())
|
|
require.NoError(t, err)
|
|
require.EqualValues(t, read, written)
|
|
})
|
|
|
|
server := &http.Server{
|
|
Handler: wrapHandlerWithCallbackInterceptor(noopHandler, nil),
|
|
}
|
|
|
|
// Use error channel so that we can use test assertions on the returned error.
|
|
// It is illegal to call `t.FailNow()` from a goroutine.
|
|
// https://pkg.go.dev/testing#T.FailNow
|
|
errChan := make(chan error)
|
|
go func() {
|
|
errChan <- server.Serve(listener)
|
|
}()
|
|
t.Cleanup(func() {
|
|
if err := <-errChan; err != http.ErrServerClosed {
|
|
require.NoError(t, err)
|
|
}
|
|
})
|
|
|
|
testCases := []struct {
|
|
name string
|
|
path string
|
|
args url.Values
|
|
wantJson *cmdAttrs
|
|
}{
|
|
{
|
|
name: "not callback, no args",
|
|
path: "v1/auth-methods/ampw_1234567890:read",
|
|
},
|
|
{
|
|
name: "not callback, with args",
|
|
path: "v1/auth-methods/ampw_1234567890:read",
|
|
args: url.Values{
|
|
"state": []string{"fooBar"},
|
|
"token": []string{"barFoo"},
|
|
},
|
|
},
|
|
{
|
|
name: "callback, no args",
|
|
path: "v1/auth-methods/ampw_1234567890:authenticate:callback",
|
|
wantJson: &cmdAttrs{Command: "callback"},
|
|
},
|
|
{
|
|
name: "callback, invalid pattern",
|
|
path: "v1/auth-methods/ampw_1234567890:read:callback",
|
|
},
|
|
{
|
|
name: "callback, with args",
|
|
path: "v1/auth-methods/ampw_1234567890:authenticate:callback",
|
|
args: url.Values{
|
|
"state": []string{"fooBar"},
|
|
"token": []string{"barFoo"},
|
|
},
|
|
wantJson: &cmdAttrs{
|
|
Command: "callback",
|
|
Attributes: map[string]interface{}{
|
|
"state": "fooBar",
|
|
"token": "barFoo",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
req, err := http.NewRequest(http.MethodGet,
|
|
fmt.Sprintf("http://%s/%s", listener.Addr().String(), tc.path),
|
|
nil)
|
|
require.NoError(err)
|
|
|
|
if tc.args != nil {
|
|
req.URL.RawQuery = tc.args.Encode()
|
|
}
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
require.NoError(err)
|
|
require.EqualValues(http.StatusOK, resp.StatusCode)
|
|
|
|
if tc.wantJson != nil {
|
|
require.NotNil(resp.Body)
|
|
defer resp.Body.Close()
|
|
wantJsonJson, err := json.Marshal(tc.wantJson)
|
|
require.NoError(err)
|
|
var buf bytes.Buffer
|
|
_, err = buf.ReadFrom(resp.Body)
|
|
require.NoError(err)
|
|
require.Equal(wantJsonJson, buf.Bytes())
|
|
} else {
|
|
require.Equal(http.NoBody, resp.Body)
|
|
}
|
|
})
|
|
}
|
|
|
|
require.NoError(t, server.Shutdown(context.Background()))
|
|
}
|