Authenticate Handler API and SDK, and much more (#183)
This commit is contained in:
1 parent
acce195dcb
commit
28dbcd84bd
89 files changed
+5296
-1943
No files matched your search
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user