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"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
@ -885,6 +886,85 @@ func (s *HTTPServer) AgentConnectCALeafCert(resp http.ResponseWriter, req *http.
|
||||||
//
|
//
|
||||||
// POST /v1/agent/connect/authorize
|
// POST /v1/agent/connect/authorize
|
||||||
func (s *HTTPServer) AgentConnectAuthorize(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
func (s *HTTPServer) AgentConnectAuthorize(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||||
// NOTE(mitchellh): return 200 for now. To be implemented later.
|
// 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
|
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
|
// 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