mirror of
https://github.com/fleetdm/fleet
synced 2026-05-23 08:58:41 +00:00
Use auth header for android end-points (#36594)
**Related issue:** Resolves #36287 Updated 'fleetd/certificates/<id>' and 'fleetd/certificates/<id>/status' to authenticate using the orbit_node_key provided in the 'Authentication' header.
This commit is contained in:
parent
5b171d9a99
commit
068ffeaf40
6 changed files with 102 additions and 34 deletions
1
changes/36287-android-cert-crud-use-auth-header
Normal file
1
changes/36287-android-cert-crud-use-auth-header
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Updated 'fleetd/certificates/<id>' and 'fleetd/certificates/<id>/status' to authenticate using the orbit_node_key provided in the 'Authentication' header.
|
||||
|
|
@ -115,12 +115,7 @@ func (svc *Service) ListCertificateTemplates(ctx context.Context, teamID uint, o
|
|||
}
|
||||
|
||||
type getDeviceCertificateTemplateRequest struct {
|
||||
ID uint `url:"id"`
|
||||
NodeKey string `query:"node_key"`
|
||||
}
|
||||
|
||||
func (r *getDeviceCertificateTemplateRequest) hostNodeKey() string {
|
||||
return r.NodeKey
|
||||
ID uint `url:"id"`
|
||||
}
|
||||
|
||||
type getDeviceCertificateTemplateResponse struct {
|
||||
|
|
@ -349,17 +344,12 @@ func (svc *Service) DeleteCertificateTemplateSpecs(ctx context.Context, certific
|
|||
|
||||
type updateCertificateStatusRequest struct {
|
||||
CertificateTemplateID uint `url:"id"`
|
||||
NodeKey string `json:"node_key"`
|
||||
Status string `json:"status"`
|
||||
// Detail provides additional information about the status change.
|
||||
// For example, it can be used to provide a reason for a failed status change.
|
||||
Detail *string `json:"detail,omitempty"`
|
||||
}
|
||||
|
||||
func (r *updateCertificateStatusRequest) hostNodeKey() string {
|
||||
return r.NodeKey
|
||||
}
|
||||
|
||||
type updateCertificateStatusResponse struct {
|
||||
Err error `json:"error,omitempty"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/certserial"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/logging"
|
||||
|
|
@ -157,9 +158,14 @@ func authenticatedHost(svc fleet.Service, logger log.Logger, next endpoint.Endpo
|
|||
return middleware_log.Logged(authHostFunc)
|
||||
}
|
||||
|
||||
func authenticatedOrbitHost(svc fleet.Service, logger log.Logger, next endpoint.Endpoint) endpoint.Endpoint {
|
||||
func authenticatedOrbitHost(
|
||||
svc fleet.Service,
|
||||
logger log.Logger,
|
||||
next endpoint.Endpoint,
|
||||
orbitNodeKeyGetter func(context.Context, interface{}) (string, error),
|
||||
) endpoint.Endpoint {
|
||||
authHostFunc := func(ctx context.Context, request interface{}) (interface{}, error) {
|
||||
nodeKey, err := getOrbitNodeKey(request)
|
||||
nodeKey, err := orbitNodeKeyGetter(ctx, request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -194,13 +200,22 @@ func authenticatedOrbitHost(svc fleet.Service, logger log.Logger, next endpoint.
|
|||
return middleware_log.Logged(authHostFunc)
|
||||
}
|
||||
|
||||
func getOrbitNodeKey(r interface{}) (string, error) {
|
||||
func getOrbitNodeKey(ctx context.Context, r interface{}) (string, error) {
|
||||
if onk, err := r.(interface{ orbitHostNodeKey() string }); err {
|
||||
return onk.orbitHostNodeKey(), nil
|
||||
}
|
||||
return "", errors.New("error getting orbit node key")
|
||||
}
|
||||
|
||||
func authHeaderValue(prefix string) func(ctx context.Context, r interface{}) (string, error) {
|
||||
return func(ctx context.Context, r interface{}) (string, error) {
|
||||
if authHeader, ok := ctx.Value(kithttp.ContextKeyRequestAuthorization).(string); ok {
|
||||
return strings.TrimPrefix(authHeader, prefix), nil
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
}
|
||||
|
||||
func getNodeKey(r interface{}) (string, error) {
|
||||
if hnk, ok := r.(interface{ hostNodeKey() string }); ok {
|
||||
return hnk.hostNodeKey(), nil
|
||||
|
|
|
|||
|
|
@ -217,11 +217,39 @@ func newHostAuthenticatedEndpointer(svc fleet.Service, logger log.Logger, opts [
|
|||
}
|
||||
}
|
||||
|
||||
func androidAuthenticatedEndpointer(
|
||||
svc fleet.Service,
|
||||
logger log.Logger,
|
||||
opts []kithttp.ServerOption,
|
||||
r *mux.Router,
|
||||
versions ...string,
|
||||
) *eu.CommonEndpointer[eu.HandlerFunc] {
|
||||
// Inject the fleet.Capabilities header to the response for Orbit hosts
|
||||
opts = append(opts, capabilitiesResponseFunc(fleet.GetServerOrbitCapabilities()))
|
||||
// Add the capabilities reported by Orbit to the request context
|
||||
opts = append(opts, capabilitiesContextFunc())
|
||||
|
||||
return &eu.CommonEndpointer[eu.HandlerFunc]{
|
||||
EP: &endpointer{
|
||||
svc: svc,
|
||||
},
|
||||
MakeDecoderFn: makeDecoder,
|
||||
EncodeFn: encodeResponse,
|
||||
Opts: opts,
|
||||
AuthFunc: func(svc fleet.Service, next endpoint.Endpoint) endpoint.Endpoint {
|
||||
return authenticatedOrbitHost(svc, logger, next, authHeaderValue("Node key "))
|
||||
},
|
||||
FleetService: svc,
|
||||
Router: r,
|
||||
Versions: versions,
|
||||
}
|
||||
}
|
||||
|
||||
func newOrbitAuthenticatedEndpointer(svc fleet.Service, logger log.Logger, opts []kithttp.ServerOption, r *mux.Router,
|
||||
versions ...string,
|
||||
) *eu.CommonEndpointer[eu.HandlerFunc] {
|
||||
authFunc := func(svc fleet.Service, next endpoint.Endpoint) endpoint.Endpoint {
|
||||
return authenticatedOrbitHost(svc, logger, next)
|
||||
return authenticatedOrbitHost(svc, logger, next, getOrbitNodeKey)
|
||||
}
|
||||
|
||||
// Inject the fleet.Capabilities header to the response for Orbit hosts
|
||||
|
|
|
|||
|
|
@ -906,10 +906,14 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
|
|||
POST("/api/osquery/log", submitLogsEndpoint, submitLogsRequest{})
|
||||
he.WithAltPaths("/api/v1/osquery/yara/{name}").
|
||||
POST("/api/osquery/yara/{name}", getYaraEndpoint, getYaraRequest{})
|
||||
he.WithAltPaths("/api/v1/fleetd/certificates/{id:[0-9]+}").
|
||||
GET("/api/fleetd/certificates/{id:[0-9]+}", getDeviceCertificateTemplateEndpoint, getDeviceCertificateTemplateRequest{})
|
||||
he.WithAltPaths("/api/v1/fleetd/certificates/{id:[0-9]+}/status").
|
||||
PUT("/api/fleetd/certificates/{id:[0-9]+}/status", updateCertificateStatusEndpoint, updateCertificateStatusRequest{})
|
||||
|
||||
// android authenticated end-points
|
||||
// Authentication is implemented using the orbit_node_key from the 'Authentication' header.
|
||||
// The 'orbit_node_key' is used because it's the only thing we have available when the device gets enrolled
|
||||
// after the MDM setup is complete.
|
||||
androidEndpoints := androidAuthenticatedEndpointer(svc, logger, opts, r, apiVersions...)
|
||||
androidEndpoints.GET("/api/fleetd/certificates/{id:[0-9]+}", getDeviceCertificateTemplateEndpoint, getDeviceCertificateTemplateRequest{})
|
||||
androidEndpoints.PUT("/api/fleetd/certificates/{id:[0-9]+}/status", updateCertificateStatusEndpoint, updateCertificateStatusRequest{})
|
||||
|
||||
// orbit authenticated endpoints
|
||||
oe := newOrbitAuthenticatedEndpointer(svc, logger, opts, r, apiVersions...)
|
||||
|
|
|
|||
|
|
@ -8027,6 +8027,10 @@ func (s *integrationTestSuite) TestCertificatesSpecs() {
|
|||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
orbitNodeKey := uuid.New().String()
|
||||
host.OrbitNodeKey = &orbitNodeKey
|
||||
require.NoError(t, s.ds.UpdateHost(ctx, host))
|
||||
|
||||
// Add an IDP user for the host
|
||||
err = s.ds.ReplaceHostDeviceMapping(ctx, host.ID, []*fleet.HostDeviceMapping{
|
||||
{
|
||||
|
|
@ -8043,10 +8047,16 @@ func (s *integrationTestSuite) TestCertificatesSpecs() {
|
|||
|
||||
// Get certificate without node_key
|
||||
var getCertResp getDeviceCertificateTemplateResponse
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/v1/fleetd/certificates/%d", certID), nil, http.StatusBadRequest, &getCertResp)
|
||||
|
||||
resp := s.DoRawWithHeaders("GET", fmt.Sprintf("/api/fleetd/certificates/%d", certID), nil, http.StatusUnauthorized, nil)
|
||||
require.NoError(t, resp.Body.Close())
|
||||
|
||||
// Get certificate with node_key (should return replaced variables)
|
||||
s.DoJSON("GET", fmt.Sprintf("/api/v1/fleetd/certificates/%d?node_key=%s", certID, *host.NodeKey), nil, http.StatusOK, &getCertResp)
|
||||
resp = s.DoRawWithHeaders("GET", fmt.Sprintf("/api/fleetd/certificates/%d", certID), nil, http.StatusOK, map[string]string{
|
||||
"Authorization": fmt.Sprintf("Node key %s", orbitNodeKey),
|
||||
})
|
||||
require.NoError(t, json.NewDecoder(resp.Body).Decode(&getCertResp))
|
||||
require.NoError(t, resp.Body.Close())
|
||||
require.NotNil(t, getCertResp.Certificate)
|
||||
|
||||
assert.Contains(t, getCertResp.Certificate.SubjectName, "test.user@example.com")
|
||||
|
|
@ -14661,13 +14671,13 @@ func (s *integrationTestSuite) TestUpdateHostCertificateTemplate() {
|
|||
require.NoError(t, err)
|
||||
require.NotNil(t, savedTemplate)
|
||||
|
||||
nodeKey := uuid.New().String()
|
||||
orbitNodeKey := uuid.New().String()
|
||||
uuid := uuid.New().String()
|
||||
hostName := "test-update-host-certificate-template"
|
||||
|
||||
// Create a host
|
||||
host, err := s.ds.NewHost(context.Background(), &fleet.Host{
|
||||
NodeKey: &nodeKey,
|
||||
NodeKey: &orbitNodeKey,
|
||||
UUID: uuid,
|
||||
Hostname: hostName,
|
||||
Platform: "android",
|
||||
|
|
@ -14675,6 +14685,9 @@ func (s *integrationTestSuite) TestUpdateHostCertificateTemplate() {
|
|||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
host.OrbitNodeKey = &orbitNodeKey
|
||||
require.NoError(t, s.ds.UpdateHost(ctx, host))
|
||||
|
||||
certificateTemplateID := savedTemplate.ID
|
||||
|
||||
// Delete the certificate after the test is done, so the team can be deleted.
|
||||
|
|
@ -14703,32 +14716,45 @@ INSERT INTO host_certificate_templates (
|
|||
cases := []struct {
|
||||
name string
|
||||
templateID uint
|
||||
nodeKey string
|
||||
newStatus string
|
||||
detail *string
|
||||
expectedResponseStatus int
|
||||
expectedResponseMessage string
|
||||
headers map[string]string
|
||||
}{
|
||||
{
|
||||
name: "Valid Update",
|
||||
templateID: certificateTemplateID,
|
||||
nodeKey: nodeKey,
|
||||
newStatus: "verified",
|
||||
detail: ptr.String("Certificate Verified"),
|
||||
expectedResponseStatus: http.StatusOK,
|
||||
headers: map[string]string{
|
||||
"Authorization": fmt.Sprintf("Node key %s", orbitNodeKey),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Invalid Status",
|
||||
templateID: certificateTemplateID,
|
||||
nodeKey: nodeKey,
|
||||
newStatus: "invalid_status",
|
||||
expectedResponseStatus: http.StatusUnprocessableEntity,
|
||||
expectedResponseMessage: "invalid status value",
|
||||
headers: map[string]string{
|
||||
"Authorization": fmt.Sprintf("Node key %s", orbitNodeKey),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Wrong node key",
|
||||
templateID: certificateTemplateID,
|
||||
nodeKey: "wrong-nodekey",
|
||||
newStatus: "verified",
|
||||
expectedResponseStatus: http.StatusUnauthorized,
|
||||
expectedResponseMessage: "host certificate template not found",
|
||||
headers: map[string]string{
|
||||
"Authorization": "Node key wrong-node-key",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "With no auth headers",
|
||||
templateID: certificateTemplateID,
|
||||
newStatus: "verified",
|
||||
expectedResponseStatus: http.StatusUnauthorized,
|
||||
expectedResponseMessage: "host certificate template not found",
|
||||
|
|
@ -14736,21 +14762,25 @@ INSERT INTO host_certificate_templates (
|
|||
{
|
||||
name: "Wrong Template ID",
|
||||
templateID: 9999,
|
||||
nodeKey: nodeKey,
|
||||
newStatus: "verified",
|
||||
expectedResponseStatus: http.StatusNotFound,
|
||||
expectedResponseMessage: "host certificate template not found",
|
||||
headers: map[string]string{
|
||||
"Authorization": fmt.Sprintf("Node key %s", orbitNodeKey),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(fmt.Sprintf("TestUpdateHostCertificateTemplate:%s", tc.name), func(t *testing.T) {
|
||||
var resp map[string]interface{}
|
||||
s.DoJSON("PUT", fmt.Sprintf("/api/fleetd/certificates/%d/status", tc.templateID), updateCertificateStatusRequest{
|
||||
NodeKey: tc.nodeKey,
|
||||
Status: tc.newStatus,
|
||||
Detail: tc.detail,
|
||||
}, tc.expectedResponseStatus, &resp)
|
||||
req, err := json.Marshal(updateCertificateStatusRequest{
|
||||
Status: tc.newStatus,
|
||||
Detail: tc.detail,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
resp := s.DoRawWithHeaders("PUT", fmt.Sprintf("/api/fleetd/certificates/%d/status", tc.templateID), req, tc.expectedResponseStatus, tc.headers)
|
||||
require.NoError(t, resp.Body.Close())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue