mirror of
https://github.com/micromdm/micromdm/
synced 2026-08-05 08:55:51 +08:00
Tidy code for Go 1.20 and update Go version for Docker and CI --------- Co-authored-by: Kory Prince <korylprince@gmail.com>
381 lines
9.7 KiB
Go
381 lines
9.7 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto"
|
|
"crypto/x509"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"flag"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/go-kit/kit/log"
|
|
"github.com/google/uuid"
|
|
"github.com/pkg/errors"
|
|
"golang.org/x/crypto/pkcs12"
|
|
|
|
"github.com/micromdm/micromdm/pkg/crypto/profileutil"
|
|
"github.com/micromdm/micromdm/platform/blueprint"
|
|
"github.com/micromdm/micromdm/platform/profile"
|
|
)
|
|
|
|
type applyCommand struct {
|
|
config *ServerConfig
|
|
*remoteServices
|
|
}
|
|
|
|
func (cmd *applyCommand) setup() error {
|
|
cfg, err := LoadServerConfig()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cmd.config = cfg
|
|
logger := log.NewLogfmtLogger(os.Stderr)
|
|
remote, err := setupClient(logger)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cmd.remoteServices = remote
|
|
return nil
|
|
}
|
|
|
|
func (cmd *applyCommand) Run(args []string) error {
|
|
if len(args) < 1 {
|
|
cmd.Usage()
|
|
os.Exit(1)
|
|
}
|
|
if err := cmd.setup(); err != nil {
|
|
return err
|
|
}
|
|
var run func([]string) error
|
|
switch strings.ToLower(args[0]) {
|
|
case "blueprints":
|
|
run = cmd.applyBlueprint
|
|
case "dep-tokens":
|
|
run = cmd.applyDEPTokens
|
|
case "dep-profiles":
|
|
run = cmd.applyDEPProfile
|
|
case "profiles":
|
|
run = cmd.applyProfile
|
|
case "app":
|
|
run = cmd.applyApp
|
|
case "block":
|
|
run = cmd.applyBlock
|
|
case "users":
|
|
run = cmd.applyUser
|
|
case "dep-autoassigner":
|
|
run = cmd.applyDEPAutoAssigner
|
|
default:
|
|
cmd.Usage()
|
|
os.Exit(1)
|
|
}
|
|
return run(args[1:])
|
|
}
|
|
|
|
func (cmd *applyCommand) Usage() error {
|
|
const applyUsage = `
|
|
Apply a resource.
|
|
|
|
Valid resource types:
|
|
|
|
* blueprints
|
|
* profiles
|
|
* users
|
|
* dep-tokens
|
|
* dep-profiles
|
|
* dep-autoassigner
|
|
* app
|
|
* block
|
|
|
|
Examples:
|
|
# Apply a Blueprint.
|
|
mdmctl apply blueprints -f /path/to/blueprint.json
|
|
|
|
# Apply a DEP Profile.
|
|
mdmctl apply dep-profiles -f /path/to/dep-profile.json
|
|
|
|
|
|
`
|
|
fmt.Print(applyUsage)
|
|
return nil
|
|
}
|
|
|
|
func (cmd *applyCommand) applyBlueprint(args []string) error {
|
|
flagset := flag.NewFlagSet("blueprints", flag.ExitOnError)
|
|
var (
|
|
flBlueprintPath = flagset.String("f", "", "filename of blueprint JSON to apply")
|
|
flTemplate = flagset.Bool("template", false, "print a new blueprint template")
|
|
)
|
|
flagset.Usage = usageFor(flagset, "mdmctl apply blueprints [flags]")
|
|
if err := flagset.Parse(args); err != nil {
|
|
return err
|
|
}
|
|
|
|
if *flTemplate {
|
|
newBlueprint := &blueprint.Blueprint{
|
|
Name: "exampleName",
|
|
UUID: uuid.New().String(),
|
|
ApplicationURLs: []string{cmd.config.ServerURL + "repo/exampleAppManifest.plist"},
|
|
ProfileIdentifiers: []string{"com.example.my.profile"},
|
|
UserUUID: []string{"your-admin-account-uuid"},
|
|
ApplyAt: []string{"Enroll"},
|
|
}
|
|
|
|
enc := json.NewEncoder(os.Stdout)
|
|
enc.SetIndent("", " ")
|
|
if err := enc.Encode(newBlueprint); err != nil {
|
|
return errors.Wrap(err, "encode blueprint template")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if *flBlueprintPath == "" {
|
|
flagset.Usage()
|
|
return errors.New("bad input: must provide -f or -template flag")
|
|
}
|
|
|
|
if *flBlueprintPath != "" {
|
|
jsonBytes, err := readBytesFromPath(*flBlueprintPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var blpt blueprint.Blueprint
|
|
err = json.Unmarshal(jsonBytes, &blpt)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// validate the blueprint account rules
|
|
if len(blpt.UserUUID) == 0 && (blpt.SkipPrimarySetupAccountCreation || blpt.SetPrimarySetupAccountAsRegularUser) {
|
|
return errors.New("SkipPrimarySetupAccountCreation and SetPrimarySetupAccountAsRegularUser can only be true if there is an account in the UserUUID array.")
|
|
}
|
|
|
|
ctx := context.Background()
|
|
err = cmd.blueprintsvc.ApplyBlueprint(ctx, &blpt)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Println("applied blueprint", *flBlueprintPath)
|
|
return nil
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (cmd *applyCommand) applyDEPTokens(args []string) error {
|
|
flagset := flag.NewFlagSet("dep-tokens", flag.ExitOnError)
|
|
var (
|
|
flTokenPath = flagset.String(
|
|
"import",
|
|
filepath.Join(defaultmdmctlFilesPath, "DEPOAuthToken.json"),
|
|
"Filename of p7m encrypted token file (downloaded from DEP portal)")
|
|
)
|
|
|
|
flagset.Usage = usageFor(flagset, "mdmctl apply dep-tokens [flags]")
|
|
if err := flagset.Parse(args); err != nil {
|
|
return err
|
|
}
|
|
|
|
if *flTokenPath == "" {
|
|
return errors.New("must provide -import-token parameter")
|
|
}
|
|
if _, err := os.Stat(*flTokenPath); os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
p7mBytes, err := ioutil.ReadFile(*flTokenPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ctx := context.Background()
|
|
err = cmd.configsvc.ApplyDEPToken(ctx, p7mBytes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Println("imported DEP token")
|
|
return nil
|
|
}
|
|
|
|
func (cmd *applyCommand) applyBlock(args []string) error {
|
|
flagset := flag.NewFlagSet("block", flag.ExitOnError)
|
|
var (
|
|
flUDID = flagset.String("udid", "", "UDID of a device to block.")
|
|
)
|
|
flagset.Usage = usageFor(flagset, "mdmctl apply block [flags]")
|
|
if err := flagset.Parse(args); err != nil {
|
|
return err
|
|
}
|
|
if *flUDID == "" {
|
|
flagset.Usage()
|
|
return errors.New("bad input: must provide a device UDID to block.")
|
|
}
|
|
if err := cmd.blocksvc.BlockDevice(context.Background(), *flUDID); err != nil {
|
|
return err
|
|
}
|
|
|
|
// trigger a push
|
|
u, err := url.Parse(cmd.config.ServerURL)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
return nil
|
|
}
|
|
u.Path = "/push/" + url.QueryEscape(*flUDID)
|
|
req, err := http.NewRequest("GET", u.String(), nil)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
return nil
|
|
}
|
|
req.SetBasicAuth("micromdm", cmd.config.APIToken)
|
|
skipVerifyHTTPClient(cmd.config.SkipVerify).Do(req)
|
|
return nil
|
|
}
|
|
|
|
func loadSigningKey(keyPass, keyPath, certPath string) (crypto.PrivateKey, *x509.Certificate, error) {
|
|
certData, err := ioutil.ReadFile(certPath)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
isP12 := filepath.Ext(certPath) == ".p12"
|
|
if isP12 {
|
|
pkey, cert, err := pkcs12.Decode(certData, keyPass)
|
|
return pkey, cert, errors.Wrap(err, "decode p12 contents")
|
|
}
|
|
|
|
keyData, err := ioutil.ReadFile(keyPath)
|
|
if err != nil {
|
|
return nil, nil, errors.Wrap(err, "read key from file")
|
|
}
|
|
|
|
keyDataBlock, _ := pem.Decode(keyData)
|
|
if keyDataBlock == nil {
|
|
return nil, nil, errors.Errorf("invalid PEM data for private key %s", keyPath)
|
|
}
|
|
var pemKeyData []byte
|
|
if x509.IsEncryptedPEMBlock(keyDataBlock) {
|
|
b, err := x509.DecryptPEMBlock(keyDataBlock, []byte(keyPass))
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("decrypting DES private key %s", err)
|
|
}
|
|
pemKeyData = b
|
|
} else {
|
|
pemKeyData = keyDataBlock.Bytes
|
|
}
|
|
|
|
priv, err := x509.ParsePKCS1PrivateKey(pemKeyData)
|
|
if err != nil {
|
|
return nil, nil, errors.Wrap(err, "parse private key")
|
|
}
|
|
|
|
pub, _ := pem.Decode(certData)
|
|
if pub == nil {
|
|
return nil, nil, errors.Errorf("invalid PEM data for certificate %q", certPath)
|
|
}
|
|
|
|
cert, err := x509.ParseCertificate(pub.Bytes)
|
|
if err != nil {
|
|
return nil, nil, errors.Wrap(err, "parse PEM certificate data")
|
|
}
|
|
|
|
return priv, cert, nil
|
|
}
|
|
|
|
func (cmd *applyCommand) applyProfile(args []string) error {
|
|
flagset := flag.NewFlagSet("profiles", flag.ExitOnError)
|
|
var (
|
|
flProfilePath = flagset.String("f", "", "Path to profile payload.")
|
|
flSign = flagset.Bool("sign", false, "Sign the profile. Requires key and certificate path.")
|
|
flOut = flagset.String("out", "", "Output path for signed profile(optional).")
|
|
flKeyPass = flagset.String("password", "", "Password to encrypt/read the signing key(optional) or p12 file.")
|
|
flKeyPath = flagset.String("private-key", "", "Path to the signing private key. Don't use with p12 file.")
|
|
flCertPath = flagset.String("cert", "", "Path to the signing certificate or p12 file.")
|
|
)
|
|
flagset.Usage = func() {
|
|
fmt.Fprintf(os.Stderr, "%s\n",
|
|
`Upload profiles to the server.
|
|
|
|
Uploaded profiles can also be specified in a blueprint, which will be applied on device enrollment.
|
|
This command can also be used to replace the enrollment profile.
|
|
Profiles can be signed before upload.
|
|
|
|
Examples
|
|
|
|
# Upload a mobileconfig
|
|
mdmctl apply profiles -f /path/to/profile.mobileconfig
|
|
|
|
# Sign and upload
|
|
mdmctl apply profiles -f /path/to/profile.mobileconfig -private-key key.pem -cert certificate.pem -password secret -sign
|
|
|
|
# Sign and save to local directory instead of uploading
|
|
# Use "-out -" print the output to stdout instead of a file.
|
|
mdmctl apply profiles -f /path/to/profile.mobileconfig -private-key key.pem -cert certificate.pem -password secret -sign -out signed.mobileconfig
|
|
|
|
`)
|
|
usageFor(flagset, "mdmctl apply profiles [flags]")()
|
|
}
|
|
if err := flagset.Parse(args); err != nil {
|
|
return err
|
|
}
|
|
if *flProfilePath == "" {
|
|
flagset.Usage()
|
|
return errors.New("bad input: must provide -f parameter. use - for stdin")
|
|
}
|
|
profileBytes, err := readBytesFromPath(*flProfilePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if *flSign {
|
|
priv, pub, err := loadSigningKey(*flKeyPass, *flKeyPath, *flCertPath)
|
|
if err != nil {
|
|
return errors.Wrap(err, "loading signing certificate and private key")
|
|
}
|
|
signed, err := profileutil.Sign(priv, pub, profileBytes)
|
|
if err != nil {
|
|
return errors.Wrap(err, "signing profile with the specified key")
|
|
}
|
|
|
|
if *flOut == "-" { // print to stdout and return
|
|
_, err = os.Stdout.Write(signed)
|
|
return err
|
|
} else if *flOut != "" { // write to file and return
|
|
return ioutil.WriteFile(*flOut, signed, 0644)
|
|
}
|
|
|
|
profileBytes = signed
|
|
}
|
|
|
|
// TODO: to consider just uploading the Mobileconfig data (without a
|
|
// Profile struct and doing init server side)
|
|
var p profile.Profile
|
|
p.Mobileconfig = profileBytes
|
|
p.Identifier, err = p.Mobileconfig.GetPayloadIdentifier()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ctx := context.Background()
|
|
err = cmd.profilesvc.ApplyProfile(ctx, &p)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
fmt.Printf("applied profile id %s from %s\n", p.Identifier, *flProfilePath)
|
|
return nil
|
|
}
|
|
|
|
func readBytesFromPath(path string) ([]byte, error) {
|
|
if path == "-" {
|
|
return ioutil.ReadAll(os.Stdin)
|
|
}
|
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
|
return nil, err
|
|
}
|
|
return ioutil.ReadFile(path)
|
|
}
|