mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
Add endpoint to trigger CSR request for APNs on fleetdm.com (#9494)
This commit is contained in:
parent
f095431f12
commit
d0e6891d10
16 changed files with 274 additions and 67 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -75,3 +75,8 @@ test_tuf
|
|||
# Residual files when running the msrc generate command
|
||||
msrc_in/
|
||||
msrc_out/
|
||||
|
||||
# Keys and certificates that may be generated in the root of the repo
|
||||
# (e.g. with ./build/fleetctl generate ...).
|
||||
/*.key
|
||||
/*.crt
|
||||
|
|
|
|||
1
changes/issue-9202-request-csr-endpoint
Normal file
1
changes/issue-9202-request-csr-endpoint
Normal file
|
|
@ -0,0 +1 @@
|
|||
* Added the `POST /api/v1/fleet/mdm/apple/request_csr` endpoint to trigger a Certificate Signing Request to fleetdm.com and return the associated APNs private key and SCEP certificate and key.
|
||||
|
|
@ -9,9 +9,7 @@ import (
|
|||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
|
||||
"github.com/fleetdm/fleet/v4/server/service"
|
||||
|
|
@ -108,6 +106,13 @@ func generateMDMAppleCommand() *cli.Command {
|
|||
scepCACertPath := c.String("scep-cert")
|
||||
scepCAKeyPath := c.String("scep-key")
|
||||
|
||||
// get the fleet API client first, so that any login requirement are met
|
||||
// before printing the CSR output message.
|
||||
client, err := clientFromCLI(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintf(
|
||||
c.App.Writer,
|
||||
`Sending certificate signing request (CSR) for Apple Push Notification service (APNs) to %s...
|
||||
|
|
@ -117,45 +122,21 @@ Generating APNs key, Simple Certificate Enrollment Protocol (SCEP) certificate,
|
|||
email,
|
||||
)
|
||||
|
||||
// create apns csr and send to fleetdm.com
|
||||
apnsCSR, apnsKey, err := apple_mdm.GenerateAPNSCSRKey(email, org)
|
||||
csr, err := client.RequestAppleCSR(email, org)
|
||||
if err != nil {
|
||||
return fmt.Errorf("generate apns csr: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
apnsKeyPEM := apple_mdm.EncodePrivateKeyPEM(apnsKey)
|
||||
|
||||
err = os.WriteFile(apnsKeyPath, apnsKeyPEM, 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write private key: %w", err)
|
||||
if err := os.WriteFile(apnsKeyPath, csr.APNsKey, 0600); err != nil {
|
||||
return fmt.Errorf("failed to write APNs private key: %w", err)
|
||||
}
|
||||
if err := os.WriteFile(scepCACertPath, csr.SCEPCert, 0600); err != nil {
|
||||
return fmt.Errorf("failed to write SCEP CA certificate: %w", err)
|
||||
}
|
||||
if err := os.WriteFile(scepCAKeyPath, csr.SCEPKey, 0600); err != nil {
|
||||
return fmt.Errorf("failed to write SCEP CA private key: %w", err)
|
||||
}
|
||||
|
||||
client := fleethttp.NewClient(fleethttp.WithTimeout(10 * time.Second))
|
||||
|
||||
err = apple_mdm.GetSignedAPNSCSR(client, apnsCSR)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get signed apns csr: %w", err)
|
||||
}
|
||||
|
||||
// init scep ca
|
||||
scepCACert, scepCAKey, err := apple_mdm.NewSCEPCACertKey()
|
||||
if err != nil {
|
||||
return fmt.Errorf("init scep CA: %w", err)
|
||||
}
|
||||
|
||||
scepCACertPEM := apple_mdm.EncodeCertPEM(scepCACert)
|
||||
err = os.WriteFile(scepCACertPath, scepCACertPEM, 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write scep ca certificate: %w", err)
|
||||
}
|
||||
|
||||
scepCAKeyPEM := apple_mdm.EncodePrivateKeyPEM(scepCAKey)
|
||||
err = os.WriteFile(scepCAKeyPath, scepCAKeyPEM, 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write scep ca private key: %w", err)
|
||||
}
|
||||
|
||||
// TODO: update text once https://github.com/fleetdm/fleet/issues/8595 is complete. Consider linking to specific configuration section.
|
||||
fmt.Fprintf(
|
||||
c.App.Writer,
|
||||
`Success!
|
||||
|
|
|
|||
|
|
@ -529,6 +529,8 @@ The MDM endpoints exist to support the related command-line interface sub-comman
|
|||
- [Get Apple MDM](#get-apple-mdm)
|
||||
- [Get Apple BM](#get-apple-bm)
|
||||
- [Unenroll host from Fleet MDM](#unenroll-host-from-fleet-mdm)
|
||||
- [Generate Apple DEP Key Pair](#generate-apple-dep-key-pair)
|
||||
- [Request Certificate Signing Request (CSR)](#request-certificate-signing-request-csr)
|
||||
|
||||
|
||||
### Get Apple MDM
|
||||
|
|
@ -623,6 +625,33 @@ None.
|
|||
|
||||
Note that the `public_key` and `private_key` are base64 encoded and should be decoded before writing them to files.
|
||||
|
||||
### Request Certificate Signing Request (CSR)
|
||||
|
||||
`POST /api/v1/fleet/mdm/apple/request_csr`
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type | In | Description |
|
||||
| ---- | ------- | ---- | --------------------------------------- |
|
||||
| email_address | string | body | **Required.** The email that will be associated with the Apple APNs certificate. |
|
||||
| organization | string | body | **Required.** The name of the organization associated with the Apple APNs certificate. |
|
||||
|
||||
#### Example
|
||||
|
||||
`POST /api/v1/fleet/mdm/apple/request_csr`
|
||||
|
||||
##### Default response
|
||||
|
||||
```
|
||||
{
|
||||
"apns_key": "aGV5LCBJJ20gc2VjcmV0Cg==",
|
||||
"scep_cert": "bHR5LCBJJ20gc2VjcmV0Cg=",
|
||||
"scep_key": "lKT5LCBJJ20gc2VjcmV0Cg="
|
||||
}
|
||||
```
|
||||
|
||||
Note that the response fields are base64 encoded and should be decoded before writing them to files. Once base64-decoded, they are PEM-encoded certificate and keys.
|
||||
|
||||
## Get or apply configuration files
|
||||
|
||||
These API routes are used by the `fleetctl` CLI tool. Users can manage Fleet with `fleetctl` and [configuration files in YAML syntax](https://fleetdm.com/docs/using-fleet/configuration-files/).
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ Users with the Admin role receive all permissions.
|
|||
| Retrieve contents from file carving | | | ✅ |
|
||||
| View Apple mobile device management (MDM) certificate information | | | ✅ |
|
||||
| View Apple business manager (BM) information | | | ✅ |
|
||||
| Generate Apple mobile device management (MDM) certificate signing request (CSR) | | | ✅ |
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,17 @@ func (a AppleBM) AuthzType() string {
|
|||
return "mdm_apple"
|
||||
}
|
||||
|
||||
type AppleCSR struct {
|
||||
// NOTE: []byte automatically JSON-encodes as a base64-encoded string
|
||||
APNsKey []byte `json:"apns_key"`
|
||||
SCEPCert []byte `json:"scep_cert"`
|
||||
SCEPKey []byte `json:"scep_key"`
|
||||
}
|
||||
|
||||
func (a AppleCSR) AuthzType() string {
|
||||
return "mdm_apple"
|
||||
}
|
||||
|
||||
// AppConfigUpdated is the minimal interface required to get and update the
|
||||
// AppConfig, as required to handle the DEP API errors to flag that Apple's
|
||||
// terms have changed and must be accepted. The Fleet Datastore satisfies
|
||||
|
|
|
|||
|
|
@ -542,6 +542,7 @@ type Service interface {
|
|||
|
||||
GetAppleMDM(ctx context.Context) (*AppleMDM, error)
|
||||
GetAppleBM(ctx context.Context) (*AppleBM, error)
|
||||
RequestMDMAppleCSR(ctx context.Context, email, org string) (*AppleCSR, error)
|
||||
|
||||
// NewMDMAppleEnrollmentProfile creates and returns new enrollment profile.
|
||||
// Such enrollment profiles allow devices to enroll to Fleet MDM.
|
||||
|
|
|
|||
|
|
@ -11,23 +11,24 @@ import (
|
|||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/micromdm/nanodep/tokenpki"
|
||||
"github.com/micromdm/scep/v2/depot"
|
||||
)
|
||||
|
||||
const defaultFleetDMAPIURL = "https://fleetdm.com"
|
||||
|
||||
const getSignedAPNSCSRPath = "/api/v1/get_signed_apns_csr"
|
||||
|
||||
const depCertificateCommonName = "FleetDM"
|
||||
|
||||
const depCertificateExpiryDays = 30
|
||||
const (
|
||||
defaultFleetDMAPIURL = "https://fleetdm.com"
|
||||
getSignedAPNSCSRPath = "/api/v1/deliver-apple-csr"
|
||||
depCertificateCommonName = "FleetDM"
|
||||
depCertificateExpiryDays = 30
|
||||
)
|
||||
|
||||
// emailAddressOID defined by https://oidref.com/1.2.840.113549.1.9.1
|
||||
var emailAddressOID = []int{1, 2, 840, 113549, 1, 9, 1}
|
||||
|
||||
// GenerateAPNSCSRKey generates a APNS csr to be sent to fleetdm.com and returns a csr and key.
|
||||
// GenerateAPNSCSRKey generates a APNS CSR (certificate signing request) and
|
||||
// returns the CSR and private key.
|
||||
func GenerateAPNSCSRKey(email, org string) (*x509.CertificateRequest, *rsa.PrivateKey, error) {
|
||||
key, err := newPrivateKey()
|
||||
if err != nil {
|
||||
|
|
@ -60,16 +61,16 @@ func GenerateAPNSCSRKey(email, org string) (*x509.CertificateRequest, *rsa.Priva
|
|||
}
|
||||
|
||||
type getSignedAPNSCSRRequest struct {
|
||||
// CSR is the pem encoded certificate request.
|
||||
CSR []byte `json:"csr"`
|
||||
UnsignedCSRData []byte `json:"unsignedCsrData"`
|
||||
}
|
||||
|
||||
// GetSignedAPNSCSR makes a request to the fleetdm.com API to get a signed apns csr that is sent to the email provided in the certificate subject.
|
||||
// GetSignedAPNSCSR makes a request to the fleetdm.com API to get a signed APNs
|
||||
// CSR that is sent to the email provided in the certificate subject.
|
||||
func GetSignedAPNSCSR(client *http.Client, csr *x509.CertificateRequest) error {
|
||||
csrPEM := EncodeCertRequestPEM(csr)
|
||||
|
||||
payload := getSignedAPNSCSRRequest{
|
||||
CSR: csrPEM,
|
||||
UnsignedCSRData: csrPEM,
|
||||
}
|
||||
|
||||
b, err := json.Marshal(payload)
|
||||
|
|
@ -80,9 +81,8 @@ func GetSignedAPNSCSR(client *http.Client, csr *x509.CertificateRequest) error {
|
|||
// for testing
|
||||
baseURL := defaultFleetDMAPIURL
|
||||
if x := os.Getenv("TEST_FLEETDM_API_URL"); x != "" {
|
||||
baseURL = x
|
||||
baseURL = strings.TrimRight(x, "/")
|
||||
}
|
||||
|
||||
u := baseURL + getSignedAPNSCSRPath
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, u, bytes.NewReader(b))
|
||||
|
|
@ -100,11 +100,11 @@ func GetSignedAPNSCSR(client *http.Client, csr *x509.CertificateRequest) error {
|
|||
b, _ := ioutil.ReadAll(resp.Body)
|
||||
return fmt.Errorf("api responded with %d: %s", resp.StatusCode, string(b))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewSCEPCACertKey creates a self-signed CA certificate for use with SCEP and returns the certificate and its private key.
|
||||
// NewSCEPCACertKey creates a self-signed CA certificate for use with SCEP and
|
||||
// returns the certificate and its private key.
|
||||
func NewSCEPCACertKey() (*x509.Certificate, *rsa.PrivateKey, error) {
|
||||
key, err := newPrivateKey()
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -17,3 +17,16 @@ func (c *Client) GetAppleBM() (*fleet.AppleBM, error) {
|
|||
err := c.authenticatedRequestWithQuery(nil, verb, path, &responseBody, "")
|
||||
return responseBody.AppleBM, err
|
||||
}
|
||||
|
||||
// RequestAppleCSR requests a signed CSR from the Fleet server and returns the
|
||||
// SCEP certificate and key along with the APNs key used for the CSR.
|
||||
func (c *Client) RequestAppleCSR(email, org string) (*fleet.AppleCSR, error) {
|
||||
verb, path := "POST", "/api/latest/fleet/mdm/apple/request_csr"
|
||||
request := requestMDMAppleCSRRequest{
|
||||
EmailAddress: email,
|
||||
Organization: org,
|
||||
}
|
||||
var responseBody requestMDMAppleCSRResponse
|
||||
err := c.authenticatedRequest(request, verb, path, &responseBody)
|
||||
return responseBody.AppleCSR, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -440,6 +440,9 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
|
|||
}
|
||||
ue.GET("/api/_version_/fleet/mdm/apple", getAppleMDMEndpoint, nil)
|
||||
ue.GET("/api/_version_/fleet/mdm/apple_bm", getAppleBMEndpoint, nil)
|
||||
// this endpoint must always be accessible (even if MDM is not configured) as
|
||||
// it bootstraps the setup of MDM (generates CSR request for APNs and SCEP).
|
||||
ue.POST("/api/_version_/fleet/mdm/apple/request_csr", requestMDMAppleCSREndpoint, requestMDMAppleCSRRequest{})
|
||||
|
||||
errorLimiter := ratelimit.NewErrorMiddleware(limitStore)
|
||||
|
||||
|
|
|
|||
|
|
@ -5871,13 +5871,7 @@ func (s *integrationTestSuite) TestHostsReportDownload() {
|
|||
require.NoError(t, s.ds.SetOrUpdateHostDisksSpace(ctx, hosts[1].ID, 3.0, 4.0))
|
||||
|
||||
res := s.DoRaw("GET", "/api/latest/fleet/hosts/report", nil, http.StatusUnsupportedMediaType, "format", "gzip")
|
||||
var errs struct {
|
||||
Message string `json:"message"`
|
||||
Errors []struct {
|
||||
Name string `json:"name"`
|
||||
Reason string `json:"reason"`
|
||||
} `json:"errors"`
|
||||
}
|
||||
var errs validationErrResp
|
||||
require.NoError(t, json.NewDecoder(res.Body).Decode(&errs))
|
||||
res.Body.Close()
|
||||
require.Len(t, errs.Errors, 1)
|
||||
|
|
@ -6260,8 +6254,9 @@ func (s *integrationTestSuite) TestPingEndpoints() {
|
|||
}
|
||||
|
||||
func (s *integrationTestSuite) TestAppleMDMNotConfigured() {
|
||||
var resp getAppleMDMResponse
|
||||
s.DoJSON("GET", "/api/latest/fleet/mdm/apple", nil, http.StatusNotFound, &resp)
|
||||
var rawResp json.RawMessage
|
||||
s.DoJSON("GET", "/api/latest/fleet/mdm/apple", nil, http.StatusNotFound, &rawResp)
|
||||
s.DoJSON("GET", "/api/latest/fleet/mdm/apple_bm", nil, http.StatusPaymentRequired, &rawResp) //premium only
|
||||
}
|
||||
|
||||
func (s *integrationTestSuite) TestOrbitConfigNotifications() {
|
||||
|
|
@ -6336,6 +6331,14 @@ func (s *integrationTestSuite) TestAPIVersion_v1_2022_04() {
|
|||
s.DoJSON("DELETE", fmt.Sprintf("/api/v1/fleet/global/schedule/%d", createResp.Scheduled.ID), nil, http.StatusOK, &delResp)
|
||||
}
|
||||
|
||||
type validationErrResp struct {
|
||||
Message string `json:"message"`
|
||||
Errors []struct {
|
||||
Name string `json:"name"`
|
||||
Reason string `json:"reason"`
|
||||
} `json:"errors"`
|
||||
}
|
||||
|
||||
func createOrbitEnrolledHost(t *testing.T, os, suffix string, ds fleet.Datastore) *fleet.Host {
|
||||
name := t.Name() + suffix
|
||||
h, err := ds.NewHost(context.Background(), &fleet.Host{
|
||||
|
|
|
|||
|
|
@ -1835,10 +1835,9 @@ func (s *integrationEnterpriseTestSuite) TestListHosts() {
|
|||
}
|
||||
|
||||
func (s *integrationEnterpriseTestSuite) TestAppleMDMNotConfigured() {
|
||||
var mdmResp getAppleMDMResponse
|
||||
s.DoJSON("GET", "/api/latest/fleet/mdm/apple", nil, http.StatusNotFound, &mdmResp)
|
||||
var bmResp getAppleBMResponse
|
||||
s.DoJSON("GET", "/api/latest/fleet/mdm/apple_bm", nil, http.StatusNotFound, &bmResp)
|
||||
var rawResp json.RawMessage
|
||||
s.DoJSON("GET", "/api/latest/fleet/mdm/apple", nil, http.StatusNotFound, &rawResp)
|
||||
s.DoJSON("GET", "/api/latest/fleet/mdm/apple_bm", nil, http.StatusNotFound, &rawResp)
|
||||
}
|
||||
|
||||
func (s *integrationEnterpriseTestSuite) TestGlobalPolicyCreateReadPatch() {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import (
|
|||
"math/big"
|
||||
mathrand "math/rand"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
|
@ -36,6 +37,7 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
"go.mozilla.org/pkcs7"
|
||||
"go.uber.org/atomic"
|
||||
)
|
||||
|
||||
func TestIntegrationsMDM(t *testing.T) {
|
||||
|
|
@ -47,7 +49,8 @@ func TestIntegrationsMDM(t *testing.T) {
|
|||
type integrationMDMTestSuite struct {
|
||||
withServer
|
||||
suite.Suite
|
||||
fleetCfg config.FleetConfig
|
||||
fleetCfg config.FleetConfig
|
||||
fleetDMFailCSR atomic.Bool
|
||||
}
|
||||
|
||||
func (s *integrationMDMTestSuite) SetupSuite() {
|
||||
|
|
@ -84,6 +87,27 @@ func (s *integrationMDMTestSuite) SetupSuite() {
|
|||
s.token = s.getTestAdminToken()
|
||||
s.cachedAdminToken = s.token
|
||||
s.fleetCfg = fleetCfg
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if s.fleetDMFailCSR.Swap(false) {
|
||||
// fail this call
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = w.Write([]byte("bad request"))
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
}))
|
||||
s.T().Setenv("TEST_FLEETDM_API_URL", srv.URL)
|
||||
s.T().Cleanup(srv.Close)
|
||||
}
|
||||
|
||||
func (s *integrationMDMTestSuite) FailNextCSRRequest() {
|
||||
s.fleetDMFailCSR.Store(true)
|
||||
}
|
||||
|
||||
func (s *integrationMDMTestSuite) SucceedNextCSRRequest() {
|
||||
s.fleetDMFailCSR.Store(false)
|
||||
}
|
||||
|
||||
func (s *integrationMDMTestSuite) TearDownTest() {
|
||||
|
|
@ -255,6 +279,43 @@ func (s *integrationMDMTestSuite) TestDeviceMultipleAuthMessages() {
|
|||
require.Len(s.T(), listHostsRes.Hosts, 1)
|
||||
}
|
||||
|
||||
func (s *integrationMDMTestSuite) TestAppleMDMCSRRequest() {
|
||||
t := s.T()
|
||||
|
||||
var errResp validationErrResp
|
||||
// missing arguments
|
||||
s.DoJSON("POST", "/api/latest/fleet/mdm/apple/request_csr", requestMDMAppleCSRRequest{}, http.StatusUnprocessableEntity, &errResp)
|
||||
require.Len(t, errResp.Errors, 1)
|
||||
require.Equal(t, errResp.Errors[0].Name, "email_address")
|
||||
|
||||
// invalid email address
|
||||
errResp = validationErrResp{}
|
||||
s.DoJSON("POST", "/api/latest/fleet/mdm/apple/request_csr", requestMDMAppleCSRRequest{EmailAddress: "abc", Organization: "def"}, http.StatusUnprocessableEntity, &errResp)
|
||||
require.Len(t, errResp.Errors, 1)
|
||||
require.Equal(t, errResp.Errors[0].Name, "email_address")
|
||||
|
||||
// missing organization
|
||||
errResp = validationErrResp{}
|
||||
s.DoJSON("POST", "/api/latest/fleet/mdm/apple/request_csr", requestMDMAppleCSRRequest{EmailAddress: "a@b.c", Organization: ""}, http.StatusUnprocessableEntity, &errResp)
|
||||
require.Len(t, errResp.Errors, 1)
|
||||
require.Equal(t, errResp.Errors[0].Name, "organization")
|
||||
|
||||
// fleetdm CSR request failed
|
||||
s.FailNextCSRRequest()
|
||||
errResp = validationErrResp{}
|
||||
s.DoJSON("POST", "/api/latest/fleet/mdm/apple/request_csr", requestMDMAppleCSRRequest{EmailAddress: "a@b.c", Organization: "test"}, http.StatusBadGateway, &errResp)
|
||||
require.Len(t, errResp.Errors, 1)
|
||||
require.Contains(t, errResp.Errors[0].Reason, "FleetDM CSR request failed")
|
||||
|
||||
var reqCSRResp requestMDMAppleCSRResponse
|
||||
// fleetdm CSR request succeeds
|
||||
s.SucceedNextCSRRequest()
|
||||
s.DoJSON("POST", "/api/latest/fleet/mdm/apple/request_csr", requestMDMAppleCSRRequest{EmailAddress: "a@b.c", Organization: "test"}, http.StatusOK, &reqCSRResp)
|
||||
require.Contains(t, string(reqCSRResp.APNsKey), "-----BEGIN RSA PRIVATE KEY-----\n")
|
||||
require.Contains(t, string(reqCSRResp.SCEPCert), "-----BEGIN CERTIFICATE-----\n")
|
||||
require.Contains(t, string(reqCSRResp.SCEPKey), "-----BEGIN RSA PRIVATE KEY-----\n")
|
||||
}
|
||||
|
||||
type device struct {
|
||||
uuid string
|
||||
serial string
|
||||
|
|
|
|||
|
|
@ -2,10 +2,21 @@ package service
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
|
||||
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
||||
"github.com/fleetdm/fleet/v4/server/fleet"
|
||||
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
|
||||
)
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// GET /mdm/apple
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
type getAppleMDMResponse struct {
|
||||
*fleet.AppleMDM
|
||||
Err error `json:"error,omitempty"`
|
||||
|
|
@ -49,6 +60,10 @@ func (svc *Service) GetAppleMDM(ctx context.Context) (*fleet.AppleMDM, error) {
|
|||
return appleMDM, nil
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// GET /mdm/apple_bm
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
type getAppleBMResponse struct {
|
||||
*fleet.AppleBM
|
||||
Err error `json:"error,omitempty"`
|
||||
|
|
@ -72,3 +87,79 @@ func (svc *Service) GetAppleBM(ctx context.Context) (*fleet.AppleBM, error) {
|
|||
|
||||
return nil, fleet.ErrMissingLicense
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// GET /mdm/apple/request_csr
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
type requestMDMAppleCSRRequest struct {
|
||||
EmailAddress string `json:"email_address"`
|
||||
Organization string `json:"organization"`
|
||||
}
|
||||
|
||||
type requestMDMAppleCSRResponse struct {
|
||||
*fleet.AppleCSR
|
||||
Err error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (r requestMDMAppleCSRResponse) error() error { return r.Err }
|
||||
|
||||
func requestMDMAppleCSREndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
||||
req := request.(*requestMDMAppleCSRRequest)
|
||||
|
||||
csr, err := svc.RequestMDMAppleCSR(ctx, req.EmailAddress, req.Organization)
|
||||
if err != nil {
|
||||
return requestMDMAppleCSRResponse{Err: err}, nil
|
||||
}
|
||||
return requestMDMAppleCSRResponse{
|
||||
AppleCSR: csr,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) RequestMDMAppleCSR(ctx context.Context, email, org string) (*fleet.AppleCSR, error) {
|
||||
if err := svc.authz.Authorize(ctx, &fleet.AppleCSR{}, fleet.ActionWrite); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := fleet.ValidateEmail(email); err != nil {
|
||||
if strings.TrimSpace(email) == "" {
|
||||
return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("email_address", "missing email address"))
|
||||
}
|
||||
return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("email_address", fmt.Sprintf("invalid email address: %v", err)))
|
||||
}
|
||||
if strings.TrimSpace(org) == "" {
|
||||
return nil, ctxerr.Wrap(ctx, fleet.NewInvalidArgumentError("organization", "missing organization"))
|
||||
}
|
||||
|
||||
// create the raw SCEP CA cert and key (creating before the CSR signing
|
||||
// request so that nothing can fail after the request is made, except for the
|
||||
// network during the response of course)
|
||||
scepCACert, scepCAKey, err := apple_mdm.NewSCEPCACertKey()
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "generate SCEP CA cert and key")
|
||||
}
|
||||
|
||||
// create the APNs CSR
|
||||
apnsCSR, apnsKey, err := apple_mdm.GenerateAPNSCSRKey(email, org)
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "generate APNs CSR")
|
||||
}
|
||||
|
||||
// request the signed APNs CSR from fleetdm.com
|
||||
client := fleethttp.NewClient(fleethttp.WithTimeout(10 * time.Second))
|
||||
if err := apple_mdm.GetSignedAPNSCSR(client, apnsCSR); err != nil {
|
||||
err = fleet.NewUserMessageError(fmt.Errorf("FleetDM CSR request failed: %w", err), http.StatusBadGateway)
|
||||
return nil, ctxerr.Wrap(ctx, err)
|
||||
}
|
||||
|
||||
// PEM-encode the cert and keys
|
||||
scepCACertPEM := apple_mdm.EncodeCertPEM(scepCACert)
|
||||
scepCAKeyPEM := apple_mdm.EncodePrivateKeyPEM(scepCAKey)
|
||||
apnsKeyPEM := apple_mdm.EncodePrivateKeyPEM(apnsKey)
|
||||
|
||||
return &fleet.AppleCSR{
|
||||
APNsKey: apnsKeyPEM,
|
||||
SCEPCert: scepCACertPEM,
|
||||
SCEPKey: scepCAKeyPEM,
|
||||
}, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,6 +59,13 @@ func TestMDMAppleAuthorization(t *testing.T) {
|
|||
ctx := test.UserContext(ctx, user)
|
||||
_, err := svc.GetAppleMDM(ctx)
|
||||
checkAuthErr(t, shouldFailWithAuth, err)
|
||||
_, err = svc.GetAppleBM(ctx)
|
||||
checkAuthErr(t, shouldFailWithAuth, err)
|
||||
|
||||
// deliberately send invalid args so it doesn't actually generate a CSR
|
||||
_, err = svc.RequestMDMAppleCSR(ctx, "not-an-email", "")
|
||||
require.Error(t, err) // it *will* always fail, but not necessarily due to authorization
|
||||
checkAuthErr(t, shouldFailWithAuth, err)
|
||||
}
|
||||
|
||||
// Only global admins can access the endpoints.
|
||||
|
|
|
|||
3
website/api/controllers/deliver-apple-csr.js
vendored
3
website/api/controllers/deliver-apple-csr.js
vendored
|
|
@ -25,7 +25,8 @@ module.exports = {
|
|||
},
|
||||
|
||||
invalidEmailDomain: {
|
||||
description: 'This email address is on a denylist of domains and was not delivered.'
|
||||
description: 'This email address is on a denylist of domains and was not delivered.',
|
||||
responseType: 'badRequest'
|
||||
},
|
||||
|
||||
badRequest: {
|
||||
|
|
|
|||
Loading…
Reference in a new issue