2022-12-05 16:35:45 +00:00
|
|
|
package service
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2023-01-25 19:44:29 +00:00
|
|
|
"fmt"
|
|
|
|
"net/http"
|
2023-03-13 13:33:32 +00:00
|
|
|
"strconv"
|
2023-01-25 19:44:29 +00:00
|
|
|
"strings"
|
|
|
|
"time"
|
2022-12-05 16:35:45 +00:00
|
|
|
|
2023-01-25 19:44:29 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
|
|
|
|
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
2023-03-13 13:33:32 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/server/contexts/logging"
|
2022-12-05 16:35:45 +00:00
|
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
2023-01-25 19:44:29 +00:00
|
|
|
apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
|
2022-12-05 16:35:45 +00:00
|
|
|
)
|
|
|
|
|
2023-01-25 19:44:29 +00:00
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// GET /mdm/apple
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
2022-12-05 16:35:45 +00:00
|
|
|
type getAppleMDMResponse struct {
|
|
|
|
*fleet.AppleMDM
|
|
|
|
Err error `json:"error,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r getAppleMDMResponse) error() error { return r.Err }
|
|
|
|
|
2022-12-27 14:26:59 +00:00
|
|
|
func getAppleMDMEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
2022-12-05 16:35:45 +00:00
|
|
|
appleMDM, err := svc.GetAppleMDM(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return getAppleMDMResponse{Err: err}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return getAppleMDMResponse{AppleMDM: appleMDM}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (svc *Service) GetAppleMDM(ctx context.Context) (*fleet.AppleMDM, error) {
|
|
|
|
if err := svc.authz.Authorize(ctx, &fleet.AppleMDM{}, fleet.ActionRead); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// if there is no apple mdm config, fail with a 404
|
|
|
|
if !svc.config.MDM.IsAppleAPNsSet() {
|
Add UUID to Fleet errors and clean up error msgs (#10411)
#8129
Apart from fixing the issue in #8129, this change also introduces UUIDs
to Fleet errors. To be able to match a returned error from the API to a
error in the Fleet logs. See
https://fleetdm.slack.com/archives/C019WG4GH0A/p1677780622769939 for
more context.
Samples with the changes in this PR:
```
curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d ''
{
"message": "Bad request",
"errors": [
{
"name": "base",
"reason": "Expected JSON Body"
}
],
"uuid": "a01f6e10-354c-4ff0-b96e-1f64adb500b0"
}
```
```
curl -k -H "Authorization: Bearer $TEST_TOKEN" -H 'Content-Type:application/json' "https://localhost:8080/api/v1/fleet/sso" -d 'asd'
{
"message": "Bad request",
"errors": [
{
"name": "base",
"reason": "json decoder error"
}
],
"uuid": "5f716a64-7550-464b-a1dd-e6a505a9f89d"
}
```
```
curl -k -X GET -H "Authorization: Bearer badtoken" "https://localhost:8080/api/latest/fleet/teams"
{
"message": "Authentication required",
"errors": [
{
"name": "base",
"reason": "Authentication required"
}
],
"uuid": "efe45bc0-f956-4bf9-ba4f-aa9020a9aaaf"
}
```
```
curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}'
{
"message": "Authorization header required",
"errors": [
{
"name": "base",
"reason": "Authorization header required"
}
],
"uuid": "57f78cd0-4559-464f-9df7-36c9ef7c89b3"
}
```
```
curl -k -X PATCH -H "Authorization: Bearer $TEST_TOKEN" "https://localhost:8080/api/latest/fleet/users/14" -d '{"name": "Manuel2", "password": "what", "new_password": "p4ssw0rd.12345"}'
{
"message": "Permission Denied",
"uuid": "7f0220ad-6de7-4faf-8b6c-8d7ff9d2ca06"
}
```
- [X] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- [X] Documented any API changes (docs/Using-Fleet/REST-API.md or
docs/Contributing/API-for-contributors.md)
- ~[ ] Documented any permissions changes~
- ~[ ] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)~
- ~[ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for
new osquery data ingestion features.~
- [X] Added/updated tests
- [X] Manual QA for all new/changed functionality
- For Orbit and Fleet Desktop changes:
- [X] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.
- ~[ ] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).~
2023-03-13 16:44:06 +00:00
|
|
|
return nil, newNotFoundError()
|
2022-12-05 16:35:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
apns, _, _, err := svc.config.MDM.AppleAPNs()
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
appleMDM := &fleet.AppleMDM{
|
|
|
|
CommonName: apns.Leaf.Subject.CommonName,
|
|
|
|
Issuer: apns.Leaf.Issuer.CommonName,
|
|
|
|
RenewDate: apns.Leaf.NotAfter,
|
|
|
|
}
|
|
|
|
if apns.Leaf.SerialNumber != nil {
|
|
|
|
appleMDM.SerialNumber = apns.Leaf.SerialNumber.String()
|
|
|
|
}
|
|
|
|
|
|
|
|
return appleMDM, nil
|
|
|
|
}
|
2022-12-12 20:45:53 +00:00
|
|
|
|
2023-01-25 19:44:29 +00:00
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// GET /mdm/apple_bm
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
2022-12-12 20:45:53 +00:00
|
|
|
type getAppleBMResponse struct {
|
|
|
|
*fleet.AppleBM
|
|
|
|
Err error `json:"error,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r getAppleBMResponse) error() error { return r.Err }
|
|
|
|
|
2022-12-27 14:26:59 +00:00
|
|
|
func getAppleBMEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
2022-12-12 20:45:53 +00:00
|
|
|
appleBM, err := svc.GetAppleBM(ctx)
|
|
|
|
if err != nil {
|
|
|
|
return getAppleBMResponse{Err: err}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return getAppleBMResponse{AppleBM: appleBM}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (svc *Service) GetAppleBM(ctx context.Context) (*fleet.AppleBM, error) {
|
|
|
|
// skipauth: No authorization check needed due to implementation returning
|
|
|
|
// only license error.
|
|
|
|
svc.authz.SkipAuthorization(ctx)
|
|
|
|
|
|
|
|
return nil, fleet.ErrMissingLicense
|
|
|
|
}
|
2023-01-25 19:44:29 +00:00
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// 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 {
|
2023-02-01 15:50:22 +00:00
|
|
|
if ferr, ok := err.(apple_mdm.FleetWebsiteError); ok {
|
|
|
|
status := http.StatusBadGateway
|
|
|
|
if ferr.Status >= 400 && ferr.Status <= 499 {
|
|
|
|
// TODO: fleetdm.com returns a genereric "Bad
|
|
|
|
// Request" message, we should coordinate and
|
|
|
|
// stablish a response schema from which we can get
|
|
|
|
// the invalid field and use
|
|
|
|
// fleet.NewInvalidArgumentError instead
|
|
|
|
//
|
|
|
|
// For now, since we have already validated
|
|
|
|
// everything else, we assume that a 4xx
|
|
|
|
// response is an email with an invalid domain
|
|
|
|
return nil, ctxerr.Wrap(
|
|
|
|
ctx,
|
|
|
|
fleet.NewInvalidArgumentError(
|
|
|
|
"email_address",
|
|
|
|
fmt.Sprintf("this email address is not valid: %v", err),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
return nil, ctxerr.Wrap(
|
|
|
|
ctx,
|
|
|
|
fleet.NewUserMessageError(
|
|
|
|
fmt.Errorf("FleetDM CSR request failed: %w", err),
|
|
|
|
status,
|
|
|
|
),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil, ctxerr.Wrap(ctx, err, "get signed CSR")
|
2023-01-25 19:44:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
2023-03-13 13:33:32 +00:00
|
|
|
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// POST /mdm/apple/dep_login
|
|
|
|
////////////////////////////////////////////////////////////////////////////////
|
|
|
|
|
|
|
|
type mdmAppleDEPLoginRequest struct {
|
|
|
|
Username string `json:"username"`
|
|
|
|
Password string `json:"password"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type mdmAppleDEPLoginResponse struct {
|
|
|
|
// profile is used in hijackRender for the response.
|
|
|
|
profile []byte
|
|
|
|
Err error `json:"error,omitempty"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r mdmAppleDEPLoginResponse) hijackRender(ctx context.Context, w http.ResponseWriter) {
|
|
|
|
w.Header().Set("Content-Length", strconv.FormatInt(int64(len(r.profile)), 10))
|
|
|
|
w.Header().Set("Content-Type", "application/x-apple-aspen-config")
|
|
|
|
|
|
|
|
// OK to just log the error here as writing anything on
|
|
|
|
// `http.ResponseWriter` sets the status code to 200 (and it can't be
|
|
|
|
// changed.) Clients should rely on matching content-length with the
|
|
|
|
// header provided.
|
|
|
|
if n, err := w.Write(r.profile); err != nil {
|
|
|
|
logging.WithExtras(ctx, "err", err, "written", n)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r mdmAppleDEPLoginResponse) error() error { return r.Err }
|
|
|
|
|
|
|
|
func mdmAppleDEPLoginEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
|
|
|
|
req := request.(*mdmAppleDEPLoginRequest)
|
|
|
|
enrollProfile, err := svc.MDMAppleOktaLogin(ctx, req.Username, req.Password)
|
|
|
|
if err != nil {
|
|
|
|
return mdmAppleDEPLoginResponse{Err: err}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return mdmAppleDEPLoginResponse{profile: enrollProfile}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (svc *Service) MDMAppleOktaLogin(ctx context.Context, username, password string) ([]byte, error) {
|
|
|
|
// skipauth: No authorization check needed due to implementation returning
|
|
|
|
// only license error.
|
|
|
|
svc.authz.SkipAuthorization(ctx)
|
|
|
|
|
|
|
|
return nil, fleet.ErrMissingLicense
|
|
|
|
}
|