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:
Juan Fernandez 2025-12-03 15:42:03 -04:00 committed by GitHub
parent 5b171d9a99
commit 068ffeaf40
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 102 additions and 34 deletions

View 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.

View file

@ -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"`
}

View file

@ -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

View file

@ -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

View file

@ -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...)

View file

@ -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())
})
}
}