Add users (#258)

For #248 

Added a User DB similar to the Device DB. 
The user specification be added with the API/mdmctl commands and then referenced with a blueprint. 
If added to the blueprint, the user will be installed as an administrator during the AccountConfiguration step.
This commit is contained in:
Victor Vrantchan
2017-10-24 20:34:41 -04:00
committed by GitHub
parent ad28f03a95
commit 43d026c970
24 changed files with 830 additions and 22 deletions

View File

@@ -21,6 +21,7 @@ type Blueprint struct {
Name string `json:"name"`
ApplicationURLs []string `json:"install_application_manifest_urls"`
ProfileIdentifiers []string `json:"profile_ids"`
UserUUID []string `json:"user_uuids"`
ApplyAt []string `json:"apply_at"`
}
@@ -37,6 +38,7 @@ func MarshalBlueprint(bp *Blueprint) ([]byte, error) {
Name: bp.Name,
ManifestUrls: bp.ApplicationURLs,
ProfileIds: bp.ProfileIdentifiers,
UserUuid: bp.UserUUID,
ApplyAt: bp.ApplyAt,
}
return proto.Marshal(&protobp)
@@ -52,5 +54,6 @@ func UnmarshalBlueprint(data []byte, bp *Blueprint) error {
bp.ApplicationURLs = pb.GetManifestUrls()
bp.ProfileIdentifiers = pb.GetProfileIds()
bp.ApplyAt = pb.GetApplyAt()
bp.UserUUID = pb.GetUserUuid()
return nil
}

View File

@@ -8,6 +8,7 @@ import (
"github.com/pkg/errors"
"github.com/micromdm/micromdm/profile"
"github.com/micromdm/micromdm/user"
)
const (
@@ -18,9 +19,14 @@ const (
type DB struct {
*bolt.DB
profDB *profile.DB
userDB *user.DB
}
func NewDB(db *bolt.DB, pDB *profile.DB) (*DB, error) {
func NewDB(
db *bolt.DB,
profileDB *profile.DB,
userDB *user.DB,
) (*DB, error) {
err := db.Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte(blueprintIndexBucket))
if err != nil {
@@ -34,7 +40,8 @@ func NewDB(db *bolt.DB, pDB *profile.DB) (*DB, error) {
}
datastore := &DB{
DB: db,
profDB: pDB,
profDB: profileDB,
userDB: userDB,
}
return datastore, nil
}

View File

@@ -1,6 +1,5 @@
// Code generated by protoc-gen-go.
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: blueprint.proto
// DO NOT EDIT!
/*
Package blueprintproto is a generated protocol buffer package.
@@ -34,6 +33,7 @@ type Blueprint struct {
ManifestUrls []string `protobuf:"bytes,3,rep,name=manifest_urls,json=manifestUrls" json:"manifest_urls,omitempty"`
ProfileIds []string `protobuf:"bytes,5,rep,name=profile_ids,json=profileIds" json:"profile_ids,omitempty"`
ApplyAt []string `protobuf:"bytes,6,rep,name=apply_at,json=applyAt" json:"apply_at,omitempty"`
UserUuid []string `protobuf:"bytes,7,rep,name=user_uuid,json=userUuid" json:"user_uuid,omitempty"`
}
func (m *Blueprint) Reset() { *m = Blueprint{} }
@@ -76,6 +76,13 @@ func (m *Blueprint) GetApplyAt() []string {
return nil
}
func (m *Blueprint) GetUserUuid() []string {
if m != nil {
return m.UserUuid
}
return nil
}
func init() {
proto.RegisterType((*Blueprint)(nil), "blueprintproto.Blueprint")
}
@@ -83,17 +90,18 @@ func init() {
func init() { proto.RegisterFile("blueprint.proto", fileDescriptor0) }
var fileDescriptor0 = []byte{
// 185 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x3c, 0xce, 0x3d, 0x8e, 0xc2, 0x30,
0x10, 0x05, 0x60, 0x65, 0xf3, 0xb3, 0xc9, 0xec, 0x06, 0x90, 0x2b, 0x53, 0x11, 0x41, 0x93, 0x8a,
0x86, 0x13, 0x40, 0x07, 0x65, 0x24, 0xea, 0xc8, 0x21, 0x0e, 0x1a, 0xc9, 0xb1, 0x2d, 0xff, 0x14,
0x1c, 0x89, 0x5b, 0xa2, 0x18, 0x42, 0xf7, 0xde, 0x37, 0x53, 0x3c, 0x58, 0x76, 0xc2, 0x73, 0x6d,
0x50, 0xba, 0xbd, 0x36, 0xca, 0x29, 0xb2, 0xf8, 0x42, 0xe8, 0xdb, 0x67, 0x04, 0xc5, 0x69, 0x26,
0x42, 0x20, 0xf1, 0x1e, 0x7b, 0x1a, 0x55, 0x51, 0x5d, 0x34, 0x21, 0x4f, 0x26, 0xd9, 0xc8, 0xe9,
0xcf, 0xdb, 0xa6, 0x4c, 0x76, 0x50, 0x8e, 0x4c, 0xe2, 0xc0, 0xad, 0x6b, 0xbd, 0x11, 0x96, 0xc6,
0x55, 0x5c, 0x17, 0xcd, 0xff, 0x8c, 0x57, 0x23, 0x2c, 0xd9, 0xc0, 0x9f, 0x36, 0x6a, 0x40, 0xc1,
0x5b, 0xec, 0x2d, 0x4d, 0xc3, 0x0b, 0x7c, 0xe8, 0xdc, 0x5b, 0xb2, 0x86, 0x9c, 0x69, 0x2d, 0x1e,
0x2d, 0x73, 0x34, 0x0b, 0xd7, 0xdf, 0xd0, 0x8f, 0xee, 0x92, 0xe4, 0xc9, 0x2a, 0x6d, 0xca, 0x51,
0x75, 0x28, 0xf8, 0x4d, 0xc9, 0x01, 0xef, 0xb6, 0xcb, 0xc2, 0xe4, 0xc3, 0x2b, 0x00, 0x00, 0xff,
0xff, 0x1d, 0x05, 0x14, 0x9a, 0xd5, 0x00, 0x00, 0x00,
// 202 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x3c, 0xcf, 0x3d, 0x4e, 0xc4, 0x30,
0x10, 0x05, 0x60, 0x85, 0xcd, 0x6e, 0xe2, 0x81, 0x00, 0x72, 0x65, 0x44, 0x41, 0x04, 0x4d, 0x2a,
0x1a, 0x4e, 0x00, 0x1d, 0x94, 0x91, 0x52, 0x5b, 0x0e, 0x76, 0xd0, 0x48, 0x8e, 0x6d, 0xf9, 0xa7,
0xe0, 0x82, 0x9c, 0x0b, 0x65, 0x20, 0xdb, 0xbd, 0xf7, 0x3d, 0x17, 0x1e, 0xb8, 0x99, 0x6d, 0x31,
0x21, 0xa2, 0xcb, 0xcf, 0x21, 0xfa, 0xec, 0xf9, 0xf5, 0x19, 0xa8, 0x3f, 0xfe, 0x54, 0xc0, 0xde,
0x76, 0xe2, 0x1c, 0xea, 0x52, 0x50, 0x8b, 0xaa, 0xaf, 0x06, 0x36, 0x52, 0xde, 0xcc, 0xa9, 0xd5,
0x88, 0x8b, 0x3f, 0xdb, 0x32, 0x7f, 0x82, 0x6e, 0x55, 0x0e, 0x17, 0x93, 0xb2, 0x2c, 0xd1, 0x26,
0x71, 0xe8, 0x0f, 0x03, 0x1b, 0xaf, 0x76, 0x9c, 0xa2, 0x4d, 0xfc, 0x01, 0x2e, 0x43, 0xf4, 0x0b,
0x5a, 0x23, 0x51, 0x27, 0x71, 0xa4, 0x27, 0xf0, 0x4f, 0xef, 0x3a, 0xf1, 0x3b, 0x68, 0x55, 0x08,
0xf6, 0x5b, 0xaa, 0x2c, 0x4e, 0xb4, 0x36, 0xd4, 0x5f, 0x33, 0xbf, 0x07, 0x56, 0x92, 0x89, 0x92,
0x7e, 0xd3, 0xd0, 0xd6, 0x6e, 0x30, 0x15, 0xd4, 0x1f, 0x75, 0x5b, 0xdf, 0x1e, 0xc7, 0x6e, 0xf5,
0x33, 0x5a, 0xf3, 0xe9, 0xdd, 0x82, 0x5f, 0x69, 0x3e, 0xd1, 0x3d, 0x2f, 0xbf, 0x01, 0x00, 0x00,
0xff, 0xff, 0xc6, 0x8c, 0x53, 0x51, 0xf2, 0x00, 0x00, 0x00,
}

View File

@@ -11,4 +11,5 @@ message Blueprint {
repeated string manifest_urls = 3;
repeated string profile_ids = 5;
repeated string apply_at = 6;
repeated string user_uuid = 7;
}

View File

@@ -15,8 +15,34 @@ import (
)
func (db *DB) ApplyToDevice(ctx context.Context, svc command.Service, bp *Blueprint, udid string) error {
var requests []*mdm.CommandRequest
for _, uuid := range bp.UserUUID {
fmt.Println("Adding user to admin account")
u, err := db.userDB.User(uuid)
if err != nil {
fmt.Printf("User UUID %s in Blueprint %s not added \n", bp.UserUUID, bp.Name)
continue
}
requests = append(requests, &mdm.CommandRequest{
UDID: udid,
Command: mdm.Command{
RequestType: "AccountConfiguration",
AccountConfiguration: mdm.AccountConfiguration{
SkipPrimarySetupAccountCreation: true,
SetPrimarySetupAccountAsRegularUser: false,
AutoSetupAdminAccounts: []mdm.AdminAccount{
mdm.AdminAccount{
ShortName: u.UserShortname,
FullName: u.UserLongname,
PasswordHash: u.PasswordHash,
Hidden: u.Hidden,
},
},
},
},
})
}
for _, appURL := range bp.ApplicationURLs {
requests = append(requests, &mdm.CommandRequest{
UDID: udid,
@@ -55,7 +81,7 @@ func (db *DB) ApplyToDevice(ctx context.Context, svc command.Service, bp *Bluepr
for _, r := range requests {
_, err := svc.NewCommand(ctx, r)
if err != nil {
return err
return errors.Wrap(err, "create new command from blueprint")
}
}
return nil

View File

@@ -59,6 +59,8 @@ func (cmd *applyCommand) Run(args []string) error {
run = cmd.applyProfile
case "app":
run = cmd.applyApp
case "users":
run = cmd.applyUser
default:
cmd.Usage()
os.Exit(1)
@@ -74,6 +76,7 @@ Valid resource types:
* blueprints
* profiles
* users
* dep-tokens
* dep-profiles
* app
@@ -107,6 +110,7 @@ func (cmd *applyCommand) applyBlueprint(args []string) error {
UUID: uuid.NewV4().String(),
ApplicationURLs: []string{cmd.config.ServerURL + "repo/exampleAppManifest.plist"},
ProfileIdentifiers: []string{"com.example.my.profile"},
UserUUID: []string{"your-admin-account-uuid"},
ApplyAt: []string{"Enroll"},
}

95
cmd/mdmctl/apply_user.go Normal file
View File

@@ -0,0 +1,95 @@
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"os"
"github.com/groob/plist"
"github.com/pkg/errors"
"github.com/micromdm/micromdm/crypto/password"
"github.com/micromdm/micromdm/user"
)
func (cmd *applyCommand) applyUser(args []string) error {
flagset := flag.NewFlagSet("users", flag.ExitOnError)
var (
flUserManifest = flagset.String("f", "", "Path to user manifest")
flTemplate = flagset.Bool("template", false, "Print a JSON example of a user manifest.")
flPassword = flagset.String("password", "", "Password of the user. Only required when creating a new user.")
)
flagset.Usage = usageFor(flagset, "mdmctl apply users [flags]")
if err := flagset.Parse(args); err != nil {
return err
}
if *flTemplate {
printUserTemplate()
return nil
}
manifestData, err := ioutil.ReadFile(*flUserManifest)
if err != nil {
return errors.Wrap(err, "read user manifest file")
}
var manifest user.User
if err := json.Unmarshal(manifestData, &manifest); err != nil {
return errors.Wrap(err, "unmarshal user manifest")
}
if manifest.UUID == "" && *flPassword == "" {
return errors.New("password argument must be provided when creating a user")
}
if *flPassword != "" {
salted, err := password.SaltedSHA512PBKDF2(*flPassword)
if err != nil {
return errors.Wrap(err, "salting plaintext password")
}
hashDict := struct {
SaltedSHA512PBKDF2 password.SaltedSHA512PBKDF2Dictionary `plist:"SALTED-SHA512-PBKDF2"`
}{
SaltedSHA512PBKDF2: salted,
}
hashPlist, err := plist.Marshal(hashDict)
if err != nil {
return errors.Wrap(err, "marshal salted password to plist")
}
manifest.PasswordHash = hashPlist
}
usr, err := cmd.applysvc.ApplyUser(context.TODO(), manifest)
if err != nil {
return errors.Wrap(err, "apply user with mdmctl")
}
f, err := os.OpenFile(*flUserManifest, os.O_RDWR, os.ModeAppend)
if err != nil {
return errors.Wrapf(err, "open user manifest %s", *flUserManifest)
}
defer f.Close()
enc := json.NewEncoder(f)
enc.SetIndent("", " ")
if err := enc.Encode(usr); err != nil {
return errors.Wrap(err, "encode user manifest")
}
return nil
}
func printUserTemplate() {
jsn := `
{
"user_shortname": "",
"user_longname": "",
"hidden": false
}
`
fmt.Println(jsn)
}

View File

@@ -65,6 +65,8 @@ func (cmd *getCommand) Run(args []string) error {
run = cmd.getBlueprints
case "profiles":
run = cmd.getProfiles
case "users":
run = cmd.getUsers
case "apps":
run = cmd.getApps
default:
@@ -87,6 +89,7 @@ Valid resource types:
* dep-devices
* dep-account
* dep-profiles
* users
* profiles
* apps

47
cmd/mdmctl/get_users.go Normal file
View File

@@ -0,0 +1,47 @@
package main
import (
"context"
"flag"
"fmt"
"os"
"text/tabwriter"
"github.com/pkg/errors"
"github.com/micromdm/micromdm/core/list"
)
type usersTableOutput struct{ w *tabwriter.Writer }
func (out *usersTableOutput) BasicHeader() {
fmt.Fprintf(out.w, "UUID\tUDID\tUserID\tUserShortName\tUserLongName\n")
}
func (out *usersTableOutput) BasicFooter() {
out.w.Flush()
}
func (cmd *getCommand) getUsers(args []string) error {
flagset := flag.NewFlagSet("users", flag.ExitOnError)
flagset.Usage = usageFor(flagset, "mdmctl get users [flags]")
if err := flagset.Parse(args); err != nil {
return err
}
w := tabwriter.NewWriter(os.Stderr, 0, 4, 2, ' ', 0)
out := &usersTableOutput{w}
out.BasicHeader()
defer out.BasicFooter()
users, err := cmd.list.ListUsers(context.TODO(), list.ListUsersOption{})
if err != nil {
return errors.Wrap(err, "list users")
}
for _, u := range users {
fmt.Fprintf(out.w, "%s\t%s\t%s\t%v\t%s\n", u.UUID, u.UDID, u.UserID, u.UserShortname, u.UserLongname)
}
return nil
}

View File

@@ -69,12 +69,24 @@ func NewClient(instance string, logger log.Logger, token string, opts ...httptra
).Endpoint()
}
var applyUserEndpoint endpoint.Endpoint
{
applyUserEndpoint = httptransport.NewClient(
"PUT",
copyURL(u, "/v1/users"),
encodeRequestWithToken(token, EncodeHTTPGenericRequest),
DecodeApplyUserResponse,
opts...,
).Endpoint()
}
return Endpoints{
ApplyBlueprintEndpoint: applyBlueprintEndpoint,
ApplyDEPTokensEndpoint: applyDEPTokensEndpoint,
ApplyProfileEndpoint: applyProfileEndpoint,
DefineDEPProfileEndpoint: defineDEPProfileEndpoint,
AppUploadEndpoint: uploadAppEndpoint,
ApplyUserEndpoint: applyUserEndpoint,
}, nil
}

View File

@@ -8,6 +8,7 @@ import (
"github.com/micromdm/dep"
"github.com/micromdm/micromdm/blueprint"
"github.com/micromdm/micromdm/profile"
"github.com/micromdm/micromdm/user"
)
type Endpoints struct {
@@ -16,6 +17,19 @@ type Endpoints struct {
ApplyProfileEndpoint endpoint.Endpoint
DefineDEPProfileEndpoint endpoint.Endpoint
AppUploadEndpoint endpoint.Endpoint
ApplyUserEndpoint endpoint.Endpoint
}
func (e Endpoints) ApplyUser(ctx context.Context, u user.User) (*user.User, error) {
request := applyUserRequest{
User: u,
}
resp, err := e.ApplyUserEndpoint(ctx, request)
if err != nil {
return nil, err
}
usr := resp.(applyUserResponse).User
return &usr, resp.(applyUserResponse).Err
}
func (e Endpoints) UploadApp(ctx context.Context, manifestName string, manifest io.Reader, pkgName string, pkg io.Reader) error {
@@ -110,6 +124,17 @@ func MakeDefineDEPProfile(svc Service) endpoint.Endpoint {
}
}
func MakeApplyUserEndpoint(svc Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (response interface{}, err error) {
req := request.(applyUserRequest)
u, err := svc.ApplyUser(ctx, req.User)
return applyUserResponse{
User: *u,
Err: err,
}, nil
}
}
func MakeUploadAppEndpiont(svc Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (response interface{}, err error) {
req := request.(appUploadRequest)
@@ -171,3 +196,14 @@ type depProfileResponse struct {
}
func (r *depProfileResponse) error() error { return r.Err }
type applyUserRequest struct {
User user.User `json:"user"`
}
type applyUserResponse struct {
Err error `json:"err"`
User user.User `json:"user"`
}
func (r applyUserResponse) error() error { return r.Err }

View File

@@ -13,6 +13,7 @@ import (
"net/textproto"
"github.com/fullsailor/pkcs7"
"github.com/pkg/errors"
"github.com/micromdm/dep"
"github.com/micromdm/micromdm/appstore"
@@ -20,6 +21,7 @@ import (
"github.com/micromdm/micromdm/deptoken"
"github.com/micromdm/micromdm/profile"
"github.com/micromdm/micromdm/pubsub"
"github.com/micromdm/micromdm/user"
)
type Service interface {
@@ -27,6 +29,7 @@ type Service interface {
ApplyDEPToken(ctx context.Context, P7MContent []byte) error
ApplyProfile(ctx context.Context, p *profile.Profile) error
UploadApp(ctx context.Context, manifestName string, manifest io.Reader, pkgName string, pkg io.Reader) error
ApplyUser(ctx context.Context, u user.User) (*user.User, error)
DEPService
}
@@ -38,6 +41,20 @@ type ApplyService struct {
Profiles *profile.DB
Tokens *deptoken.DB
Apps appstore.AppStore
Users *user.DB
}
func (svc *ApplyService) ApplyUser(ctx context.Context, u user.User) (*user.User, error) {
toSave := &u
if u.UUID == "" { //newUser
usr, err := user.NewFromRequest(u)
if err != nil {
return nil, errors.Wrap(err, "create user from request")
}
toSave = usr
}
err := svc.Users.Save(toSave)
return toSave, errors.Wrap(err, "apply user")
}
func (svc *ApplyService) UploadApp(ctx context.Context, manifestName string, manifest io.Reader, pkgName string, pkg io.Reader) error {

View File

@@ -19,6 +19,7 @@ type HTTPHandlers struct {
ProfileHandler http.Handler
DefineDEPProfileHandler http.Handler
AppUploadHandler http.Handler
ApplyUserhandler http.Handler
}
func MakeHTTPHandlers(ctx context.Context, endpoints Endpoints, opts ...httptransport.ServerOption) HTTPHandlers {
@@ -53,6 +54,12 @@ func MakeHTTPHandlers(ctx context.Context, endpoints Endpoints, opts ...httptran
encodeResponse,
opts...,
),
ApplyUserhandler: httptransport.NewServer(
endpoints.ApplyUserEndpoint,
decodeUserRequest,
encodeResponse,
opts...,
),
}
return h
}
@@ -110,6 +117,15 @@ func decodeAppUploadRequest(ctx context.Context, r *http.Request) (interface{},
}, nil
}
func decodeUserRequest(ctx context.Context, r *http.Request) (interface{}, error) {
var req applyUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, errors.Wrap(err, "decode user request")
}
defer r.Body.Close()
return req, nil
}
func EncodeUploadAppRequest(_ context.Context, r *http.Request, request interface{}) error {
req := request.(appUploadRequest)
body := new(bytes.Buffer)
@@ -236,3 +252,12 @@ func DecodeUploadAppResponse(_ context.Context, r *http.Response) (interface{},
err := json.NewDecoder(r.Body).Decode(&resp)
return resp, err
}
func DecodeApplyUserResponse(_ context.Context, r *http.Response) (interface{}, error) {
if r.StatusCode != http.StatusOK {
return nil, errorDecoder(r)
}
var resp applyUserResponse
err := json.NewDecoder(r.Body).Decode(&resp)
return resp, err
}

View File

@@ -101,6 +101,17 @@ func NewClient(instance string, logger log.Logger, token string, opts ...httptra
).Endpoint()
}
var listUsersEndpoint endpoint.Endpoint
{
listUsersEndpoint = httptransport.NewClient(
"GET",
copyURL(u, "/v1/users"),
encodeRequestWithToken(token, EncodeHTTPGenericRequest),
DecodeListUsersResponse,
opts...,
).Endpoint()
}
return Endpoints{
ListDevicesEndpoint: listDevicesEndpoint,
GetDEPTokensEndpoint: getDEPTokensEndpoint,
@@ -110,6 +121,7 @@ func NewClient(instance string, logger log.Logger, token string, opts ...httptra
GetDEPDeviceEndpoint: getDEPDeviceDetailsEndpoint,
GetDEPProfileEndpoint: getDEPProfilesEndpoint,
ListAppsEndpont: listAppsEndpoint,
ListUserEndpoint: listUsersEndpoint,
}, nil
}

View File

@@ -9,6 +9,7 @@ import (
"github.com/micromdm/micromdm/blueprint"
"github.com/micromdm/micromdm/deptoken"
"github.com/micromdm/micromdm/profile"
"github.com/micromdm/micromdm/user"
)
type Endpoints struct {
@@ -20,6 +21,16 @@ type Endpoints struct {
GetDEPDeviceEndpoint endpoint.Endpoint
GetDEPProfileEndpoint endpoint.Endpoint
ListAppsEndpont endpoint.Endpoint
ListUserEndpoint endpoint.Endpoint
}
func (e Endpoints) ListUsers(ctx context.Context, opts ListUsersOption) ([]user.User, error) {
request := userRequest{opts}
response, err := e.ListUserEndpoint(ctx, request.Opts)
if err != nil {
return nil, err
}
return response.(userResponse).Users, response.(userResponse).Err
}
func (e Endpoints) ListDevices(ctx context.Context, opts ListDevicesOption) ([]DeviceDTO, error) {
@@ -84,6 +95,17 @@ func (e Endpoints) GetDEPDevice(ctx context.Context, serials []string) (*dep.Dev
return response.(depDeviceDetailsResponse).DeviceDetailsResponse, response.(depDeviceDetailsResponse).Err
}
func MakeListUsersEndpoint(svc Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (response interface{}, err error) {
req := request.(userRequest)
dto, err := svc.ListUsers(ctx, req.Opts)
return userResponse{
Users: dto,
Err: err,
}, nil
}
}
func MakeListDevicesEndpoint(svc Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (response interface{}, err error) {
req := request.(devicesRequest)
@@ -178,6 +200,14 @@ type DeviceDTO struct {
LastSeen time.Time `json:"last_seen"`
}
type userRequest struct{ Opts ListUsersOption }
type userResponse struct {
Users []user.User `json:"users"`
Err error `json:"err,omitempty"`
}
func (r userResponse) error() error { return r.Err }
type devicesRequest struct{ Opts ListDevicesOption }
type devicesResponse struct {
Devices []DeviceDTO `json:"devices"`

View File

@@ -8,13 +8,15 @@ import (
"github.com/groob/plist"
"github.com/micromdm/dep"
"github.com/pkg/errors"
"github.com/micromdm/micromdm/appstore"
"github.com/micromdm/micromdm/blueprint"
"github.com/micromdm/micromdm/deptoken"
"github.com/micromdm/micromdm/device"
"github.com/micromdm/micromdm/profile"
"github.com/micromdm/micromdm/pubsub"
"github.com/pkg/errors"
"github.com/micromdm/micromdm/user"
)
type ListDevicesOption struct {
@@ -25,6 +27,14 @@ type ListDevicesOption struct {
FilterUDID []string
}
type ListUsersOption struct {
Page int
PerPage int
FilterUserID []string
FilterUDID []string
}
type GetBlueprintsOption struct {
FilterName string
}
@@ -39,6 +49,7 @@ type ListAppsOption struct {
type Service interface {
ListDevices(ctx context.Context, opt ListDevicesOption) ([]DeviceDTO, error)
ListUsers(ctx context.Context, opt ListUsersOption) ([]user.User, error)
GetDEPTokens(ctx context.Context) ([]deptoken.DEPToken, []byte, error)
GetBlueprints(ctx context.Context, opt GetBlueprintsOption) ([]blueprint.Blueprint, error)
GetProfiles(ctx context.Context, opt GetProfilesOption) ([]profile.Profile, error)
@@ -55,6 +66,7 @@ type ListService struct {
Profiles *profile.DB
Tokens *deptoken.DB
Apps appstore.AppStore
Users *user.DB
}
func (svc *ListService) ListApplications(ctx context.Context, opts ListAppsOption) ([]AppDTO, error) {
@@ -126,6 +138,11 @@ func (svc *ListService) ListDevices(ctx context.Context, opt ListDevicesOption)
return dto, err
}
func (svc *ListService) ListUsers(ctx context.Context, opts ListUsersOption) ([]user.User, error) {
u, err := svc.Users.List()
return u, errors.Wrap(err, "list users from api request")
}
func (svc *ListService) GetDEPTokens(ctx context.Context) ([]deptoken.DEPToken, []byte, error) {
_, cert, err := svc.Tokens.DEPKeypair()
if err != nil {

View File

@@ -4,11 +4,11 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"io/ioutil"
"net/http"
httptransport "github.com/go-kit/kit/transport/http"
"github.com/pkg/errors"
)
type HTTPHandlers struct {
@@ -20,6 +20,7 @@ type HTTPHandlers struct {
GetDEPProfileHandler http.Handler
GetDEPDeviceDetailsHandler http.Handler
ListAppsHandler http.Handler
ListUsersHander http.Handler
}
func MakeHTTPHandlers(ctx context.Context, endpoints Endpoints, opts ...httptransport.ServerOption) HTTPHandlers {
@@ -69,6 +70,12 @@ func MakeHTTPHandlers(ctx context.Context, endpoints Endpoints, opts ...httptran
encodeResponse,
opts...,
),
ListUsersHander: httptransport.NewServer(
endpoints.ListUserEndpoint,
decodeListUsersRequest,
encodeResponse,
opts...,
),
}
return h
}
@@ -84,6 +91,11 @@ func decodeListDevicesRequest(ctx context.Context, r *http.Request) (interface{}
return req, nil
}
func decodeListUsersRequest(ctx context.Context, r *http.Request) (interface{}, error) {
req := userRequest{}
return req, nil
}
func decodeGetBlueprintsRequest(ctx context.Context, r *http.Request) (interface{}, error) {
var opts GetBlueprintsOption
if err := json.NewDecoder(r.Body).Decode(&opts); err != nil {
@@ -250,3 +262,12 @@ func DecodeListAppsResponse(_ context.Context, r *http.Response) (interface{}, e
err := json.NewDecoder(r.Body).Decode(&resp)
return resp, err
}
func DecodeListUsersResponse(_ context.Context, r *http.Response) (interface{}, error) {
if r.StatusCode != http.StatusOK {
return nil, errorDecoder(r)
}
var resp userResponse
err := json.NewDecoder(r.Body).Decode(&resp)
return resp, errors.Wrap(err, "decode user response")
}

View File

@@ -0,0 +1,74 @@
// Package password provides utilities for creating and verifying macOS
// passwords for MDM account configuration.
package password
import (
"bytes"
"crypto/rand"
"crypto/sha512"
"errors"
"math/big"
"golang.org/x/crypto/pbkdf2"
)
// macKeyLen is the length of a PBKDF2 salt.
const macKeyLen = 128
// ErrNoMatch is returned by Verify if the password does not match.
var ErrNoMatch = errors.New("password does not match")
// SaltedSha512PBKDF2Dictionary is a SHA512 PBKDF2 dictionary.
type SaltedSHA512PBKDF2Dictionary struct {
Iterations int `plist:"iterations"`
Salt []byte `plist:"salt"`
Entropy []byte `plist:"entropy"`
}
// SaltedSHA512PBKDF2 creates a SALTED-SHA512-PBKDF2 dictionary
// from a plaintext password. The hash function will use a 128 bit
// salt.
func SaltedSHA512PBKDF2(plaintext string) (SaltedSHA512PBKDF2Dictionary, error) {
salt := make([]byte, 32)
_, err := rand.Read(salt)
if err != nil {
return SaltedSHA512PBKDF2Dictionary{}, err
}
iterations, err := secureRandInt(20000, 40000)
if err != nil {
return SaltedSHA512PBKDF2Dictionary{}, err
}
return SaltedSHA512PBKDF2Dictionary{
Iterations: iterations,
Salt: salt,
Entropy: pbkdf2.Key([]byte(plaintext),
salt, iterations, macKeyLen, sha512.New),
}, nil
}
// Verify verifies a plaintext password against a existing SALTED-SHA512-PBKDF2
// password dictionary.
func Verify(plaintext string, h SaltedSHA512PBKDF2Dictionary) error {
hashed := pbkdf2.Key([]byte(plaintext), h.Salt, h.Iterations, macKeyLen, sha512.New)
if !bytes.Equal(h.Entropy, hashed) {
return ErrNoMatch
}
return nil
}
// CCCalibratePBKDF uses a pseudorandom value returned within 100 milliseconds.
// Use a random int from crypto/rand between 20,000 and 40,000 instead.
func secureRandInt(min, max int64) (int, error) {
var random int
for {
iter, err := rand.Int(rand.Reader, big.NewInt(max))
if err != nil {
return 0, err
}
if iter.Int64() >= min {
random = int(iter.Int64())
break
}
}
return random, nil
}

View File

@@ -56,6 +56,7 @@ import (
"github.com/micromdm/micromdm/pubsub/inmem"
nanopush "github.com/micromdm/micromdm/push"
"github.com/micromdm/micromdm/queue"
"github.com/micromdm/micromdm/user"
)
const homePage = `<!doctype html>
@@ -154,6 +155,11 @@ func serve(args []string) error {
stdlog.Fatal(err)
}
userDB, err := user.NewDB(sm.db, sm.pubclient)
if err != nil {
stdlog.Fatal(err)
}
sm.profileDB, err = profile.NewDB(sm.db)
if err != nil {
stdlog.Fatal(err)
@@ -164,7 +170,7 @@ func serve(args []string) error {
stdlog.Fatalf("enrollment service: %s", sm.err)
}
bpDB, err := blueprint.NewDB(sm.db, sm.profileDB)
bpDB, err := blueprint.NewDB(sm.db, sm.profileDB, userDB)
if err != nil {
stdlog.Fatal(err)
}
@@ -245,6 +251,7 @@ func serve(args []string) error {
}
tokenDB := &deptoken.DB{DB: sm.db, Publisher: sm.pubclient}
appDB := &appstore.Repo{Path: *flRepoPath}
var listsvc list.Service
{
l := &list.ListService{
@@ -254,6 +261,7 @@ func serve(args []string) error {
Blueprints: bpDB,
Profiles: sm.profileDB,
Apps: appDB,
Users: userDB,
}
listsvc = l
@@ -275,6 +283,7 @@ func serve(args []string) error {
GetDEPProfileEndpoint: list.MakeGetDEPProfileEndpoint(listsvc),
GetDEPDeviceEndpoint: list.MakeGetDEPDeviceDetailsEndpoint(listsvc),
ListAppsEndpont: list.MakeListAppsEndpoint(listsvc),
ListUserEndpoint: list.MakeListUsersEndpoint(listsvc),
}
var applysvc apply.Service
@@ -285,6 +294,7 @@ func serve(args []string) error {
Tokens: tokenDB,
Profiles: sm.profileDB,
Apps: appDB,
Users: userDB,
}
applysvc = l
if err := l.WatchTokenUpdates(sm.pubclient); err != nil {
@@ -312,12 +322,18 @@ func serve(args []string) error {
appUploadEndpoint = apply.MakeUploadAppEndpiont(applysvc)
}
var applyUserEndpoint endpoint.Endpoint
{
applyUserEndpoint = apply.MakeApplyUserEndpoint(applysvc)
}
applyEndpoints := apply.Endpoints{
ApplyBlueprintEndpoint: applyBlueprintEndpoint,
ApplyDEPTokensEndpoint: apply.MakeApplyDEPTokensEndpoint(applysvc),
ApplyProfileEndpoint: applyProfileEndpoint,
DefineDEPProfileEndpoint: defineDEPProfileEndpoint,
AppUploadEndpoint: appUploadEndpoint,
ApplyUserEndpoint: applyUserEndpoint,
}
applyAPIHandlers := apply.MakeHTTPHandlers(ctx, applyEndpoints, connectOpts...)
@@ -361,6 +377,8 @@ func serve(args []string) error {
r.Handle("/v1/dep/profiles", apiAuthMiddleware(*flAPIKey, applyAPIHandlers.DefineDEPProfileHandler)).Methods("POST")
r.Handle("/v1/apps", apiAuthMiddleware(*flAPIKey, applyAPIHandlers.AppUploadHandler)).Methods("POST")
r.Handle("/v1/apps", apiAuthMiddleware(*flAPIKey, listAPIHandlers.ListAppsHandler)).Methods("GET")
r.Handle("/v1/users", apiAuthMiddleware(*flAPIKey, applyAPIHandlers.ApplyUserhandler)).Methods("PUT")
r.Handle("/v1/users", apiAuthMiddleware(*flAPIKey, listAPIHandlers.ListUsersHander)).Methods("GET")
r.Handle("/v1/config/certificate", apiAuthMiddleware(*flAPIKey, configHandlers.SavePushCertificateHandler)).Methods("PUT")
}

148
user/db.go Normal file
View File

@@ -0,0 +1,148 @@
package user
import (
"fmt"
"github.com/boltdb/bolt"
"github.com/pkg/errors"
"github.com/micromdm/micromdm/pubsub"
)
const (
UserBucket = "mdm.Users"
userIndexBucket = "mdm.UserIdx"
)
type DB struct {
*bolt.DB
}
func NewDB(db *bolt.DB, pubsubSvc pubsub.PublishSubscriber) (*DB, error) {
err := db.Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists([]byte(userIndexBucket))
if err != nil {
return err
}
_, err = tx.CreateBucketIfNotExists([]byte(UserBucket))
return err
})
if err != nil {
return nil, errors.Wrapf(err, "creating %s bucket", UserBucket)
}
datastore := &DB{
DB: db,
}
return datastore, nil
}
func (db *DB) List() ([]User, error) {
var users []User
err := db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(UserBucket))
c := b.Cursor()
for k, v := c.First(); k != nil; k, v = c.Next() {
var u User
if err := UnmarshalUser(v, &u); err != nil {
return err
}
users = append(users, u)
}
return nil
})
return users, errors.Wrap(err, "list users")
}
func (db *DB) Save(u *User) error {
tx, err := db.DB.Begin(true)
if err != nil {
return errors.Wrap(err, "begin transaction")
}
bkt := tx.Bucket([]byte(UserBucket))
if bkt == nil {
return fmt.Errorf("bucket %q not found!", UserBucket)
}
userpb, err := MarshalUser(u)
if err != nil {
return errors.Wrap(err, "marshalling user")
}
// store an array of indices to reference the UUID, which will be the
// key used to store the actual user.
indexes := []string{u.UDID, u.UserID}
idxBucket := tx.Bucket([]byte(userIndexBucket))
if idxBucket == nil {
return fmt.Errorf("bucket %q not found!", userIndexBucket)
}
for _, idx := range indexes {
if idx == "" {
continue
}
key := []byte(idx)
if err := idxBucket.Put(key, []byte(u.UUID)); err != nil {
return errors.Wrap(err, "user userIdx in boltdb")
}
}
key := []byte(u.UUID)
if err := bkt.Put(key, userpb); err != nil {
return errors.Wrap(err, "store user in boltdb")
}
return tx.Commit()
}
func (db *DB) User(uuid string) (*User, error) {
var u User
err := db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(UserBucket))
v := b.Get([]byte(uuid))
if v == nil {
return &notFound{"User", fmt.Sprintf("uuid %s", uuid)}
}
return UnmarshalUser(v, &u)
})
if err != nil {
return nil, errors.Wrap(err, "get user by user id from bolt")
}
return &u, nil
}
func (db *DB) UserByUserID(userID string) (*User, error) {
var u User
err := db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(UserBucket))
ib := tx.Bucket([]byte(userIndexBucket))
idx := ib.Get([]byte(userID))
if idx == nil {
return &notFound{"User", fmt.Sprintf("user id %s", userID)}
}
v := b.Get(idx)
if idx == nil {
return &notFound{"User", fmt.Sprintf("uuid %s", string(idx))}
}
return UnmarshalUser(v, &u)
})
if err != nil {
return nil, errors.Wrap(err, "get user by user id from bolt")
}
return &u, nil
}
type notFound struct {
ResourceType string
Message string
}
func (e *notFound) Error() string {
return fmt.Sprintf("not found: %s %s", e.ResourceType, e.Message)
}
func isNotFound(err error) bool {
if _, ok := err.(*notFound); ok {
return true
}
return false
}

View File

@@ -0,0 +1,3 @@
package userproto
//go:generate protoc --go_out=. user.proto

View File

@@ -0,0 +1,123 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: user.proto
/*
Package userproto is a generated protocol buffer package.
It is generated from these files:
user.proto
It has these top-level messages:
User
*/
package userproto
import proto "github.com/golang/protobuf/proto"
import fmt "fmt"
import math "math"
// Reference imports to suppress errors if they are not otherwise used.
var _ = proto.Marshal
var _ = fmt.Errorf
var _ = math.Inf
// This is a compile-time assertion to ensure that this generated file
// is compatible with the proto package it is being compiled against.
// A compilation error at this line likely means your copy of the
// proto package needs to be updated.
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
type User struct {
Uuid string `protobuf:"bytes,1,opt,name=uuid" json:"uuid,omitempty"`
Udid string `protobuf:"bytes,2,opt,name=udid" json:"udid,omitempty"`
UserId string `protobuf:"bytes,3,opt,name=user_id,json=userId" json:"user_id,omitempty"`
UserShortname string `protobuf:"bytes,4,opt,name=user_shortname,json=userShortname" json:"user_shortname,omitempty"`
UserLongname string `protobuf:"bytes,5,opt,name=user_longname,json=userLongname" json:"user_longname,omitempty"`
AuthToken string `protobuf:"bytes,6,opt,name=auth_token,json=authToken" json:"auth_token,omitempty"`
PasswordHash []byte `protobuf:"bytes,7,opt,name=password_hash,json=passwordHash,proto3" json:"password_hash,omitempty"`
Hidden bool `protobuf:"varint,8,opt,name=hidden" json:"hidden,omitempty"`
}
func (m *User) Reset() { *m = User{} }
func (m *User) String() string { return proto.CompactTextString(m) }
func (*User) ProtoMessage() {}
func (*User) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} }
func (m *User) GetUuid() string {
if m != nil {
return m.Uuid
}
return ""
}
func (m *User) GetUdid() string {
if m != nil {
return m.Udid
}
return ""
}
func (m *User) GetUserId() string {
if m != nil {
return m.UserId
}
return ""
}
func (m *User) GetUserShortname() string {
if m != nil {
return m.UserShortname
}
return ""
}
func (m *User) GetUserLongname() string {
if m != nil {
return m.UserLongname
}
return ""
}
func (m *User) GetAuthToken() string {
if m != nil {
return m.AuthToken
}
return ""
}
func (m *User) GetPasswordHash() []byte {
if m != nil {
return m.PasswordHash
}
return nil
}
func (m *User) GetHidden() bool {
if m != nil {
return m.Hidden
}
return false
}
func init() {
proto.RegisterType((*User)(nil), "userproto.User")
}
func init() { proto.RegisterFile("user.proto", fileDescriptor0) }
var fileDescriptor0 = []byte{
// 206 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x34, 0xcf, 0xbf, 0x4a, 0x04, 0x31,
0x10, 0x06, 0x70, 0xa2, 0x6b, 0xee, 0x76, 0x58, 0x2d, 0xa6, 0xd0, 0x34, 0xc2, 0xa2, 0x08, 0x5b,
0xd9, 0xf8, 0x12, 0x0a, 0x56, 0xab, 0xd6, 0x21, 0x32, 0xc1, 0x04, 0x35, 0x39, 0xf2, 0x07, 0x1f,
0xdb, 0x57, 0x38, 0x66, 0xd8, 0xeb, 0xbe, 0xf9, 0xcd, 0x97, 0xc0, 0x00, 0xf4, 0xea, 0xcb, 0xe3,
0xa1, 0xe4, 0x96, 0x71, 0xe4, 0x2c, 0xf1, 0xee, 0x5f, 0xc1, 0xf0, 0x51, 0x7d, 0x41, 0x84, 0xa1,
0xf7, 0x48, 0x46, 0xcd, 0x6a, 0x19, 0x57, 0xc9, 0x62, 0x14, 0xc9, 0x9c, 0x6d, 0x46, 0x91, 0xf0,
0x06, 0x76, 0xfc, 0xda, 0x46, 0x32, 0xe7, 0xc2, 0x9a, 0xc7, 0x17, 0xc2, 0x07, 0xb8, 0x92, 0x45,
0x0d, 0xb9, 0xb4, 0xe4, 0x7e, 0xbd, 0x19, 0x64, 0x7f, 0xc9, 0xfa, 0x76, 0x42, 0xbc, 0x07, 0x01,
0xfb, 0x93, 0xd3, 0x97, 0xb4, 0x2e, 0xa4, 0x35, 0x31, 0xbe, 0x6e, 0x86, 0xb7, 0x00, 0xae, 0xb7,
0x60, 0x5b, 0xfe, 0xf6, 0xc9, 0x68, 0x69, 0x8c, 0x2c, 0xef, 0x0c, 0xfc, 0xc7, 0xc1, 0xd5, 0xfa,
0x97, 0x0b, 0xd9, 0xe0, 0x6a, 0x30, 0xbb, 0x59, 0x2d, 0xd3, 0x3a, 0x9d, 0xf0, 0xd9, 0xd5, 0x80,
0xd7, 0xa0, 0x43, 0x24, 0xf2, 0xc9, 0xec, 0x67, 0xb5, 0xec, 0xd7, 0x6d, 0xfa, 0xd4, 0x72, 0xf8,
0xd3, 0x31, 0x00, 0x00, 0xff, 0xff, 0x35, 0xa8, 0xcb, 0x32, 0x11, 0x01, 0x00, 0x00,
}

View File

@@ -0,0 +1,16 @@
syntax = "proto3";
package userproto;
message User {
string uuid = 1;
string udid = 2;
string user_id = 3;
string user_shortname = 4;
string user_longname = 5;
string auth_token = 6;
bytes password_hash = 7;
bool hidden = 8;
}

62
user/user.go Normal file
View File

@@ -0,0 +1,62 @@
// Package user provides utilites for managing users with MDM.
package user
import (
"github.com/gogo/protobuf/proto"
"github.com/pkg/errors"
uuid "github.com/satori/go.uuid"
"github.com/micromdm/micromdm/user/internal/userproto"
)
type User struct {
UUID string `json:"uuid"`
UDID string `json:"udid"`
UserID string `json:"user_id"`
UserShortname string `json:"user_shortname"`
UserLongname string `json:"user_longname"`
AuthToken string `json:"auth_token"`
PasswordHash []byte `json:"password_hash"`
Hidden bool `json:"hidden"`
}
func NewFromRequest(u User) (*User, error) {
newUser := User{
UUID: uuid.NewV4().String(),
UserShortname: u.UserShortname,
UserLongname: u.UserLongname,
PasswordHash: u.PasswordHash,
Hidden: u.Hidden,
}
return &newUser, nil
}
func MarshalUser(u *User) ([]byte, error) {
pb := userproto.User{
Uuid: u.UUID,
Udid: u.UDID,
UserId: u.UserID,
UserShortname: u.UserShortname,
UserLongname: u.UserLongname,
AuthToken: u.AuthToken,
PasswordHash: u.PasswordHash,
Hidden: u.Hidden,
}
return proto.Marshal(&pb)
}
func UnmarshalUser(data []byte, u *User) error {
var pb userproto.User
if err := proto.Unmarshal(data, &pb); err != nil {
return errors.Wrap(err, "unmarshal proto to user")
}
u.UUID = pb.GetUuid()
u.UDID = pb.GetUdid()
u.UserID = pb.GetUserId()
u.UserShortname = pb.GetUserShortname()
u.UserLongname = pb.GetUserLongname()
u.AuthToken = pb.GetAuthToken()
u.PasswordHash = pb.GetPasswordHash()
u.Hidden = pb.GetHidden()
return nil
}