Authenticate Handler API and SDK, and much more (#183)

This commit is contained in:
Todd Knight authored and GitHub committed 2020-07-14 17:32:47 -04:00
1 parent acce195dcb
commit 28dbcd84bd
89 files changed
+5296 -1943

No files matched your search

+7 -4
View File
@@ -1,14 +1,17 @@
package controller
import (
"context"
"github.com/hashicorp/watchtower/internal/cmd/base"
"github.com/hashicorp/watchtower/internal/cmd/config"
)
type Config struct {
// The base Server object, containing things shared between Controllers and
// Workers
*base.Server
RawConfig *config.Config
BaseContext context.Context
// The underlying configuration, passed in here to avoid duplicating values
// everywhere
RawConfig *config.Config
// If set, authorization checking occurrs but failures are ignored
DisableAuthorizationFailures bool
}
+8 -5
View File
@@ -6,6 +6,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/hashicorp/watchtower/internal/cmd/config"
)
@@ -65,8 +66,9 @@ func TestHandler_CORS(t *testing.T) {
t.Fatal(err)
}
tc := NewTestController(t, &TestControllerOpts{
Config: cfg,
DefaultOrgId: "o_1234567890",
Config: cfg,
DefaultOrgId: "o_1234567890",
DisableAuthorizationFailures: true,
})
defer tc.Shutdown()
@@ -161,12 +163,13 @@ func TestHandler_CORS(t *testing.T) {
t.Run(c.name, func(t *testing.T) {
// Create a client with the right address
client := tc.Client()
client.SetAddr(tc.ApiAddrs()[c.listenerNum-1])
err := client.SetAddr(tc.ApiAddrs()[c.listenerNum-1])
require.NoError(t, err)
client.SetOrg("o_1234567890")
// Create the request
req, err := client.NewRequest(tc.Context(), c.method, "projects", nil)
assert.NoError(t, err)
require.NoError(t, err)
// Append headers
if c.origin != "" {
@@ -178,7 +181,7 @@ func TestHandler_CORS(t *testing.T) {
// Run the request, do basic checks
resp, err := client.Do(req)
assert.NoError(t, err)
require.NoError(t, err)
assert.Equal(t, c.code, resp.HttpResponse().StatusCode)
// If options and we expect it to be successful, run some checks
+349 -22
View File
@@ -3,9 +3,10 @@ package controller
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"path"
"os"
"strings"
"time"
@@ -15,7 +16,9 @@ import (
"github.com/hashicorp/watchtower/api"
"github.com/hashicorp/watchtower/globals"
"github.com/hashicorp/watchtower/internal/gen/controller/api/services"
"github.com/hashicorp/watchtower/internal/perms"
"github.com/hashicorp/watchtower/internal/servers/controller/handlers"
"github.com/hashicorp/watchtower/internal/servers/controller/handlers/authenticate"
"github.com/hashicorp/watchtower/internal/servers/controller/handlers/groups"
"github.com/hashicorp/watchtower/internal/servers/controller/handlers/host_catalogs"
"github.com/hashicorp/watchtower/internal/servers/controller/handlers/host_sets"
@@ -24,6 +27,10 @@ import (
"github.com/hashicorp/watchtower/internal/servers/controller/handlers/projects"
"github.com/hashicorp/watchtower/internal/servers/controller/handlers/roles"
"github.com/hashicorp/watchtower/internal/servers/controller/handlers/users"
"github.com/hashicorp/watchtower/internal/types/action"
"github.com/hashicorp/watchtower/internal/types/resource"
"github.com/hashicorp/watchtower/internal/types/scope"
"github.com/kr/pretty"
)
type HandlerProperties struct {
@@ -54,8 +61,7 @@ func handleGrpcGateway(c *Controller) (http.Handler, error) {
// Register*ServiceHandlerServer methods ignore the passed in ctx. Using the baseContext now just in case this changes
// in the future, at which point we'll want to be using the baseContext.
ctx := c.baseContext
mux := runtime.NewServeMux(runtime.WithMetadata(handlers.TokenAuthenticator(c.logger, c.AuthTokenRepoFn)),
runtime.WithProtoErrorHandler(handlers.ErrorHandler(c.logger)))
mux := runtime.NewServeMux(runtime.WithProtoErrorHandler(handlers.ErrorHandler(c.logger)))
hcs, err := host_catalogs.NewService(c.StaticHostRepoFn)
if err != nil {
return nil, fmt.Errorf("failed to create host catalog handler service: %w", err)
@@ -69,6 +75,13 @@ func handleGrpcGateway(c *Controller) (http.Handler, error) {
if err := services.RegisterHostServiceHandlerServer(ctx, mux, &hosts.Service{}); err != nil {
return nil, fmt.Errorf("failed to register host service handler: %w", err)
}
auths, err := authenticate.NewService(c.IamRepoFn, c.AuthTokenRepoFn)
if err != nil {
return nil, fmt.Errorf("failed to create authentication handler service: %w", err)
}
if err := services.RegisterAuthenticationServiceHandlerServer(ctx, mux, auths); err != nil {
return nil, fmt.Errorf("failed to register authenticate service handler: %w", err)
}
os, err := orgs.NewService(c.IamRepoFn)
if err != nil {
return nil, fmt.Errorf("failed to create org handler service: %w", err)
@@ -121,27 +134,76 @@ func wrapHandlerWithCommonFuncs(h http.Handler, c *Controller, props HandlerProp
if maxRequestSize == 0 {
maxRequestSize = globals.DefaultMaxRequestSize
}
var defaultOrgId string
if c != nil && c.conf != nil {
defaultOrgId = c.conf.DefaultOrgId
logUrls := os.Getenv("WATCHTOWER_LOG_URLS") != ""
disableAuthzFailures := c.conf.DisableAuthorizationFailures ||
(c.conf.RawConfig.DevController && os.Getenv("WATCHTOWER_DEV_SKIP_AUTHZ") != "")
if disableAuthzFailures {
c.logger.Warn("AUTHORIZATION CHECKING DISABLED")
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if defaultOrgId != "" {
splitPath := strings.Split(r.URL.Path, "/")
if len(splitPath) >= 3 && splitPath[2] == "projects" {
http.Redirect(w, r, path.Join("/v1/orgs", defaultOrgId, strings.Join(splitPath[2:], "/")), 307)
return
}
if logUrls {
c.logger.Trace("request received", "url", r.URL.String())
}
// Set the Cache-Control header for all responses returned
w.Header().Set("Cache-Control", "no-store")
// Start with the request context
ctx := r.Context()
var cancelFunc context.CancelFunc
// Add our timeout
ctx, cancelFunc = context.WithTimeout(ctx, maxRequestDuration)
// Start with the request context and our timeout
ctx, cancelFunc := context.WithTimeout(r.Context(), maxRequestDuration)
defer cancelFunc()
userId := "u_anon"
var res *perms.Resource
var act action.Type
var err error
var authzResults *perms.ACLResults
if r.Method == http.MethodOptions {
// Don't perform authorization checking for preflight requests, at
// least for now. We could add it later if we wanted as an action on
// global or something but likely it would just be enabled/disabled
// per listener instead.
goto AUTHZ_FINISHED
}
// Perform authz checking
res, act, err = decorateAuthParams(r)
if err != nil {
c.logger.Trace("error reading auth parameters from URL", "url", r.URL.Path, "error", err)
// Maybe this isn't the best option, but a URL we can't parse from
// an auth perspective is probably just an invalid URL altogether.
// Treating it as a bad request can be perceived to be less leaky
// than a 404.
w.WriteHeader(http.StatusBadRequest)
return
}
// The resource is only nil with no error if the path is something
// unauthenticated, e.g. the UI serving path.
if res != nil {
authzResults, userId, err = c.performAuthzCheck(ctx, r, res, act)
if err != nil {
c.logger.Error("error during authz check", "error", err)
w.WriteHeader(http.StatusForbidden)
return
}
if userId == "" || !authzResults.Allowed {
// TODO: Decide whether to remove this
if disableAuthzFailures {
c.logger.Info("failed authz info for request", "resource", pretty.Sprint(res), "user_id", userId, "action", act.String())
} else {
w.WriteHeader(http.StatusForbidden)
return
}
}
}
AUTHZ_FINISHED:
// Add the user ID to the context
ctx = context.WithValue(ctx, globals.ContextUserIdValue, userId)
// Add a size limiter if desired
if maxRequestSize > 0 {
ctx = context.WithValue(ctx, "max_request_size", maxRequestSize)
@@ -150,8 +212,6 @@ func wrapHandlerWithCommonFuncs(h http.Handler, c *Controller, props HandlerProp
r = r.WithContext(ctx)
h.ServeHTTP(w, r)
cancelFunc()
return
})
}
@@ -203,8 +263,8 @@ func wrapHandlerWithCors(h http.Handler, props HandlerProperties) http.Handler {
w.WriteHeader(http.StatusForbidden)
err := &api.Error{
Status: api.Int(http.StatusForbidden),
Code: api.String("origin forbidden"),
Status: http.StatusForbidden,
Code: "origin forbidden",
}
enc := json.NewEncoder(w)
@@ -231,10 +291,277 @@ func wrapHandlerWithCors(h http.Handler, props HandlerProperties) http.Handler {
}
h.ServeHTTP(w, req)
return
})
}
func decorateAuthParams(r *http.Request) (*perms.Resource, action.Type, error) {
if r == nil {
return nil, action.Unknown, errors.New("decorate auth params: incoming request is nil")
}
// Remove trailing and leading slashes
trimmedPath := strings.Trim(r.URL.Path, "/")
if !strings.HasPrefix(trimmedPath, "v1") {
// Don't look for auth params for requests to fetch the UI
return nil, action.Unknown, nil
}
splitPath := strings.Split(strings.TrimPrefix(trimmedPath, "v1"), "/")
splitLen := len(splitPath)
if splitLen == 0 {
return nil, action.Unknown, fmt.Errorf("decorate auth params: invalid path")
}
var act action.Type
var typStr string
scp := scope.Global
res := &perms.Resource{
ScopeId: scope.Global.String(),
}
// Handle non-custom types. We'll deal with custom types, including list,
// after parsing the path.
switch r.Method {
case "GET":
act = action.Read
case "POST":
act = action.Create
case "PATCH":
act = action.Update
case "DELETE":
act = action.Delete
default:
return nil, action.Unknown, fmt.Errorf("decorate auth params: unknown method %q", r.Method)
}
// Look for a custom action
colonSplit := strings.Split(splitPath[splitLen-1], ":")
switch len(colonSplit) {
case 1:
// No custom action specified
case 2:
actStr := colonSplit[len(colonSplit)-1]
act = action.Map[actStr]
if act == action.Unknown || act == action.All {
return nil, action.Unknown, fmt.Errorf("decorate auth params: unknown action %q", actStr)
}
// Keep going with the logic without the custom action
splitPath[splitLen-1] = colonSplit[0]
default:
return nil, action.Unknown, fmt.Errorf("decorate auth params: unexpected number of colons in last segment %q", colonSplit[len(colonSplit)-1])
}
// Walk backwards. As we walk backwards we look for scopes and figure out if
// we're operating on a resource or a collection. We also populate the pin.
// The rules for the pin are as follows:
//
// * If the last segment is a collection, the pin is the immediately
// preceding ID
//
// * If the last segment is an ID, the pin is the immediately preceding ID
// not including the last segment
//
// * If at the end of the logic the pin is the id of a scope ("global",
// "o_...", "p_...") then there is no pin. The scopes are already enclosing
// so a pin is redundant.
nextIdIsPin := true
for i := splitLen - 1; i >= 0; i-- {
segment := splitPath[i]
// Collections don't contain underscores; every resource ID does.
segmentIsCollection := !strings.Contains(segment, "_")
if !segmentIsCollection && i != splitLen-1 && nextIdIsPin {
res.Pin = segment
nextIdIsPin = false
}
// Update the scope. Set it to org only if it's at global (that way we
// don't override project with org). We have to check if it's one less
// than the length of the split because operating on the id of a scope
// is actually in the enclosing scope (since you're in the parent scope
// operating on a child scope).
switch segment {
case "projects":
if i < splitLen-2 {
scp = scope.Project
res.ScopeId = splitPath[i+1]
}
case "orgs":
if scp == scope.Global {
if i < splitLen-2 {
scp = scope.Org
res.ScopeId = splitPath[i+1]
}
}
}
if segment == "" {
// This could be the case if we have an action like
// /orgs/o_1234/projects/p_1234/:set-defaults to act on the project
// itself but within its own scope
continue
}
if typStr == "" {
// The resource check takes place inside the type check because if
// we've identified the type we have either already identified the
// right-most resource ID or we're operating on a collection, so
// this prevents us from finding a different ID earlier in the path.
//
// We continue on with the enclosing loop anyways though to ensure
// we find the right scope.
if res.Id == "" && !segmentIsCollection {
res.Id = segment
} else {
// Every collection is the plural of the resource type so drop
// the last 's'
if !strings.HasSuffix(segment, "s") {
return nil, action.Unknown, fmt.Errorf("decorate auth params: invalid collection syntax for %q", segment)
}
typStr = strings.TrimSuffix(segment, "s")
}
}
}
if typStr != "" {
res.Type = resource.Map[typStr]
if res.Type == resource.Unknown {
return nil, action.Unknown, fmt.Errorf("decorate auth params: unknown resource type %q", typStr)
}
} else if res.Id == "" {
return nil, action.Unknown, errors.New("decorate auth params: id and type both not found")
}
// If we're operating on a collection (that is, the ID is blank) and it's a
// GET, it's actually a list
if res.Id == "" && act == action.Read {
act = action.List
}
// If the pin ended up being a scope, nil it out
if res.Pin != "" {
if res.Pin == "global" ||
strings.HasPrefix(res.Pin, "o_") ||
strings.HasPrefix(res.Pin, "p_") {
res.Pin = ""
}
}
return res, act, nil
}
type tokenFormat int
const (
authTokenTypeUnknown tokenFormat = iota
authTokenTypeBearer
authTokenTypeSplitCookie
)
const (
headerAuthMethod = "Authorization"
httpOnlyCookieName = "wt-http-token-cookie"
jsVisibleCookieName = "wt-js-token-cookie"
)
func (c *Controller) performAuthzCheck(ctx context.Context, req *http.Request, res *perms.Resource, act action.Type) (*perms.ACLResults, string, error) {
userId := "u_anon"
if res == nil {
return nil, userId, errors.New("perform authz check: res is nil")
}
var receivedTokenType tokenFormat
var fullToken, token, publicId string
// First, get the token, either from the authorization header or from split
// cookies
{
if authHeader := req.Header.Get("Authorization"); authHeader != "" {
headerSplit := strings.SplitN(strings.TrimSpace(authHeader), " ", 2)
if len(headerSplit) == 2 && strings.EqualFold(strings.TrimSpace(headerSplit[0]), "bearer") {
receivedTokenType = authTokenTypeBearer
fullToken = strings.TrimSpace(headerSplit[1])
}
}
if receivedTokenType != authTokenTypeBearer {
var httpCookiePayload string
var jsCookiePayload string
if hc, err := req.Cookie(httpOnlyCookieName); err == nil {
httpCookiePayload = hc.Value
}
if jc, err := req.Cookie(jsVisibleCookieName); err == nil {
jsCookiePayload = jc.Value
}
if httpCookiePayload != "" && jsCookiePayload != "" {
receivedTokenType = authTokenTypeSplitCookie
fullToken = jsCookiePayload + httpCookiePayload
}
}
if receivedTokenType == authTokenTypeUnknown || fullToken == "" {
// We didn't find auth info or a client screwed up and put in a
// blank header instead of nothing at all, so carry on as the
// anonymous user
goto GRANTSLOOKUP
}
splitFullToken := strings.Split(fullToken, "_")
if len(splitFullToken) != 3 {
return nil, userId, fmt.Errorf("perform authz check: unexpected number of segments in token, expected %d, found %d", 3, len(splitFullToken))
}
token = splitFullToken[2]
publicId = strings.Join(splitFullToken[0:2], "_")
if receivedTokenType == authTokenTypeUnknown || token == "" || publicId == "" {
return nil, userId, fmt.Errorf("perform authz check: after parsing, could not find valid token")
}
}
// Validate the token and fetch the corresponding user ID
{
tokenRepo, err := c.AuthTokenRepoFn()
if err != nil {
return nil, userId, fmt.Errorf("perform authz check: failed to get authtoken repo: %w", err)
}
at, err := tokenRepo.ValidateToken(ctx, publicId, token)
if err != nil {
return nil, userId, fmt.Errorf("perform authz check: failed to validate token: %w", err)
}
if at != nil {
userId = at.GetIamUserId()
}
}
GRANTSLOOKUP:
var parsedGrants []perms.Grant
var grantPairs []perms.GrantPair
// Fetch and parse grants for this user ID
{
iamRepo, err := c.IamRepoFn()
if err != nil {
return nil, userId, fmt.Errorf("perform authz check: failed to get iam repo: %w", err)
}
grantPairs, err = iamRepo.GrantsForUser(ctx, userId)
if err != nil {
return nil, userId, fmt.Errorf("perform authz check: failed to query for user grants: %w", err)
}
parsedGrants = make([]perms.Grant, 0, len(grantPairs))
for _, pair := range grantPairs {
parsed, err := perms.Parse(pair.ScopeId, userId, pair.Grant)
if err != nil {
return nil, userId, fmt.Errorf("perform authz check: failed to parse grant %#v: %w", pair.Grant, err)
}
parsedGrants = append(parsedGrants, parsed)
}
}
acl := perms.NewACL(parsedGrants...)
allowed := acl.Allowed(*res, act)
return &allowed, userId, nil
}
/*
func WrapForwardedForHandler(h http.Handler, authorizedAddrs []*sockaddr.SockAddrMarshaler, rejectNotPresent, rejectNonAuthz bool, hopSkips int) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+251 -4
View File
@@ -2,13 +2,18 @@ package controller
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/grpc-ecosystem/grpc-gateway/runtime"
"github.com/hashicorp/watchtower/internal/gen/controller/api/services"
"github.com/hashicorp/watchtower/internal/types/action"
"github.com/hashicorp/watchtower/internal/types/resource"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -119,6 +124,28 @@ func TestGrpcGatewayRouting(t *testing.T) {
}
}
func TestAuthenticationHandler(t *testing.T) {
c := NewTestController(t, &TestControllerOpts{DefaultOrgId: "o_1234567890"})
defer c.Shutdown()
resp, err := http.Post(fmt.Sprintf("%s/v1/orgs/o_1234567890/auth-methods/am_1234567890:authenticate", c.ApiAddrs()[0]), "application/json",
strings.NewReader(`{"token_type": null, "credentials": {"name":"test", "password": "test"}}`))
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, "id")
require.Contains(t, body, "token")
pubId, tok := body["id"].(string), body["token"].(string)
assert.NotEmpty(t, pubId)
assert.NotEmpty(t, tok)
assert.Truef(t, strings.HasPrefix(tok, pubId), "Token: %q, Id: %q", tok, pubId)
}
func TestGrpcGatewayRouting_CustomActions(t *testing.T) {
ctx := context.Background()
// The unimplemented result indicates the grpc routing is happening successfully otherwise it would return NotFound.
@@ -167,7 +194,9 @@ func TestGrpcGatewayRouting_CustomActions(t *testing.T) {
}
func TestHandleGrpcGateway(t *testing.T) {
c := NewTestController(t, nil)
c := NewTestController(t, &TestControllerOpts{
DisableAuthorizationFailures: true,
})
defer c.Shutdown()
cases := []struct {
@@ -177,12 +206,12 @@ func TestHandleGrpcGateway(t *testing.T) {
}{
{
"Non existent path",
"v1/this-is-made-up",
http.StatusNotFound,
"v1/this-is-made-ups",
http.StatusBadRequest,
},
{
"Unimplemented path",
"v1/orgs/1/projects/2/host-catalogs/3/host-sets/4",
"v1/orgs/1/projects/2/host-catalogs/3/host-sets/hs_4",
http.StatusMethodNotAllowed,
},
}
@@ -200,3 +229,221 @@ func TestHandleGrpcGateway(t *testing.T) {
})
}
}
func TestHandler_AuthDecoration(t *testing.T) {
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/users/u_anon",
method: "GET",
action: action.Read,
scope: "global",
resource: resource.User,
id: "u_anon",
},
{
name: "global scope, update, with id",
path: "/v1/auth-methods/am_1234",
method: "PATCH",
action: action.Update,
scope: "global",
resource: resource.AuthMethod,
id: "am_1234",
},
{
name: "global scope, delete",
path: "/v1/orgs/o_1234",
method: "DELETE",
action: action.Delete,
scope: "global",
resource: resource.Org,
id: "o_1234",
},
{
name: "global scope, create",
path: "/v1/roles",
method: "POST",
action: action.Create,
scope: "global",
resource: resource.Role,
},
{
name: "global, invalid collection syntax",
path: "/v1/org",
wantErrContains: "invalid collection syntax",
},
{
name: "global, custom action",
path: "/v1/orgs/o_123/auth-methods/am_1234:authenticate",
method: "POST",
action: action.Authenticate,
scope: "o_123",
resource: resource.AuthMethod,
id: "am_1234",
},
{
name: "root, unknown action",
path: "/v1/:authentifake",
wantErrContains: "unknown action",
},
{
name: "root, unknown empty action",
path: "/v1/:",
wantErrContains: "unknown action",
},
{
name: "root, invalid method",
path: "/v1/:authenticate",
method: "FOOBAR",
wantErrContains: "unknown method",
},
{
name: "root, wrong number of colons",
path: "/v1/:auth:enticate",
wantErrContains: "unexpected number of colons",
},
{
name: "org scope, valid",
path: "/v1/orgs/o_abc123/auth-methods",
method: "POST",
action: action.Create,
scope: "o_abc123",
resource: resource.AuthMethod,
},
{
name: "project scope, valid",
path: "/v1/orgs/o_abc123/projects/p_1234/host-catalogs",
method: "POST",
action: action.Create,
scope: "p_1234",
resource: resource.HostCatalog,
},
{
name: "project scope, action on project",
path: "/v1/orgs/o_abc123/projects/p_1234/:set-principals",
method: "POST",
action: action.SetPrincipals,
scope: "p_1234",
resource: resource.Project,
id: "p_1234",
},
{
name: "org scope, action on project",
path: "/v1/orgs/o_abc123/projects/p_1234:set-principals",
method: "POST",
action: action.SetPrincipals,
scope: "o_abc123",
resource: resource.Project,
id: "p_1234",
},
{
name: "org scope, get on collection is list",
path: "/v1/orgs/o_abc123/projects",
action: action.List,
scope: "o_abc123",
resource: resource.Project,
},
{
name: "org scope, action on org",
path: "/v1/orgs/o_abc123/:deauthenticate",
action: action.Deauthenticate,
scope: "o_abc123",
resource: resource.Org,
id: "o_abc123",
},
{
name: "top level action, invalid",
path: "/v1/:read",
wantErrContains: "id and type both not found",
},
{
name: "top level, invalid",
path: "/v1/",
wantErrContains: "id and type both not found",
},
{
name: "non-api path",
path: "/",
},
{
name: "project scope, pinning collection",
path: "/v1/orgs/o_abc123/projects/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/orgs/o_abc123/projects/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/orgs/o_abc123/projects/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/orgs/o_abc123/projects/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)
res, act, err := decorateAuthParams(nil)
assert.Nil(res)
require.Error(err)
assert.Equal(action.Unknown, act)
assert.Contains(err.Error(), "incoming request is nil")
res, act, err = decorateAuthParams(req)
if tc.wantErrContains != "" {
require.Error(err)
assert.Contains(err.Error(), tc.wantErrContains, err.Error())
return
}
require.NoError(err)
if tc.path == "/" {
return
}
require.NotNil(res)
require.NotEqual(action.Unknown, act)
assert.Equal(tc.scope, res.ScopeId, "scope")
assert.Equal(tc.action, act, "action")
assert.Equal(tc.resource, res.Type, "type")
assert.Equal(tc.id, res.Id, "id")
assert.Equal(tc.pin, res.Pin, "pin")
})
}
}
@@ -0,0 +1,164 @@
package authenticate
import (
"context"
"crypto/sha256"
"encoding/base32"
"fmt"
"regexp"
"strings"
"sync/atomic"
"github.com/hashicorp/watchtower/internal/authtoken"
"github.com/hashicorp/watchtower/internal/db"
pba "github.com/hashicorp/watchtower/internal/gen/controller/api/resources/authtokens"
pbs "github.com/hashicorp/watchtower/internal/gen/controller/api/services"
"github.com/hashicorp/watchtower/internal/iam"
iamStore "github.com/hashicorp/watchtower/internal/iam/store"
"github.com/hashicorp/watchtower/internal/servers/controller/common"
"github.com/hashicorp/watchtower/internal/servers/controller/handlers"
"github.com/hashicorp/watchtower/internal/types/scope"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
const orgIdFieldName = "org_id"
var (
reInvalidID = regexp.MustCompile("[^A-Za-z0-9]")
// TODO: Remove once auth methods are in
OrgScope string
RWDb = new(atomic.Value)
)
func init() {
RWDb.Store((*db.Db)(nil))
}
// Service handles request as described by the pbs.OrgServiceServer interface.
type Service struct {
iamRepo common.IamRepoFactory
authTokenRepo common.AuthTokenRepoFactory
}
// NewService returns an org service which handles org related requests to watchtower.
func NewService(iamRepo common.IamRepoFactory, atRepo common.AuthTokenRepoFactory) (Service, error) {
if iamRepo == nil {
return Service{}, fmt.Errorf("nil iam repository provided")
}
if atRepo == nil {
return Service{}, fmt.Errorf("nil auth token repository provided")
}
return Service{iamRepo: iamRepo, authTokenRepo: atRepo}, nil
}
var _ pbs.AuthenticationServiceServer = Service{}
// Authenticate implements the interface pbs.AuthenticationServiceServer.
func (s Service) Authenticate(ctx context.Context, req *pbs.AuthenticateRequest) (*pbs.AuthenticateResponse, error) {
if err := validateAuthenticateRequest(req); err != nil {
return nil, err
}
tok, err := s.authenticateWithRepo(ctx, req)
if err != nil {
return nil, err
}
return &pbs.AuthenticateResponse{Item: tok}, nil
}
// Deauthenticate implements the interface pbs.AuthenticationServiceServer.
func (s Service) Deauthenticate(ctx context.Context, req *pbs.DeauthenticateRequest) (*pbs.DeauthenticateResponse, error) {
return nil, status.Error(codes.Unimplemented, "Requested method is unimplemented for Org.")
}
func (s Service) authenticateWithRepo(ctx context.Context, req *pbs.AuthenticateRequest) (*pba.AuthToken, error) {
userRepo, err := s.iamRepo()
if err != nil {
return nil, err
}
atRepo, err := s.authTokenRepo()
if err != nil {
return nil, err
}
// Place holder for making a request to authenticate
creds := req.GetCredentials().GetFields()
pwName, password := creds["name"], creds["password"]
if pwName.GetStringValue() == "wrong" && password.GetStringValue() == "wrong" {
return nil, status.Error(codes.Unauthenticated, "Unable to authenticate.")
}
// Get back a password.Account with a CredentialId string and a public Id
// Create a fake auth account ID for the moment
acctIdBytes := sha256.Sum256([]byte(pwName.GetStringValue()))
acctId := fmt.Sprintf("aa_%s", base32.StdEncoding.EncodeToString(acctIdBytes[:])[0:10])
aAcct := &iam.AuthAccount{AuthAccount: &iamStore.AuthAccount{
PublicId: acctId,
ScopeId: OrgScope,
AuthMethodId: req.AuthMethodId,
}}
RWDb.Load().(*db.Db).Create(ctx, aAcct)
u, err := userRepo.LookupUserWithLogin(ctx, acctId, iam.WithAutoVivify(true))
if err != nil {
return nil, err
}
tok, err := atRepo.CreateAuthToken(ctx, u.GetPublicId(), acctId)
if err != nil {
return nil, err
}
tok.Token = tok.GetPublicId() + "_" + tok.GetToken()
return toProto(tok), nil
}
func toProto(t *authtoken.AuthToken) *pba.AuthToken {
return &pba.AuthToken{
Id: t.GetPublicId(),
Token: t.GetToken(),
UserId: t.GetIamUserId(),
AuthMethodId: t.GetAuthMethodId(),
CreatedTime: t.GetCreateTime().GetTimestamp(),
UpdatedTime: t.GetUpdateTime().GetTimestamp(),
ApproximateLastUsedTime: t.GetApproximateLastAccessTime().GetTimestamp(),
ExpirationTime: t.GetExpirationTime().GetTimestamp(),
}
}
// A validateX method should exist for each method above. These methods do not make calls to any backing service but enforce
// requirements on the structure of the request. They verify that:
// * The path passed in is correctly formatted
// * All required parameters are set
// * There are no conflicting parameters provided
func validateAuthenticateRequest(req *pbs.AuthenticateRequest) error {
badFields := make(map[string]string)
if !validId(req.GetOrgId(), scope.Org.Prefix()+"_") {
badFields[orgIdFieldName] = "Invalid formatted identifier."
}
if strings.TrimSpace(req.GetAuthMethodId()) == "" {
badFields["auth_method_id"] = "This is a required field."
} else if validId(req.GetAuthMethodId(), "am") {
badFields["auth_method_id"] = "Invalid formatted identifier."
}
// TODO: Update this when we enable different auth method types.
if req.GetCredentials() == nil {
badFields["credentials"] = "This is a required field."
}
// TODO: Update this when we enable split cookie token types.
tType := strings.ToLower(strings.TrimSpace(req.GetTokenType()))
if tType != "" && tType != "token" {
badFields["token_type"] = "The only accepted type is 'token'."
}
if len(badFields) > 0 {
return handlers.InvalidArgumentErrorf("Invalid fields provided in request.", badFields)
}
return nil
}
func validId(id, prefix string) bool {
if !strings.HasPrefix(id, prefix) {
return false
}
id = strings.TrimPrefix(id, prefix)
return !reInvalidID.Match([]byte(id))
}
@@ -0,0 +1,236 @@
package authenticate
import (
"context"
"crypto/sha256"
"encoding/base32"
"fmt"
"strings"
"testing"
structpb "github.com/golang/protobuf/ptypes/struct"
"github.com/hashicorp/watchtower/internal/authtoken"
"github.com/hashicorp/watchtower/internal/db"
pba "github.com/hashicorp/watchtower/internal/gen/controller/api/resources/authtokens"
pbs "github.com/hashicorp/watchtower/internal/gen/controller/api/services"
"github.com/hashicorp/watchtower/internal/iam"
iamStore "github.com/hashicorp/watchtower/internal/iam/store"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func getAuthMethodAndAccountId(t *testing.T, org *iam.Scope, rw *db.Db) (string, string) {
// TODO: Remove this when Auth Account repo is in place.
insert := `insert into auth_method
(public_id, scope_id)
values
($1, $2);`
amId, err := db.NewPublicId("am")
require.NoError(t, err)
innerDb, err := rw.DB()
require.NoError(t, err)
_, err = innerDb.Exec(insert, amId, org.GetPublicId())
require.NoError(t, err)
aAcctId, err := db.NewPublicId("aa")
require.NoError(t, err)
aAcct := &iam.AuthAccount{AuthAccount: &iamStore.AuthAccount{
PublicId: aAcctId,
ScopeId: org.GetPublicId(),
AuthMethodId: amId,
}}
require.NoError(t, rw.Create(context.Background(), aAcct))
return aAcct.GetAuthMethodId(), aAcct.GetPublicId()
}
func TestAuthenticate(t *testing.T) {
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
RWDb.Store(rw)
wrapper := db.TestWrapper(t)
o, _ := iam.TestScopes(t, conn)
authTokenRepoFn := func() (*authtoken.Repository, error) { return authtoken.NewRepository(rw, rw, wrapper) }
iamRepoFn := func() (*iam.Repository, error) { return iam.NewRepository(rw, rw, wrapper) }
amId, _ := getAuthMethodAndAccountId(t, o, rw)
cases := []struct {
name string
request *pbs.AuthenticateRequest
want *pbs.AuthenticateResponse
wantErr error
}{
{
name: "basic",
request: &pbs.AuthenticateRequest{
OrgId: o.GetPublicId(),
AuthMethodId: amId,
TokenType: "token",
Credentials: func() *structpb.Struct {
creds := map[string]*structpb.Value{
"name": {Kind: &structpb.Value_StringValue{StringValue: "admin"}},
"password": {Kind: &structpb.Value_StringValue{StringValue: "hunter2"}},
}
return &structpb.Struct{Fields: creds}
}(),
},
want: &pbs.AuthenticateResponse{Item: &pba.AuthToken{
AuthMethodId: amId,
}},
},
{
name: "no-token-type",
request: &pbs.AuthenticateRequest{
OrgId: o.GetPublicId(),
AuthMethodId: amId,
Credentials: func() *structpb.Struct {
creds := map[string]*structpb.Value{
"name": {Kind: &structpb.Value_StringValue{StringValue: "admin"}},
"password": {Kind: &structpb.Value_StringValue{StringValue: "hunter2"}},
}
return &structpb.Struct{Fields: creds}
}(),
},
want: &pbs.AuthenticateResponse{Item: &pba.AuthToken{
AuthMethodId: amId,
}},
},
{
name: "bad-token-type",
request: &pbs.AuthenticateRequest{
OrgId: o.GetPublicId(),
AuthMethodId: amId,
TokenType: "email",
Credentials: func() *structpb.Struct {
creds := map[string]*structpb.Value{
"name": {Kind: &structpb.Value_StringValue{StringValue: "admin"}},
"password": {Kind: &structpb.Value_StringValue{StringValue: "hunter2"}},
}
return &structpb.Struct{Fields: creds}
}(),
},
wantErr: status.Error(codes.InvalidArgument, "invalid argument"),
},
{
name: "no-authmethod",
request: &pbs.AuthenticateRequest{
OrgId: o.GetPublicId(),
Credentials: func() *structpb.Struct {
creds := map[string]*structpb.Value{
"name": {Kind: &structpb.Value_StringValue{StringValue: "admin"}},
"password": {Kind: &structpb.Value_StringValue{StringValue: "hunter2"}},
}
return &structpb.Struct{Fields: creds}
}(),
},
wantErr: status.Error(codes.InvalidArgument, "invalid argument"),
},
{
name: "wrong-username-password",
request: &pbs.AuthenticateRequest{
OrgId: o.GetPublicId(),
AuthMethodId: amId,
TokenType: "token",
Credentials: func() *structpb.Struct {
creds := map[string]*structpb.Value{
"name": {Kind: &structpb.Value_StringValue{StringValue: "wrong"}},
"password": {Kind: &structpb.Value_StringValue{StringValue: "wrong"}},
}
return &structpb.Struct{Fields: creds}
}(),
},
wantErr: status.Error(codes.Unauthenticated, "unauthenticated"),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
s, err := NewService(iamRepoFn, authTokenRepoFn)
require.NoError(err)
// TODO: remove when we stop faking things
name := tc.request.Credentials.Fields["name"].GetStringValue()
acctIdBytes := sha256.Sum256([]byte(name))
acctId := fmt.Sprintf("aa_%s", base32.StdEncoding.EncodeToString(acctIdBytes[:])[0:10])
aAcct := &iam.AuthAccount{AuthAccount: &iamStore.AuthAccount{
PublicId: acctId,
ScopeId: o.PublicId,
AuthMethodId: amId,
}}
RWDb.Load().(*db.Db).Create(context.Background(), aAcct)
resp, err := s.Authenticate(context.Background(), tc.request)
if tc.wantErr != nil {
assert.Error(err)
assert.Equal(status.Code(tc.wantErr), status.Code(err))
return
}
require.NoError(err)
aToken := resp.GetItem()
assert.NotEmpty(aToken.GetId())
assert.NotEmpty(aToken.GetToken())
assert.True(strings.HasPrefix(aToken.GetToken(), aToken.GetId()))
assert.Equal(amId, aToken.GetAuthMethodId())
assert.Equal(aToken.GetCreatedTime(), aToken.GetUpdatedTime())
assert.Equal(aToken.GetCreatedTime(), aToken.GetApproximateLastUsedTime())
})
}
}
func TestAuthenticate_AuthAccountConnectedToIamUser(t *testing.T) {
assert, require := assert.New(t), require.New(t)
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
RWDb.Store(rw)
wrapper := db.TestWrapper(t)
o, _ := iam.TestScopes(t, conn)
authTokenRepoFn := func() (*authtoken.Repository, error) { return authtoken.NewRepository(rw, rw, wrapper) }
iamRepoFn := func() (*iam.Repository, error) { return iam.NewRepository(rw, rw, wrapper) }
iamRepo, err := iamRepoFn()
require.NoError(err)
// connected to an account.
amId, _ := getAuthMethodAndAccountId(t, o, rw)
name := "admin"
acctIdBytes := sha256.Sum256([]byte(name))
acctId := fmt.Sprintf("aa_%s", base32.StdEncoding.EncodeToString(acctIdBytes[:])[0:10])
aAcct := &iam.AuthAccount{AuthAccount: &iamStore.AuthAccount{
PublicId: acctId,
ScopeId: o.PublicId,
AuthMethodId: amId,
}}
RWDb.Load().(*db.Db).Create(context.Background(), aAcct)
iamUser, err := iamRepo.LookupUserWithLogin(context.Background(), acctId, iam.WithAutoVivify(true))
require.NoError(err)
s, err := NewService(iamRepoFn, authTokenRepoFn)
require.NoError(err)
resp, err := s.Authenticate(context.Background(), &pbs.AuthenticateRequest{
OrgId: o.GetPublicId(),
AuthMethodId: amId,
Credentials: func() *structpb.Struct {
creds := map[string]*structpb.Value{
"name": {Kind: &structpb.Value_StringValue{StringValue: name}},
"password": {Kind: &structpb.Value_StringValue{StringValue: "hunter2"}},
}
return &structpb.Struct{Fields: creds}
}(),
})
require.NoError(err)
aToken := resp.GetItem()
assert.Equal(iamUser.GetPublicId(), aToken.GetUserId())
assert.Equal(amId, aToken.GetAuthMethodId())
assert.NotEmpty(aToken.GetId())
assert.NotEmpty(aToken.GetToken())
assert.True(strings.HasPrefix(aToken.GetToken(), aToken.GetId()))
}
@@ -21,46 +21,110 @@ const (
// TokenAuthenticator returns a function that can be used in grpc-gateway's runtime.WithMetadata ServerOption.
// It looks at the cookies and headers of the incoming request and returns metadata that can later be
// used by handlers to build a TokenMetadata using the ToTokenMetadata function.
func TokenAuthenticator(l hclog.Logger, tokenRepo common.AuthTokenRepoFactory) func(context.Context, *http.Request) metadata.MD {
func TokenAuthenticator(l hclog.Logger, tokenRepo common.AuthTokenRepoFactory, iamRepo common.IamRepoFactory) func(context.Context, *http.Request) metadata.MD {
return func(ctx context.Context, req *http.Request) metadata.MD {
tMD := TokenMetadata{}
if authHeader := req.Header.Get(headerAuthMethod); authHeader != "" {
headerSplit := strings.SplitN(strings.TrimSpace(authHeader), " ", 2)
if len(headerSplit) == 2 && strings.EqualFold(strings.TrimSpace(headerSplit[0]), "bearer") {
tMD.recievedTokenType = authTokenTypeBearer
tMD.bearerPayload = strings.TrimSpace(headerSplit[1])
return TokenMetadata{}.toMetadata()
/*
tMD := TokenMetadata{}
if authHeader := req.Header.Get(headerAuthMethod); authHeader != "" {
headerSplit := strings.SplitN(strings.TrimSpace(authHeader), " ", 2)
if len(headerSplit) == 2 && strings.EqualFold(strings.TrimSpace(headerSplit[0]), "bearer") {
tMD.receivedTokenType = authTokenTypeBearer
tMD.bearerPayload = strings.TrimSpace(headerSplit[1])
}
}
}
if tMD.recievedTokenType != authTokenTypeBearer {
if hc, err := req.Cookie(httpOnlyCookieName); err == nil {
tMD.httpCookiePayload = hc.Value
if tMD.receivedTokenType != authTokenTypeBearer {
if hc, err := req.Cookie(httpOnlyCookieName); err == nil {
tMD.httpCookiePayload = hc.Value
}
if jc, err := req.Cookie(jsVisibleCookieName); err == nil {
tMD.jsCookiePayload = jc.Value
}
if tMD.httpCookiePayload != "" && tMD.jsCookiePayload != "" {
tMD.receivedTokenType = authTokenTypeSplitCookie
}
}
if jc, err := req.Cookie(jsVisibleCookieName); err == nil {
tMD.jsCookiePayload = jc.Value
}
if tMD.httpCookiePayload != "" && tMD.jsCookiePayload != "" {
tMD.recievedTokenType = authTokenTypeSplitCookie
}
}
if tMD.recievedTokenType == authTokenTypeUnknown || tMD.token() == "" || tMD.publicId() == "" {
if tMD.receivedTokenType == authTokenTypeUnknown || tMD.token() == "" || tMD.publicId() == "" {
return tMD.toMetadata()
}
tokenRepo, err := tokenRepo()
if err != nil {
l.Error("failed to get authtoken repo", "error", err)
return tMD.toMetadata()
}
at, err := tokenRepo.ValidateToken(ctx, tMD.publicId(), tMD.token())
if err != nil {
l.Error("failed to validate token", "error", err)
}
if at != nil {
tMD.UserId = at.GetIamUserId()
} else {
tMD.UserId = "u_anon"
}
iamRepo, err := iamRepo()
if err != nil {
l.Error("failed to get iam repo", "error", err)
return tMD.toMetadata()
}
grantPairs, err := iamRepo.GrantsForUser(ctx, tMD.UserId)
if err != nil {
l.Error("failed to query for user grants", "error", err)
return tMD.toMetadata()
}
parsedGrants := make([]perms.Grant, 0, len(grantPairs))
for _, pair := range grantPairs {
parsed, err := perms.Parse(pair.ScopeId, tMD.UserId, pair.Grant)
if err != nil {
l.Error("failed to parse grant", "grant", pair.Grant, "error", err)
return tMD.toMetadata()
}
parsedGrants = append(parsedGrants, parsed)
}
var scopeId, id, pin string
var typ resource.Type
var act action.Type
typRaw := ctx.Value(globals.ContextTypeValue)
if typRaw != nil {
typ = typRaw.(resource.Type)
}
scopeIdRaw := ctx.Value(globals.ContextScopeValue)
if scopeIdRaw != nil {
scopeId = scopeIdRaw.(string)
}
actRaw := ctx.Value(globals.ContextActionValue)
if actRaw != nil {
act = actRaw.(action.Type)
}
idRaw := ctx.Value(globals.ContextResourceValue)
if idRaw != nil {
id = idRaw.(string)
}
pinRaw := ctx.Value(globals.ContextPinValue)
if pinRaw != nil {
pin = pinRaw.(string)
}
acl := perms.NewACL(parsedGrants...)
res := perms.Resource{
// These values were parsed from the request information
ScopeId: scopeId,
Type: typ,
Id: id,
Pin: pin,
}
allowed := acl.Allowed(res, act)
if allowed.Allowed {
tMD.authorized = true
}
l.Info("grant checking", "user_id", tMD.UserId, "grants", pretty.Sprint(grantPairs), "resource", res, "authorized", allowed)
return tMD.toMetadata()
}
repo, err := tokenRepo()
if err != nil {
l.Error("failed to get authtoken repo", "error", err)
return tMD.toMetadata()
}
at, err := repo.ValidateToken(ctx, tMD.publicId(), tMD.token())
if err != nil {
l.Error("failed to validate token", "error", err)
}
if at != nil {
tMD.UserId = at.GetIamUserId()
}
return tMD.toMetadata()
*/
}
}
@@ -87,7 +151,9 @@ type TokenMetadata struct {
// Only set the UserId if the token was found and was not expired.
UserId string
recievedTokenType tokenFormat
authorized bool
receivedTokenType tokenFormat
bearerPayload string
jsCookiePayload string
@@ -96,6 +162,7 @@ type TokenMetadata struct {
const (
mdAuthTokenUserKey = "wt-authtoken-user-key"
mdAuthAuthorizedKey = "wt-authtoken-authorized"
mdAuthTokenBearerTokenKey = "wt-authtoken-bearer-token-key"
mdAuthTokenHttpTokenKey = "wt-authtoken-http-token-key"
mdAuthTokenJsTokenKey = "wt-authtoken-js-token-key"
@@ -113,6 +180,9 @@ func ToTokenMetadata(ctx context.Context) TokenMetadata {
if uid := md.Get(mdAuthTokenUserKey); len(uid) > 0 {
tMD.UserId = uid[0]
}
if authzd := md.Get(mdAuthAuthorizedKey); len(authzd) > 0 {
tMD.authorized = authzd[0] == "true"
}
if token := md.Get(mdAuthTokenBearerTokenKey); len(token) > 0 {
tMD.bearerPayload = token[0]
}
@@ -124,7 +194,7 @@ func ToTokenMetadata(ctx context.Context) TokenMetadata {
}
if sType := md.Get(mdAuthTokenTypeKey); len(sType) > 0 {
if st, err := strconv.Atoi(sType[0]); err == nil {
tMD.recievedTokenType = tokenFormat(st)
tMD.receivedTokenType = tokenFormat(st)
}
}
@@ -136,6 +206,11 @@ func (s TokenMetadata) toMetadata() metadata.MD {
if s.UserId != "" {
md.Set(mdAuthTokenUserKey, s.UserId)
}
if s.authorized {
md.Set(mdAuthAuthorizedKey, "true")
} else {
md.Set(mdAuthAuthorizedKey, "false")
}
if s.bearerPayload != "" {
md.Set(mdAuthTokenBearerTokenKey, s.bearerPayload)
}
@@ -145,9 +220,10 @@ func (s TokenMetadata) toMetadata() metadata.MD {
if s.jsCookiePayload != "" {
md.Set(mdAuthTokenJsTokenKey, s.jsCookiePayload)
}
if s.recievedTokenType != authTokenTypeUnknown {
md.Set(mdAuthTokenTypeKey, fmt.Sprint(s.recievedTokenType))
if s.receivedTokenType != authTokenTypeUnknown {
md.Set(mdAuthTokenTypeKey, fmt.Sprint(s.receivedTokenType))
}
return md
}
@@ -155,7 +231,7 @@ func (s TokenMetadata) toMetadata() metadata.MD {
// is malformed then this returns an empty string.
func (s TokenMetadata) publicId() string {
tok := ""
switch s.recievedTokenType {
switch s.receivedTokenType {
case authTokenTypeBearer:
tok = s.bearerPayload
case authTokenTypeSplitCookie:
@@ -172,7 +248,7 @@ func (s TokenMetadata) publicId() string {
// is malformed then this returns an empty string.
func (s TokenMetadata) token() string {
var tok string
switch s.recievedTokenType {
switch s.receivedTokenType {
case authTokenTypeBearer:
tok = s.bearerPayload
case authTokenTypeSplitCookie:
@@ -14,6 +14,7 @@ import (
"github.com/hashicorp/watchtower/internal/db"
"github.com/hashicorp/watchtower/internal/gen/controller/api/services"
pbs "github.com/hashicorp/watchtower/internal/gen/controller/api/services"
"github.com/hashicorp/watchtower/internal/iam"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -30,6 +31,7 @@ func (s *fakeHandler) GetOrg(ctx context.Context, _ *pbs.GetOrgRequest) (*pbs.Ge
}
func TestAuthTokenPublicIdTokenValue(t *testing.T) {
t.Skip("functionality has moved for now")
cases := []struct {
name string
in TokenMetadata
@@ -39,7 +41,7 @@ func TestAuthTokenPublicIdTokenValue(t *testing.T) {
{
name: "no delimeter",
in: TokenMetadata{
recievedTokenType: authTokenTypeBearer,
receivedTokenType: authTokenTypeBearer,
bearerPayload: "prefix_publicid_token",
jsCookiePayload: "this_is_just_junk",
httpCookiePayload: "this_can_be_ignored",
@@ -50,7 +52,7 @@ func TestAuthTokenPublicIdTokenValue(t *testing.T) {
{
name: "no delimeter",
in: TokenMetadata{
recievedTokenType: authTokenTypeSplitCookie,
receivedTokenType: authTokenTypeSplitCookie,
bearerPayload: "this_is_just_junk_that_should_be_ignored",
jsCookiePayload: "prefix_publicid_token",
httpCookiePayload: "cookiepayload",
@@ -61,7 +63,7 @@ func TestAuthTokenPublicIdTokenValue(t *testing.T) {
{
name: "no delimeter",
in: TokenMetadata{
recievedTokenType: authTokenTypeBearer,
receivedTokenType: authTokenTypeBearer,
bearerPayload: "this-doesnt-have-the-expected-delimiter",
},
wantId: "",
@@ -70,7 +72,7 @@ func TestAuthTokenPublicIdTokenValue(t *testing.T) {
{
name: "to many delimeters",
in: TokenMetadata{
recievedTokenType: authTokenTypeBearer,
receivedTokenType: authTokenTypeBearer,
bearerPayload: "this_has_to_many_delimiters",
},
wantId: "",
@@ -87,13 +89,19 @@ func TestAuthTokenPublicIdTokenValue(t *testing.T) {
}
func TestAuthTokenAuthenticator(t *testing.T) {
t.Skip("functionality has moved for now")
conn, _ := db.TestSetup(t, "postgres")
rw := db.New(conn)
wrapper := db.TestWrapper(t)
repo, err := authtoken.NewRepository(rw, rw, wrapper)
tokenRepo, err := authtoken.NewRepository(rw, rw, wrapper)
require.NoError(t, err)
repoFn := func() (*authtoken.Repository, error) {
return repo, nil
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
}
at := authtoken.TestAuthToken(t, conn, wrapper)
@@ -110,13 +118,13 @@ func TestAuthTokenAuthenticator(t *testing.T) {
{
name: "Empty headers",
headers: map[string]string{},
wantAuthTokMd: TokenMetadata{recievedTokenType: authTokenTypeUnknown},
wantAuthTokMd: TokenMetadata{receivedTokenType: authTokenTypeUnknown},
},
{
name: "Bear token",
headers: map[string]string{"Authorization": fmt.Sprintf("Bearer %s", tokValue)},
wantAuthTokMd: TokenMetadata{
recievedTokenType: authTokenTypeBearer,
receivedTokenType: authTokenTypeBearer,
bearerPayload: tokValue,
UserId: at.GetIamUserId(),
},
@@ -128,7 +136,7 @@ func TestAuthTokenAuthenticator(t *testing.T) {
{Name: jsVisibleCookieName, Value: jsCookieVal},
},
wantAuthTokMd: TokenMetadata{
recievedTokenType: authTokenTypeSplitCookie,
receivedTokenType: authTokenTypeSplitCookie,
httpCookiePayload: httpCookieVal,
jsCookiePayload: jsCookieVal,
UserId: at.GetIamUserId(),
@@ -140,7 +148,7 @@ func TestAuthTokenAuthenticator(t *testing.T) {
{Name: httpOnlyCookieName, Value: httpCookieVal},
},
wantAuthTokMd: TokenMetadata{
recievedTokenType: authTokenTypeUnknown,
receivedTokenType: authTokenTypeUnknown,
httpCookiePayload: httpCookieVal,
},
},
@@ -150,7 +158,7 @@ func TestAuthTokenAuthenticator(t *testing.T) {
{Name: jsVisibleCookieName, Value: jsCookieVal},
},
wantAuthTokMd: TokenMetadata{
recievedTokenType: authTokenTypeUnknown,
receivedTokenType: authTokenTypeUnknown,
jsCookiePayload: jsCookieVal,
},
},
@@ -163,7 +171,7 @@ func TestAuthTokenAuthenticator(t *testing.T) {
},
// We prioritize the auth header over the cookie and if the header is set we ignore the cookies completely.
wantAuthTokMd: TokenMetadata{
recievedTokenType: authTokenTypeBearer,
receivedTokenType: authTokenTypeBearer,
bearerPayload: tokValue,
UserId: at.GetIamUserId(),
},
@@ -176,7 +184,7 @@ func TestAuthTokenAuthenticator(t *testing.T) {
tMD := ToTokenMetadata(ctx)
assert.Equal(t, tc.wantAuthTokMd, tMD)
}}
mux := runtime.NewServeMux(runtime.WithMetadata(TokenAuthenticator(hclog.L(), repoFn)))
mux := runtime.NewServeMux(runtime.WithMetadata(TokenAuthenticator(hclog.L(), tokenRepoFn, iamRepoFn)))
require.NoError(t, services.RegisterOrgServiceHandlerServer(context.Background(), mux, hook))
req := httptest.NewRequest("GET", "http://127.0.0.1/v1/orgs/1", nil)
@@ -12,8 +12,6 @@ import (
"github.com/hashicorp/watchtower/internal/servers/controller/common"
"github.com/hashicorp/watchtower/internal/servers/controller/handlers"
"github.com/hashicorp/watchtower/internal/types/scope"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/wrapperspb"
)
@@ -63,16 +61,6 @@ func (s Service) GetOrg(ctx context.Context, req *pbs.GetOrgRequest) (*pbs.GetOr
return &pbs.GetOrgResponse{Item: o}, nil
}
// Authenticate implements the interface pbs.OrgServiceServer.
func (s Service) Authenticate(ctx context.Context, req *pbs.AuthenticateRequest) (*pbs.AuthenticateResponse, error) {
return nil, status.Error(codes.Unimplemented, "Requested method is unimplemented for Orgs.")
}
// Deauthenticate implements the interface pbs.OrgServiceServer.
func (s Service) Deauthenticate(ctx context.Context, req *pbs.DeauthenticateRequest) (*pbs.DeauthenticateResponse, error) {
return nil, status.Error(codes.Unimplemented, "Requested method is unimplemented for Orgs.")
}
func (s Service) getFromRepo(ctx context.Context, id string) (*pb.Org, error) {
repo, err := s.repo()
if err != nil {
@@ -130,7 +130,7 @@ func (s Service) AddRolePrincipals(ctx context.Context, req *pbs.AddRolePrincipa
if err := validateAddRolePrincipalsRequest(req); err != nil {
return nil, err
}
r, err := s.addPrinciplesInRepo(ctx, req.GetRoleId(), req.GetUserIds(), req.GetGroupIds(), req.GetVersion().GetValue())
r, err := s.addPrinciplesInRepo(ctx, req.GetRoleId(), req.GetPrincipalIds(), req.GetVersion().GetValue())
if err != nil {
return nil, err
}
@@ -144,7 +144,7 @@ func (s Service) SetRolePrincipals(ctx context.Context, req *pbs.SetRolePrincipa
if err := validateSetRolePrincipalsRequest(req); err != nil {
return nil, err
}
r, err := s.setPrinciplesInRepo(ctx, req.GetRoleId(), req.GetUserIds(), req.GetGroupIds(), req.GetVersion().GetValue())
r, err := s.setPrinciplesInRepo(ctx, req.GetRoleId(), req.GetPrincipalIds(), req.GetVersion().GetValue())
if err != nil {
return nil, err
}
@@ -158,7 +158,7 @@ func (s Service) RemoveRolePrincipals(ctx context.Context, req *pbs.RemoveRolePr
if err := validateRemoveRolePrincipalsRequest(req); err != nil {
return nil, err
}
r, err := s.removePrinciplesInRepo(ctx, req.GetRoleId(), req.GetUserIds(), req.GetGroupIds(), req.GetVersion().GetValue())
r, err := s.removePrinciplesInRepo(ctx, req.GetRoleId(), req.GetPrincipalIds(), req.GetVersion().GetValue())
if err != nil {
return nil, err
}
@@ -325,12 +325,12 @@ func (s Service) listFromRepo(ctx context.Context, scopeId string) ([]*pb.Role,
return outRl, nil
}
func (s Service) addPrinciplesInRepo(ctx context.Context, roleId string, userIds []string, groupIds []string, version uint32) (*pb.Role, error) {
func (s Service) addPrinciplesInRepo(ctx context.Context, roleId string, principalIds []string, version uint32) (*pb.Role, error) {
repo, err := s.repoFn()
if err != nil {
return nil, err
}
_, err = repo.AddPrincipalRoles(ctx, roleId, version, userIds, groupIds)
_, err = repo.AddPrincipalRoles(ctx, roleId, version, principalIds)
if err != nil {
return nil, status.Errorf(codes.Internal, "Unable to add principles to role: %v.", err)
}
@@ -344,12 +344,12 @@ func (s Service) addPrinciplesInRepo(ctx context.Context, roleId string, userIds
return toProto(out, pr, roleGrants), nil
}
func (s Service) setPrinciplesInRepo(ctx context.Context, roleId string, userIds []string, groupIds []string, version uint32) (*pb.Role, error) {
func (s Service) setPrinciplesInRepo(ctx context.Context, roleId string, principalIds []string, version uint32) (*pb.Role, error) {
repo, err := s.repoFn()
if err != nil {
return nil, err
}
_, _, err = repo.SetPrincipalRoles(ctx, roleId, version, userIds, groupIds)
_, _, err = repo.SetPrincipalRoles(ctx, roleId, version, principalIds)
if err != nil {
return nil, status.Errorf(codes.Internal, "Unable to set principles on role: %v.", err)
}
@@ -363,12 +363,12 @@ func (s Service) setPrinciplesInRepo(ctx context.Context, roleId string, userIds
return toProto(out, pr, roleGrants), nil
}
func (s Service) removePrinciplesInRepo(ctx context.Context, roleId string, userIds []string, groupIds []string, version uint32) (*pb.Role, error) {
func (s Service) removePrinciplesInRepo(ctx context.Context, roleId string, principalIds []string, version uint32) (*pb.Role, error) {
repo, err := s.repoFn()
if err != nil {
return nil, err
}
_, err = repo.DeletePrincipalRoles(ctx, roleId, version, userIds, groupIds)
_, err = repo.DeletePrincipalRoles(ctx, roleId, version, principalIds)
if err != nil {
return nil, status.Errorf(codes.Internal, "Unable to remove principles from role: %v.", err)
}
@@ -476,12 +476,13 @@ func toProto(in *iam.Role, principals []iam.PrincipalRole, grants []*iam.RoleGra
out.Name = &wrapperspb.StringValue{Value: in.GetName()}
}
for _, p := range principals {
switch p.GetType() {
case iam.UserRoleType.String():
out.UserIds = append(out.UserIds, p.GetPrincipalId())
case iam.GroupRoleType.String():
out.GroupIds = append(out.GroupIds, p.GetPrincipalId())
principal := &pb.Principal{
Id: p.GetPrincipalId(),
Type: p.GetType(),
ScopeId: p.GetPrincipalScopeId(),
}
out.Principals = append(out.Principals, principal)
out.PrincipalIds = append(out.PrincipalIds, p.GetPrincipalId())
}
for _, g := range grants {
out.Grants = append(out.Grants, g.GetRawGrant())
@@ -542,6 +543,9 @@ func validateCreateRequest(req *pbs.CreateRoleRequest) error {
badFields["grant_scope_id"] = "Must be empty or set to the project_id when the scope type is project."
}
}
if item.GetPrincipals() != nil {
badFields["principals"] = "This is a read only field."
}
if len(badFields) > 0 {
return handlers.InvalidArgumentErrorf("Argument errors found in the request.", badFields)
}
@@ -572,6 +576,9 @@ func validateUpdateRequest(req *pbs.UpdateRoleRequest) error {
if item.GetUpdatedTime() != nil {
badFields["updated_time"] = "This is a read only field and cannot be specified in an update request."
}
if item.GetPrincipals() != nil {
badFields["principals"] = "This is a read only field and cannot be specified in an update request."
}
if item.GetGrantScopeId() != nil && req.ProjectId != "" {
if item.GetGrantScopeId().Value != req.ProjectId {
badFields["grant_scope_id"] = "Must be empty or set to the project_id when the scope type is project."
@@ -611,9 +618,8 @@ func validateAddRolePrincipalsRequest(req *pbs.AddRolePrincipalsRequest) error {
if req.GetVersion() == nil {
badFields["version"] = "Required field."
}
if len(req.GetGroupIds()) == 0 && len(req.GetUserIds()) == 0 {
badFields["user_ids"] = "Either user_ids or group_ids must be non empty."
badFields["group_ids"] = "Either user_ids or group_ids must be non empty."
if len(req.GetPrincipalIds()) == 0 {
badFields["principal_ids"] = "Must be non-empty."
}
if len(badFields) > 0 {
return handlers.InvalidArgumentErrorf("Errors in provided fields.", badFields)
@@ -643,9 +649,8 @@ func validateRemoveRolePrincipalsRequest(req *pbs.RemoveRolePrincipalsRequest) e
if req.GetVersion() == nil {
badFields["version"] = "Required field."
}
if len(req.GetGroupIds()) == 0 && len(req.GetUserIds()) == 0 {
badFields["user_ids"] = "Either user_ids or group_ids must be non empty."
badFields["group_ids"] = "Either user_ids or group_ids must be non empty."
if len(req.GetPrincipalIds()) == 0 {
badFields["principal_ids"] = "Must be non-empty."
}
if len(badFields) > 0 {
return handlers.InvalidArgumentErrorf("Errors in provided fields.", badFields)
@@ -662,7 +667,7 @@ func validateAddRoleGrantsRequest(req *pbs.AddRoleGrantsRequest) error {
badFields["version"] = "Required field."
}
if len(req.GetGrants()) == 0 {
badFields["grants"] = "This must be non empty."
badFields["grants"] = "Must be non-empty."
}
if len(badFields) > 0 {
return handlers.InvalidArgumentErrorf("Errors in provided fields.", badFields)
@@ -693,7 +698,7 @@ func validateRemoveRoleGrantsRequest(req *pbs.RemoveRoleGrantsRequest) error {
badFields["version"] = "Required field."
}
if len(req.GetGrants()) == 0 {
badFields["grants"] = "This must be non empty."
badFields["grants"] = "Must be non-empty."
}
if len(badFields) > 0 {
return handlers.InvalidArgumentErrorf("Errors in provided fields.", badFields)
@@ -38,6 +38,30 @@ func createDefaultRolesAndRepo(t *testing.T) (*iam.Role, *iam.Role, func() (*iam
return or, pr, repoFn
}
func equalPrincipals(role *pb.Role, principals []string) bool {
if len(role.Principals) != len(principals) {
return false
}
for _, principal := range principals {
var foundInPrincipals bool
var foundInPrincipalIds bool
for _, v := range role.Principals {
if v.Id == principal {
foundInPrincipals = true
}
}
for _, v := range role.PrincipalIds {
if v == principal {
foundInPrincipalIds = true
}
}
if !foundInPrincipals || !foundInPrincipalIds {
return false
}
}
return true
}
func TestGet(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
@@ -891,11 +915,10 @@ func TestAddPrincipal(t *testing.T) {
role := iam.TestRole(t, conn, scope.GetPublicId())
tc.setup(role)
req := &pbs.AddRolePrincipalsRequest{
OrgId: o.GetPublicId(),
RoleId: role.GetPublicId(),
Version: &wrapperspb.UInt32Value{Value: role.GetVersion()},
UserIds: tc.addUsers,
GroupIds: tc.addGroups,
OrgId: o.GetPublicId(),
RoleId: role.GetPublicId(),
Version: &wrapperspb.UInt32Value{Value: role.GetVersion()},
PrincipalIds: append(tc.addUsers, tc.addGroups...),
}
got, err := s.AddRolePrincipals(context.Background(), req)
@@ -907,8 +930,7 @@ func TestAddPrincipal(t *testing.T) {
require.True(t, ok)
require.NoError(t, err, "Got error: %v", s)
assert.ElementsMatch(t, got.GetItem().GetUserIds(), tc.resultUsers)
assert.ElementsMatch(t, got.GetItem().GetGroupIds(), tc.resultGroups)
assert.True(t, equalPrincipals(got.GetItem(), append(tc.resultUsers, tc.resultGroups...)))
})
}
}
@@ -926,7 +948,6 @@ func TestAddPrincipal(t *testing.T) {
OrgId: "",
ProjectId: role.GetScopeId(),
RoleId: role.GetPublicId(),
GroupIds: []string{},
Version: &wrapperspb.UInt32Value{Value: role.GetVersion()},
},
errCode: codes.InvalidArgument,
@@ -937,7 +958,6 @@ func TestAddPrincipal(t *testing.T) {
OrgId: o.GetPublicId(),
ProjectId: "bad id",
RoleId: role.GetPublicId(),
GroupIds: []string{},
Version: &wrapperspb.UInt32Value{Value: role.GetVersion()},
},
errCode: codes.InvalidArgument,
@@ -948,7 +968,6 @@ func TestAddPrincipal(t *testing.T) {
OrgId: o.GetPublicId(),
ProjectId: role.GetScopeId(),
RoleId: "bad id",
GroupIds: []string{},
Version: &wrapperspb.UInt32Value{Value: role.GetVersion()},
},
errCode: codes.InvalidArgument,
@@ -1039,11 +1058,10 @@ func TestSetPrincipal(t *testing.T) {
role := iam.TestRole(t, conn, scope.GetPublicId())
tc.setup(role)
req := &pbs.SetRolePrincipalsRequest{
OrgId: o.GetPublicId(),
RoleId: role.GetPublicId(),
Version: &wrapperspb.UInt32Value{Value: role.GetVersion()},
UserIds: tc.setUsers,
GroupIds: tc.setGroups,
OrgId: o.GetPublicId(),
RoleId: role.GetPublicId(),
Version: &wrapperspb.UInt32Value{Value: role.GetVersion()},
PrincipalIds: append(tc.setUsers, tc.setGroups...),
}
got, err := s.SetRolePrincipals(context.Background(), req)
@@ -1055,8 +1073,7 @@ func TestSetPrincipal(t *testing.T) {
require.True(t, ok)
require.NoError(t, err, "Got error: %v", s)
assert.Equal(t, got.GetItem().GetUserIds(), tc.resultUsers)
assert.Equal(t, got.GetItem().GetGroupIds(), tc.resultGroups)
assert.True(t, equalPrincipals(got.GetItem(), append(tc.resultUsers, tc.resultGroups...)))
})
}
}
@@ -1074,7 +1091,6 @@ func TestSetPrincipal(t *testing.T) {
OrgId: "",
ProjectId: role.GetScopeId(),
RoleId: role.GetPublicId(),
GroupIds: []string{},
Version: &wrapperspb.UInt32Value{Value: role.GetVersion()},
},
errCode: codes.InvalidArgument,
@@ -1085,7 +1101,6 @@ func TestSetPrincipal(t *testing.T) {
OrgId: o.GetPublicId(),
ProjectId: "bad id",
RoleId: role.GetPublicId(),
GroupIds: []string{},
Version: &wrapperspb.UInt32Value{Value: role.GetVersion()},
},
errCode: codes.InvalidArgument,
@@ -1096,7 +1111,6 @@ func TestSetPrincipal(t *testing.T) {
OrgId: o.GetPublicId(),
ProjectId: role.GetScopeId(),
RoleId: "bad id",
GroupIds: []string{},
Version: &wrapperspb.UInt32Value{Value: role.GetVersion()},
},
errCode: codes.InvalidArgument,
@@ -1205,11 +1219,10 @@ func TestRemovePrincipal(t *testing.T) {
role := iam.TestRole(t, conn, scope.GetPublicId())
tc.setup(role)
req := &pbs.RemoveRolePrincipalsRequest{
OrgId: o.GetPublicId(),
RoleId: role.GetPublicId(),
Version: &wrapperspb.UInt32Value{Value: role.GetVersion()},
UserIds: tc.removeUsers,
GroupIds: tc.removeGroups,
OrgId: o.GetPublicId(),
RoleId: role.GetPublicId(),
Version: &wrapperspb.UInt32Value{Value: role.GetVersion()},
PrincipalIds: append(tc.removeUsers, tc.removeGroups...),
}
got, err := s.RemoveRolePrincipals(context.Background(), req)
@@ -1221,8 +1234,7 @@ func TestRemovePrincipal(t *testing.T) {
require.True(t, ok)
require.NoError(t, err, "Got error: %v", s)
assert.ElementsMatch(t, got.GetItem().GetUserIds(), tc.resultUsers)
assert.ElementsMatch(t, got.GetItem().GetGroupIds(), tc.resultGroups)
assert.True(t, equalPrincipals(got.GetItem(), append(tc.resultUsers, tc.resultGroups...)))
})
}
}
@@ -1240,7 +1252,6 @@ func TestRemovePrincipal(t *testing.T) {
OrgId: "",
ProjectId: role.GetScopeId(),
RoleId: role.GetPublicId(),
GroupIds: []string{},
Version: &wrapperspb.UInt32Value{Value: role.GetVersion()},
},
errCode: codes.InvalidArgument,
@@ -1251,7 +1262,6 @@ func TestRemovePrincipal(t *testing.T) {
OrgId: o.GetPublicId(),
ProjectId: "bad id",
RoleId: role.GetPublicId(),
GroupIds: []string{},
Version: &wrapperspb.UInt32Value{Value: role.GetVersion()},
},
errCode: codes.InvalidArgument,
@@ -1262,7 +1272,6 @@ func TestRemovePrincipal(t *testing.T) {
OrgId: o.GetPublicId(),
ProjectId: role.GetScopeId(),
RoleId: "bad id",
GroupIds: []string{},
Version: &wrapperspb.UInt32Value{Value: role.GetVersion()},
},
errCode: codes.InvalidArgument,
+8 -2
View File
@@ -120,6 +120,11 @@ type TestControllerOpts struct {
// If true, the controller will not be started
DisableAutoStart bool
// DisableAuthorizationFailures will still cause authz checks to be
// performed but they won't cause 403 Forbidden. Useful for API-level
// testing to avoid a lot of faff.
DisableAuthorizationFailures bool
}
func NewTestController(t *testing.T, opts *TestControllerOpts) *TestController {
@@ -183,8 +188,9 @@ func NewTestController(t *testing.T, opts *TestControllerOpts) *TestController {
}
conf := &Config{
RawConfig: opts.Config,
Server: tc.b,
RawConfig: opts.Config,
Server: tc.b,
DisableAuthorizationFailures: opts.DisableAuthorizationFailures,
}
tc.c, err = New(conf)