You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
boundary/internal/auth/auth_test.go

385 lines
10 KiB

package auth
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/hashicorp/watchtower/internal/authtoken"
"github.com/hashicorp/watchtower/internal/db"
"github.com/hashicorp/watchtower/internal/iam"
"github.com/hashicorp/watchtower/internal/types/action"
"github.com/hashicorp/watchtower/internal/types/resource"
"github.com/kr/pretty"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestHandler_AuthDecoration(t *testing.T) {
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
wrapper := db.TestWrapper(t)
iamRepo, err := iam.NewRepository(rw, rw, wrapper)
require.NoError(t, err)
iamRepoFn := func() (*iam.Repository, error) {
return iamRepo, nil
}
org, proj := iam.TestScopes(t, conn)
cases := []struct {
name string
path string
method string
action action.Type
scope string
resource resource.Type
id string
pin string
wantErrContains string
}{
{
name: "global scope, read, with id",
path: "/v1/scopes/global/users/u_anon",
method: "GET",
action: action.Read,
scope: "global",
resource: resource.User,
id: "u_anon",
},
{
name: "global scope, update, with id",
path: "/v1/scopes/global/auth-methods/am_1234",
method: "PATCH",
action: action.Update,
scope: "global",
resource: resource.AuthMethod,
id: "am_1234",
},
{
name: "global scope, delete org",
path: "/v1/scopes/o_1234",
method: "DELETE",
action: action.Delete,
scope: "global",
resource: resource.Scope,
id: "o_1234",
},
{
name: "org scope, delete project",
path: fmt.Sprintf("/v1/scopes/%s", proj.GetPublicId()),
method: "DELETE",
action: action.Delete,
scope: org.GetPublicId(),
resource: resource.Scope,
id: proj.GetPublicId(),
},
{
name: "empty segments",
path: "/v1/scopes/o_abc1234/auth-methods//am_1234",
wantErrContains: "empty segment found",
},
{
name: "global scope, create",
path: "/v1/scopes/global/roles",
method: "POST",
action: action.Create,
scope: "global",
resource: resource.Role,
},
{
name: "global, invalid collection syntax",
path: "/v1/scope",
wantErrContains: `parse auth params: invalid first segment "scope"`,
},
{
name: "invalid scopes segment with id",
path: "/v1/scope/o_abc",
wantErrContains: `parse auth params: invalid first segment "scope"`,
},
{
name: "org, custom action",
path: "/v1/scopes/o_123/auth-methods/am_1234:authenticate",
method: "POST",
action: action.Authenticate,
scope: "o_123",
resource: resource.AuthMethod,
id: "am_1234",
},
{
name: "unknown action in first segment",
path: "/v1/:authentifake",
wantErrContains: `parse auth params: invalid first segment ":authentifake"`,
},
{
name: "root, unknown action",
path: "/v1/scopes/o_abc/:authentifake",
wantErrContains: `parse auth params: unknown action "authentifake"`,
},
{
name: "root, unknown empty action",
path: "/v1/:",
wantErrContains: `parse auth params: invalid first segment ":"`,
},
{
name: "root, invalid method",
path: "/v1/scopes/o_abc/:authenticate",
method: "FOOBAR",
wantErrContains: `unknown method "FOOBAR"`,
},
{
name: "root, wrong number of colons",
path: "/v1/scopes/o_abc/:auth:enticate",
wantErrContains: "unexpected number of colons",
},
{
name: "root, action on global",
path: "/v1/scopes/global",
method: "GET",
action: action.Read,
scope: "global",
resource: resource.Scope,
id: "global",
},
{
name: "org scope, valid",
path: "/v1/scopes/o_abc123/auth-methods",
method: "POST",
action: action.Create,
scope: "o_abc123",
resource: resource.AuthMethod,
},
{
name: "project scope, valid",
path: "/v1/scopes/p_1234/host-catalogs",
method: "POST",
action: action.Create,
scope: "p_1234",
resource: resource.HostCatalog,
},
{
name: "project scope, action on project",
path: "/v1/scopes/p_1234/:deauthenticate",
method: "POST",
action: action.Deauthenticate,
scope: "p_1234",
resource: resource.Scope,
id: "p_1234",
},
{
name: "global scope, action on org, token scope",
path: "/v1/scopes/o_1234:set-principals",
method: "POST",
action: action.SetPrincipals,
scope: "global",
resource: resource.Scope,
id: "o_1234",
},
{
name: "org scope, get on collection is list",
path: "/v1/scopes/o_abc123/projects",
action: action.List,
scope: "o_abc123",
resource: resource.Project,
},
{
name: "global action, valid",
path: "/v1/scopes/global/:read",
action: action.Read,
scope: "global",
resource: resource.Scope,
id: "global",
},
{
name: "top level, invalid",
path: "/v1/",
wantErrContains: `invalid first segment "v1"`,
},
{
name: "non-api path",
path: "/",
wantErrContains: `parse auth params: invalid first segment ""`,
},
{
name: "project scope, pinning collection",
path: "/v1/scopes/p_1234/host-catalogs/hc_1234/host-sets",
action: action.List,
scope: "p_1234",
pin: "hc_1234",
resource: resource.HostSet,
},
{
name: "project scope, pinning collection, custom action",
path: "/v1/scopes/p_1234/host-catalogs/hc_1234/host-sets:create",
action: action.Create,
scope: "p_1234",
pin: "hc_1234",
resource: resource.HostSet,
},
{
name: "project scope, pinning id",
path: "/v1/scopes/p_1234/host-catalogs/hc_1234/host-sets/hs_abc",
action: action.Read,
id: "hs_abc",
scope: "p_1234",
pin: "hc_1234",
resource: resource.HostSet,
},
{
name: "project scope, pinning id, custom action",
path: "/v1/scopes/p_1234/host-catalogs/hc_1234/host-sets/hs_abc:update",
action: action.Update,
id: "hs_abc",
scope: "p_1234",
pin: "hc_1234",
resource: resource.HostSet,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
require, assert := require.New(t), assert.New(t)
if tc.method == "" {
tc.method = "GET"
}
req, err := http.NewRequest(tc.method, fmt.Sprintf("http://127.0.0.1:9200:%s", tc.path), nil)
require.NoError(err)
v := &verifier{
requestInfo: RequestInfo{
Path: req.URL.Path,
Method: tc.method,
},
iamRepoFn: iamRepoFn,
}
err = v.parseAuthParams()
if tc.wantErrContains != "" {
require.Error(err)
assert.Contains(err.Error(), tc.wantErrContains, err.Error())
return
}
require.NoError(err, pretty.Sprint(v))
if tc.path == "/" {
return
}
require.NotNil(v.res)
require.NotEqual(action.Unknown, v.act)
assert.Equal(tc.scope, v.res.ScopeId, "scope "+pretty.Sprint(v))
assert.Equal(tc.action, v.act, "action "+pretty.Sprint(v))
assert.Equal(tc.resource, v.res.Type, "type "+pretty.Sprint(v))
assert.Equal(tc.id, v.res.Id, "id "+pretty.Sprint(v))
assert.Equal(tc.pin, v.res.Pin, "pin "+pretty.Sprint(v))
})
}
}
func TestAuthTokenAuthenticator(t *testing.T) {
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
wrapper := db.TestWrapper(t)
tokenRepo, err := authtoken.NewRepository(rw, rw, wrapper)
require.NoError(t, err)
iamRepo, err := iam.NewRepository(rw, rw, wrapper)
require.NoError(t, err)
tokenRepoFn := func() (*authtoken.Repository, error) {
return tokenRepo, nil
}
iamRepoFn := func() (*iam.Repository, error) {
return iamRepo, nil
}
o, _ := iam.TestScopes(t, conn)
at := authtoken.TestAuthToken(t, conn, wrapper, o.GetPublicId())
tokValue := at.GetPublicId() + "_" + at.GetToken()
jsCookieVal, httpCookieVal := tokValue[:len(tokValue)/2], tokValue[len(tokValue)/2:]
cases := []struct {
name string
headers map[string]string
cookies []http.Cookie
userId string
tokenFormat TokenFormat
}{
{
name: "Empty headers",
headers: map[string]string{},
tokenFormat: AuthTokenTypeUnknown,
},
{
name: "Bearer token",
headers: map[string]string{"Authorization": fmt.Sprintf("Bearer %s", tokValue)},
userId: at.GetIamUserId(),
tokenFormat: AuthTokenTypeBearer,
},
{
name: "Split cookie token",
cookies: []http.Cookie{
{Name: HttpOnlyCookieName, Value: httpCookieVal},
{Name: JsVisibleCookieName, Value: jsCookieVal},
},
userId: at.GetIamUserId(),
tokenFormat: AuthTokenTypeSplitCookie,
},
{
name: "Split cookie token only http cookie",
cookies: []http.Cookie{
{Name: HttpOnlyCookieName, Value: httpCookieVal},
},
tokenFormat: AuthTokenTypeUnknown,
},
{
name: "Split cookie token only js cookie",
cookies: []http.Cookie{
{Name: JsVisibleCookieName, Value: jsCookieVal},
},
tokenFormat: AuthTokenTypeUnknown,
},
{
name: "Cookie and auth header",
headers: map[string]string{"Authorization": fmt.Sprintf("Bearer %s", tokValue)},
cookies: []http.Cookie{
{Name: HttpOnlyCookieName, Value: httpCookieVal},
{Name: JsVisibleCookieName, Value: jsCookieVal},
},
userId: at.GetIamUserId(),
tokenFormat: AuthTokenTypeBearer,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
req := httptest.NewRequest("GET", "http://127.0.0.1/v1/scopes/o_1", nil)
for k, v := range tc.headers {
req.Header.Set(k, v)
}
for _, c := range tc.cookies {
req.AddCookie(&c)
}
// Add values for authn/authz checking
requestInfo := RequestInfo{
Path: req.URL.Path,
Method: req.Method,
}
requestInfo.PublicId, requestInfo.Token, requestInfo.TokenFormat = GetTokenFromRequest(nil, req)
assert.Equal(t, tc.tokenFormat, requestInfo.TokenFormat)
if tc.userId == "" {
return
}
ctx := NewVerifierContext(context.Background(), nil, iamRepoFn, tokenRepoFn, requestInfo)
v, ok := ctx.Value(verifierKey).(*verifier)
require.True(t, ok)
require.NotNil(t, v)
at, err := tokenRepo.ValidateToken(ctx, v.requestInfo.PublicId, v.requestInfo.Token)
require.NoError(t, err)
assert.Equal(t, tc.userId, at.GetIamUserId())
})
}
}