mirror of https://github.com/hashicorp/boundary
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.
385 lines
10 KiB
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())
|
|
})
|
|
}
|
|
}
|