mirror of https://github.com/status-im/consul.git
agent: /v1/agent/connect/authorize is functional, with tests
This commit is contained in:
parent
3ef0b93159
commit
86a8ce45b9
|
@ -4,6 +4,7 @@ import (
|
|||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
|
@ -885,6 +886,85 @@ func (s *HTTPServer) AgentConnectCALeafCert(resp http.ResponseWriter, req *http.
|
|||
//
|
||||
// POST /v1/agent/connect/authorize
|
||||
func (s *HTTPServer) AgentConnectAuthorize(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||
// NOTE(mitchellh): return 200 for now. To be implemented later.
|
||||
return nil, nil
|
||||
// Decode the request from the request body
|
||||
var authReq structs.ConnectAuthorizeRequest
|
||||
if err := decodeBody(req, &authReq, nil); err != nil {
|
||||
resp.WriteHeader(http.StatusBadRequest)
|
||||
fmt.Fprintf(resp, "Request decode failed: %v", err)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// We need to have a target to check intentions
|
||||
if authReq.Target == "" {
|
||||
resp.WriteHeader(http.StatusBadRequest)
|
||||
fmt.Fprintf(resp, "Target service must be specified")
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Parse the certificate URI from the client ID
|
||||
uriRaw, err := url.Parse(authReq.ClientID)
|
||||
if err != nil {
|
||||
return &connectAuthorizeResp{
|
||||
Authorized: false,
|
||||
Reason: fmt.Sprintf("Client ID must be a URI: %s", err),
|
||||
}, nil
|
||||
}
|
||||
uri, err := connect.ParseCertURI(uriRaw)
|
||||
if err != nil {
|
||||
return &connectAuthorizeResp{
|
||||
Authorized: false,
|
||||
Reason: fmt.Sprintf("Invalid client ID: %s", err),
|
||||
}, nil
|
||||
}
|
||||
|
||||
uriService, ok := uri.(*connect.SpiffeIDService)
|
||||
if !ok {
|
||||
return &connectAuthorizeResp{
|
||||
Authorized: false,
|
||||
Reason: fmt.Sprintf("Client ID must be a valid SPIFFE service URI"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Get the intentions for this target service
|
||||
args := &structs.IntentionQueryRequest{
|
||||
Datacenter: s.agent.config.Datacenter,
|
||||
Match: &structs.IntentionQueryMatch{
|
||||
Type: structs.IntentionMatchDestination,
|
||||
Entries: []structs.IntentionMatchEntry{
|
||||
{
|
||||
Namespace: structs.IntentionDefaultNamespace,
|
||||
Name: authReq.Target,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
var reply structs.IndexedIntentionMatches
|
||||
if err := s.agent.RPC("Intention.Match", args, &reply); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(reply.Matches) != 1 {
|
||||
return nil, fmt.Errorf("Internal error loading matches")
|
||||
}
|
||||
|
||||
// Test the authorization for each match
|
||||
for _, ixn := range reply.Matches[0] {
|
||||
if auth, ok := uriService.Authorize(ixn); ok {
|
||||
return &connectAuthorizeResp{
|
||||
Authorized: auth,
|
||||
Reason: fmt.Sprintf("Matched intention %s", ixn.ID),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(mitchellh): default behavior here for now is "deny" but we
|
||||
// should consider how this is determined.
|
||||
return &connectAuthorizeResp{
|
||||
Authorized: false,
|
||||
Reason: "No matching intention, using default behavior",
|
||||
}, nil
|
||||
}
|
||||
|
||||
type connectAuthorizeResp struct {
|
||||
Authorized bool
|
||||
Reason string
|
||||
}
|
||||
|
|
|
@ -2130,3 +2130,165 @@ func TestAgentConnectCALeafCert_good(t *testing.T) {
|
|||
|
||||
// TODO(mitchellh): verify the private key matches the cert
|
||||
}
|
||||
|
||||
func TestAgentConnectAuthorize_badBody(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert := assert.New(t)
|
||||
a := NewTestAgent(t.Name(), "")
|
||||
defer a.Shutdown()
|
||||
|
||||
args := []string{}
|
||||
req, _ := http.NewRequest("POST", "/v1/agent/connect/authorize", jsonReader(args))
|
||||
resp := httptest.NewRecorder()
|
||||
_, err := a.srv.AgentConnectAuthorize(resp, req)
|
||||
assert.Nil(err)
|
||||
assert.Equal(400, resp.Code)
|
||||
assert.Contains(resp.Body.String(), "decode")
|
||||
}
|
||||
|
||||
func TestAgentConnectAuthorize_noTarget(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert := assert.New(t)
|
||||
a := NewTestAgent(t.Name(), "")
|
||||
defer a.Shutdown()
|
||||
|
||||
args := &structs.ConnectAuthorizeRequest{}
|
||||
req, _ := http.NewRequest("POST", "/v1/agent/connect/authorize", jsonReader(args))
|
||||
resp := httptest.NewRecorder()
|
||||
_, err := a.srv.AgentConnectAuthorize(resp, req)
|
||||
assert.Nil(err)
|
||||
assert.Equal(400, resp.Code)
|
||||
assert.Contains(resp.Body.String(), "Target service")
|
||||
}
|
||||
|
||||
// Client ID is not in the valid URI format
|
||||
func TestAgentConnectAuthorize_idInvalidFormat(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert := assert.New(t)
|
||||
a := NewTestAgent(t.Name(), "")
|
||||
defer a.Shutdown()
|
||||
|
||||
args := &structs.ConnectAuthorizeRequest{
|
||||
Target: "web",
|
||||
ClientID: "tubes",
|
||||
}
|
||||
req, _ := http.NewRequest("POST", "/v1/agent/connect/authorize", jsonReader(args))
|
||||
resp := httptest.NewRecorder()
|
||||
respRaw, err := a.srv.AgentConnectAuthorize(resp, req)
|
||||
assert.Nil(err)
|
||||
assert.Equal(200, resp.Code)
|
||||
|
||||
obj := respRaw.(*connectAuthorizeResp)
|
||||
assert.False(obj.Authorized)
|
||||
assert.Contains(obj.Reason, "Invalid client")
|
||||
}
|
||||
|
||||
// Client ID is a valid URI but its not a service URI
|
||||
func TestAgentConnectAuthorize_idNotService(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert := assert.New(t)
|
||||
a := NewTestAgent(t.Name(), "")
|
||||
defer a.Shutdown()
|
||||
|
||||
args := &structs.ConnectAuthorizeRequest{
|
||||
Target: "web",
|
||||
ClientID: "spiffe://1234.consul",
|
||||
}
|
||||
req, _ := http.NewRequest("POST", "/v1/agent/connect/authorize", jsonReader(args))
|
||||
resp := httptest.NewRecorder()
|
||||
respRaw, err := a.srv.AgentConnectAuthorize(resp, req)
|
||||
assert.Nil(err)
|
||||
assert.Equal(200, resp.Code)
|
||||
|
||||
obj := respRaw.(*connectAuthorizeResp)
|
||||
assert.False(obj.Authorized)
|
||||
assert.Contains(obj.Reason, "must be a valid")
|
||||
}
|
||||
|
||||
// Test when there is an intention allowing the connection
|
||||
func TestAgentConnectAuthorize_allow(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert := assert.New(t)
|
||||
a := NewTestAgent(t.Name(), "")
|
||||
defer a.Shutdown()
|
||||
|
||||
target := "db"
|
||||
|
||||
// Create some intentions
|
||||
{
|
||||
req := structs.IntentionRequest{
|
||||
Datacenter: "dc1",
|
||||
Op: structs.IntentionOpCreate,
|
||||
Intention: structs.TestIntention(t),
|
||||
}
|
||||
req.Intention.SourceNS = structs.IntentionDefaultNamespace
|
||||
req.Intention.SourceName = "web"
|
||||
req.Intention.DestinationNS = structs.IntentionDefaultNamespace
|
||||
req.Intention.DestinationName = target
|
||||
req.Intention.Action = structs.IntentionActionAllow
|
||||
|
||||
var reply string
|
||||
assert.Nil(a.RPC("Intention.Apply", &req, &reply))
|
||||
}
|
||||
|
||||
args := &structs.ConnectAuthorizeRequest{
|
||||
Target: target,
|
||||
ClientID: connect.TestSpiffeIDService(t, "web").URI().String(),
|
||||
}
|
||||
req, _ := http.NewRequest("POST", "/v1/agent/connect/authorize", jsonReader(args))
|
||||
resp := httptest.NewRecorder()
|
||||
respRaw, err := a.srv.AgentConnectAuthorize(resp, req)
|
||||
assert.Nil(err)
|
||||
assert.Equal(200, resp.Code)
|
||||
|
||||
obj := respRaw.(*connectAuthorizeResp)
|
||||
assert.True(obj.Authorized)
|
||||
assert.Contains(obj.Reason, "Matched")
|
||||
}
|
||||
|
||||
// Test when there is an intention denying the connection
|
||||
func TestAgentConnectAuthorize_deny(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
assert := assert.New(t)
|
||||
a := NewTestAgent(t.Name(), "")
|
||||
defer a.Shutdown()
|
||||
|
||||
target := "db"
|
||||
|
||||
// Create some intentions
|
||||
{
|
||||
req := structs.IntentionRequest{
|
||||
Datacenter: "dc1",
|
||||
Op: structs.IntentionOpCreate,
|
||||
Intention: structs.TestIntention(t),
|
||||
}
|
||||
req.Intention.SourceNS = structs.IntentionDefaultNamespace
|
||||
req.Intention.SourceName = "web"
|
||||
req.Intention.DestinationNS = structs.IntentionDefaultNamespace
|
||||
req.Intention.DestinationName = target
|
||||
req.Intention.Action = structs.IntentionActionDeny
|
||||
|
||||
var reply string
|
||||
assert.Nil(a.RPC("Intention.Apply", &req, &reply))
|
||||
}
|
||||
|
||||
args := &structs.ConnectAuthorizeRequest{
|
||||
Target: target,
|
||||
ClientID: connect.TestSpiffeIDService(t, "web").URI().String(),
|
||||
}
|
||||
req, _ := http.NewRequest("POST", "/v1/agent/connect/authorize", jsonReader(args))
|
||||
resp := httptest.NewRecorder()
|
||||
respRaw, err := a.srv.AgentConnectAuthorize(resp, req)
|
||||
assert.Nil(err)
|
||||
assert.Equal(200, resp.Code)
|
||||
|
||||
obj := respRaw.(*connectAuthorizeResp)
|
||||
assert.False(obj.Authorized)
|
||||
assert.Contains(obj.Reason, "Matched")
|
||||
}
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
package structs
|
||||
|
||||
// ConnectAuthorizeRequest is the structure of a request to authorize
|
||||
// a connection.
|
||||
type ConnectAuthorizeRequest struct {
|
||||
// Target is the name of the service that is being requested.
|
||||
Target string
|
||||
|
||||
// ClientID is a unique identifier for the requesting client. This
|
||||
// is currently the URI SAN from the TLS client certificate.
|
||||
//
|
||||
// ClientCertSerial is a colon-hex-encoded of the serial number for
|
||||
// the requesting client cert. This is used to check against revocation
|
||||
// lists.
|
||||
ClientID string
|
||||
ClientCertSerial string
|
||||
}
|
Loading…
Reference in New Issue