diff --git a/blueprint/blueprint.go b/blueprint/blueprint.go index c62a4d94..cf6d3b5a 100644 --- a/blueprint/blueprint.go +++ b/blueprint/blueprint.go @@ -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 } diff --git a/blueprint/db.go b/blueprint/db.go index 32b8a3b4..37461b87 100644 --- a/blueprint/db.go +++ b/blueprint/db.go @@ -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 } diff --git a/blueprint/internal/blueprintproto/blueprint.pb.go b/blueprint/internal/blueprintproto/blueprint.pb.go index 736dcb3e..28d64ccd 100644 --- a/blueprint/internal/blueprintproto/blueprint.pb.go +++ b/blueprint/internal/blueprintproto/blueprint.pb.go @@ -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, } diff --git a/blueprint/internal/blueprintproto/blueprint.proto b/blueprint/internal/blueprintproto/blueprint.proto index 5c7a7a21..6211933e 100644 --- a/blueprint/internal/blueprintproto/blueprint.proto +++ b/blueprint/internal/blueprintproto/blueprint.proto @@ -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; } diff --git a/blueprint/listener.go b/blueprint/listener.go index 0fbaf8fe..262845ca 100644 --- a/blueprint/listener.go +++ b/blueprint/listener.go @@ -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 diff --git a/cmd/mdmctl/apply.go b/cmd/mdmctl/apply.go index bf50c0dd..1d08a463 100644 --- a/cmd/mdmctl/apply.go +++ b/cmd/mdmctl/apply.go @@ -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"}, } diff --git a/cmd/mdmctl/apply_user.go b/cmd/mdmctl/apply_user.go new file mode 100644 index 00000000..4d100eba --- /dev/null +++ b/cmd/mdmctl/apply_user.go @@ -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) +} diff --git a/cmd/mdmctl/get.go b/cmd/mdmctl/get.go index d4f28ee9..1a8091e9 100644 --- a/cmd/mdmctl/get.go +++ b/cmd/mdmctl/get.go @@ -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 diff --git a/cmd/mdmctl/get_users.go b/cmd/mdmctl/get_users.go new file mode 100644 index 00000000..a6c2fe00 --- /dev/null +++ b/cmd/mdmctl/get_users.go @@ -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 +} diff --git a/core/apply/client.go b/core/apply/client.go index c8b445bf..bc7c335d 100644 --- a/core/apply/client.go +++ b/core/apply/client.go @@ -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 } diff --git a/core/apply/endpoint.go b/core/apply/endpoint.go index e790b668..374596b5 100644 --- a/core/apply/endpoint.go +++ b/core/apply/endpoint.go @@ -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 } diff --git a/core/apply/service.go b/core/apply/service.go index 45c4a78a..91bd30be 100644 --- a/core/apply/service.go +++ b/core/apply/service.go @@ -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 { diff --git a/core/apply/transport_http.go b/core/apply/transport_http.go index 1087021e..f6adae2e 100644 --- a/core/apply/transport_http.go +++ b/core/apply/transport_http.go @@ -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 +} diff --git a/core/list/client.go b/core/list/client.go index 01cb9046..0283e096 100644 --- a/core/list/client.go +++ b/core/list/client.go @@ -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 } diff --git a/core/list/endpoint.go b/core/list/endpoint.go index d8960280..23c9cf28 100644 --- a/core/list/endpoint.go +++ b/core/list/endpoint.go @@ -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"` diff --git a/core/list/service.go b/core/list/service.go index 3a365a1d..a13b29de 100644 --- a/core/list/service.go +++ b/core/list/service.go @@ -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 { diff --git a/core/list/transport_http.go b/core/list/transport_http.go index fbcd0e0a..1e53073f 100644 --- a/core/list/transport_http.go +++ b/core/list/transport_http.go @@ -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") +} diff --git a/crypto/password/password.go b/crypto/password/password.go new file mode 100644 index 00000000..32167652 --- /dev/null +++ b/crypto/password/password.go @@ -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 +} diff --git a/serve.go b/serve.go index 0e6bb2b1..83db2f3d 100644 --- a/serve.go +++ b/serve.go @@ -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 = ` @@ -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") } diff --git a/user/db.go b/user/db.go new file mode 100644 index 00000000..620e6e97 --- /dev/null +++ b/user/db.go @@ -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 ¬Found{"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 ¬Found{"User", fmt.Sprintf("user id %s", userID)} + } + v := b.Get(idx) + if idx == nil { + return ¬Found{"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 +} diff --git a/user/internal/userproto/user.go b/user/internal/userproto/user.go new file mode 100644 index 00000000..13604c02 --- /dev/null +++ b/user/internal/userproto/user.go @@ -0,0 +1,3 @@ +package userproto + +//go:generate protoc --go_out=. user.proto diff --git a/user/internal/userproto/user.pb.go b/user/internal/userproto/user.pb.go new file mode 100644 index 00000000..54c8fbba --- /dev/null +++ b/user/internal/userproto/user.pb.go @@ -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, +} diff --git a/user/internal/userproto/user.proto b/user/internal/userproto/user.proto new file mode 100644 index 00000000..53807ffc --- /dev/null +++ b/user/internal/userproto/user.proto @@ -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; +} + diff --git a/user/user.go b/user/user.go new file mode 100644 index 00000000..fba695e5 --- /dev/null +++ b/user/user.go @@ -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 +}