From 5fd576fcb6469eea34820756ca17c81361b247fe Mon Sep 17 00:00:00 2001 From: Mo Firouz Date: Wed, 4 Apr 2018 17:46:29 +0100 Subject: [PATCH] Add ability to delete or export user info for compliance purposes. #182 --- Gopkg.lock | 2 +- console/console.pb.go | 105 ++++++++---- console/console.proto | 8 +- console/console.swagger.json | 119 +++++++++++++ main.go | 8 +- migrate/migrate-packr.go | 4 +- migrate/sql/20180103142001_initial_schema.sql | 8 + server/api.go | 4 +- server/config.go | 24 +++ server/console.go | 159 ++++++++++++++++++ server/console_gdpr.go | 90 ++++++++++ server/core_account.go | 7 +- server/core_friend.go | 48 +++++- server/core_notification.go | 12 +- server/core_storage.go | 42 +++++ server/core_user.go | 9 + 16 files changed, 599 insertions(+), 50 deletions(-) create mode 100644 server/console.go create mode 100644 server/console_gdpr.go diff --git a/Gopkg.lock b/Gopkg.lock index ac79ffc2d..9a3507fee 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -392,6 +392,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "c9cf37873849b7c9ea30ef66151a1540afde6e0a518de051f8aef425e2e92f77" + inputs-digest = "92727e62a718744c1d17c1a4e2c19e4979ccc4d50296bb91c11f5f40b28a6e34" solver-name = "gps-cdcl" solver-version = 1 diff --git a/console/console.pb.go b/console/console.pb.go index 96f1abfe1..f70b2c109 100644 --- a/console/console.pb.go +++ b/console/console.pb.go @@ -44,8 +44,14 @@ const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package type AccountExport struct { // The user's account details. Account *nakama_api.Account `protobuf:"bytes,1,opt,name=account" json:"account,omitempty"` + // The user's storage. + Objects []*nakama_api.StorageObject `protobuf:"bytes,2,rep,name=objects" json:"objects,omitempty"` + // The user's friends. + Friends []*nakama_api.Friend `protobuf:"bytes,3,rep,name=friends" json:"friends,omitempty"` // The user's groups. - Groups []*nakama_api.Group `protobuf:"bytes,2,rep,name=groups" json:"groups,omitempty"` + Groups []*nakama_api.Group `protobuf:"bytes,4,rep,name=groups" json:"groups,omitempty"` + // The user's notifications. + Notifications []*nakama_api.Notification `protobuf:"bytes,5,rep,name=notifications" json:"notifications,omitempty"` } func (m *AccountExport) Reset() { *m = AccountExport{} } @@ -60,6 +66,20 @@ func (m *AccountExport) GetAccount() *nakama_api.Account { return nil } +func (m *AccountExport) GetObjects() []*nakama_api.StorageObject { + if m != nil { + return m.Objects + } + return nil +} + +func (m *AccountExport) GetFriends() []*nakama_api.Friend { + if m != nil { + return m.Friends + } + return nil +} + func (m *AccountExport) GetGroups() []*nakama_api.Group { if m != nil { return m.Groups @@ -67,6 +87,13 @@ func (m *AccountExport) GetGroups() []*nakama_api.Group { return nil } +func (m *AccountExport) GetNotifications() []*nakama_api.Notification { + if m != nil { + return m.Notifications + } + return nil +} + // * // The identifier for a user account. type AccountIdRequest struct { @@ -287,40 +314,44 @@ var _Console_serviceDesc = grpc.ServiceDesc{ func init() { proto.RegisterFile("console/console.proto", fileDescriptor0) } var fileDescriptor0 = []byte{ - // 550 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x53, 0xc1, 0x6e, 0xd4, 0x3c, - 0x10, 0x56, 0x52, 0xb5, 0xfb, 0xff, 0xae, 0x5a, 0x15, 0xb7, 0xa5, 0x4b, 0x4a, 0xd5, 0x90, 0x22, - 0x54, 0x56, 0x34, 0x46, 0xe1, 0xd6, 0x13, 0xdb, 0x52, 0xa1, 0x4a, 0x80, 0xd0, 0xc2, 0x89, 0x0b, - 0xf2, 0x26, 0x43, 0xe2, 0x36, 0xeb, 0x49, 0x6d, 0xa7, 0x05, 0xa1, 0x5e, 0x78, 0x04, 0x78, 0x2e, - 0x4e, 0xbc, 0x02, 0x27, 0x5e, 0x80, 0x2b, 0x4a, 0xec, 0x2d, 0xdb, 0x5d, 0x81, 0x38, 0x44, 0xd6, - 0xcc, 0xf7, 0xcd, 0x7c, 0x5f, 0x66, 0x6c, 0xb2, 0x9e, 0xa2, 0xd4, 0x58, 0x02, 0x73, 0x67, 0x5c, - 0x29, 0x34, 0x48, 0x97, 0x25, 0x3f, 0xe5, 0x23, 0x1e, 0xbb, 0x6c, 0xd0, 0xcb, 0x85, 0x29, 0xea, - 0x61, 0x9c, 0xe2, 0x88, 0x15, 0xa0, 0x50, 0xa4, 0x25, 0x1f, 0x6a, 0x66, 0x59, 0x8c, 0x57, 0xa2, - 0xf9, 0x6c, 0x6d, 0x70, 0x3b, 0x47, 0xcc, 0x4b, 0xb0, 0x59, 0x29, 0xd1, 0x70, 0x23, 0x50, 0x6a, - 0x87, 0x6e, 0x3a, 0xb4, 0x8d, 0x86, 0xf5, 0x3b, 0x06, 0xa3, 0xca, 0x7c, 0x70, 0xe0, 0x83, 0xf6, - 0x48, 0xf7, 0x72, 0x90, 0x7b, 0xfa, 0x82, 0xe7, 0x39, 0x28, 0x86, 0x55, 0x5b, 0x3e, 0xdb, 0x2a, - 0x12, 0x64, 0xa9, 0x9f, 0xa6, 0x58, 0x4b, 0x73, 0xf4, 0xbe, 0x42, 0x65, 0xe8, 0x1e, 0xe9, 0x70, - 0x9b, 0xe8, 0x7a, 0xa1, 0xb7, 0xbb, 0x98, 0xac, 0xc6, 0xee, 0x3f, 0x1a, 0x77, 0x8e, 0x3b, 0x18, - 0x73, 0xe8, 0x7d, 0xb2, 0x90, 0x2b, 0xac, 0x2b, 0xdd, 0xf5, 0xc3, 0xb9, 0xdd, 0xc5, 0xe4, 0xc6, - 0x24, 0xfb, 0x69, 0x83, 0x0c, 0x1c, 0x21, 0x8a, 0xc8, 0x8a, 0x2b, 0x3f, 0xce, 0x06, 0x70, 0x56, - 0x83, 0x36, 0x74, 0x99, 0xf8, 0x22, 0x6b, 0x85, 0xfe, 0x1f, 0xf8, 0x22, 0x8b, 0x9e, 0x93, 0xd5, - 0x7e, 0x6d, 0x0a, 0x90, 0x46, 0xa4, 0xdc, 0xc0, 0x98, 0x16, 0x90, 0xff, 0x6a, 0x0d, 0x4a, 0xf2, - 0x11, 0x38, 0xf2, 0x55, 0xdc, 0x60, 0x15, 0xd7, 0xfa, 0x02, 0x55, 0xd6, 0xf5, 0x2d, 0x36, 0x8e, - 0xa3, 0x6d, 0xd2, 0x79, 0x05, 0x5a, 0x0b, 0x94, 0x74, 0x8d, 0xcc, 0x1b, 0x3c, 0x05, 0xe9, 0xea, - 0x6d, 0x90, 0x7c, 0xf5, 0x49, 0xe7, 0xd0, 0xee, 0x87, 0xa6, 0x64, 0xe9, 0x09, 0x94, 0x60, 0xc0, - 0xb9, 0xa4, 0x61, 0x7c, 0x7d, 0x83, 0xf1, 0xb4, 0xfd, 0xe0, 0x66, 0x6c, 0x37, 0x11, 0x8f, 0x37, - 0x11, 0x1f, 0x35, 0x9b, 0x88, 0xba, 0x9f, 0xbe, 0x7d, 0xff, 0xe2, 0xd3, 0xde, 0x0a, 0x3b, 0x4f, - 0x98, 0x1b, 0x15, 0xfb, 0x28, 0xb2, 0x4b, 0x7a, 0x46, 0x96, 0xec, 0xa0, 0xff, 0x5d, 0x64, 0xeb, - 0x0f, 0x0c, 0xdb, 0x27, 0xda, 0x6e, 0xb5, 0x6e, 0xd1, 0x8d, 0x69, 0x2d, 0x06, 0x76, 0xa3, 0x27, - 0x64, 0xfe, 0x19, 0xe6, 0x42, 0xd2, 0x9d, 0x99, 0x46, 0xb3, 0xa3, 0x0e, 0x36, 0xa6, 0x49, 0x6e, - 0x80, 0xd1, 0x4e, 0xab, 0xb3, 0x15, 0x75, 0x27, 0x75, 0xf8, 0x44, 0x87, 0x7d, 0xaf, 0x77, 0xf0, - 0xd3, 0xfb, 0xdc, 0xff, 0xe1, 0xd1, 0x4b, 0xb2, 0xfe, 0xa2, 0xed, 0x12, 0xba, 0x2e, 0x61, 0xff, - 0xe5, 0x71, 0x78, 0x9e, 0x44, 0x6f, 0xc9, 0x9d, 0xd7, 0x05, 0x84, 0x0e, 0x6c, 0xf4, 0x51, 0xe9, - 0xf0, 0x5e, 0x78, 0x88, 0xd2, 0x28, 0x31, 0xac, 0x0d, 0x2a, 0x4d, 0xef, 0x16, 0xc6, 0x54, 0x7a, - 0x9f, 0xb1, 0xbf, 0x3d, 0x98, 0x60, 0xad, 0x80, 0xb2, 0xc4, 0xc7, 0xbf, 0x81, 0x86, 0x97, 0xcc, - 0x25, 0xf1, 0xc3, 0x9e, 0xe7, 0x25, 0x2b, 0xbc, 0xaa, 0xca, 0xc6, 0x96, 0x40, 0xc9, 0x4e, 0x34, - 0xca, 0xfd, 0x99, 0x8c, 0x3a, 0x20, 0x3b, 0xce, 0x88, 0x06, 0x75, 0x0e, 0xea, 0xca, 0x6c, 0x86, - 0x69, 0x3d, 0x02, 0x69, 0x5f, 0x0a, 0xdd, 0x1c, 0xdb, 0xb9, 0x2e, 0xc5, 0x32, 0x4c, 0xf5, 0x9b, - 0x8e, 0xab, 0x19, 0x2e, 0xb4, 0x57, 0xe0, 0xd1, 0xaf, 0x00, 0x00, 0x00, 0xff, 0xff, 0x8c, 0xff, - 0x13, 0xd0, 0x0d, 0x04, 0x00, 0x00, + // 616 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x93, 0x41, 0x4f, 0xd4, 0x40, + 0x14, 0xc7, 0xb3, 0x45, 0x58, 0x1d, 0xb2, 0x04, 0x07, 0x90, 0xb2, 0x48, 0xa8, 0xc5, 0x18, 0xdc, + 0x40, 0xc7, 0x94, 0x1b, 0x07, 0xe3, 0x82, 0x68, 0x48, 0x14, 0x4d, 0xf1, 0xe4, 0xc5, 0x74, 0xdb, + 0x47, 0xb7, 0xd0, 0x9d, 0x57, 0x66, 0xa6, 0xa0, 0x31, 0x5c, 0x3c, 0x7b, 0xd2, 0xcf, 0xe5, 0xc9, + 0xaf, 0xe0, 0xc9, 0x2f, 0xe0, 0xd5, 0xb4, 0x33, 0xc5, 0xc2, 0x46, 0xe3, 0x61, 0xd3, 0xcc, 0xfc, + 0x7f, 0xff, 0xf7, 0xde, 0xcc, 0xfe, 0x87, 0x2c, 0x44, 0xc8, 0x25, 0x66, 0xc0, 0xcc, 0xd7, 0xcb, + 0x05, 0x2a, 0xa4, 0x33, 0x3c, 0x3c, 0x09, 0x47, 0xa1, 0x67, 0x76, 0xbb, 0xbd, 0x24, 0x55, 0xc3, + 0x62, 0xe0, 0x45, 0x38, 0x62, 0x43, 0x10, 0x98, 0x46, 0x59, 0x38, 0x90, 0x4c, 0x53, 0x2c, 0xcc, + 0xd3, 0xf2, 0xa7, 0xbd, 0xdd, 0xbb, 0x09, 0x62, 0x92, 0x81, 0xde, 0xe5, 0x1c, 0x55, 0xa8, 0x52, + 0xe4, 0xd2, 0xa8, 0xcb, 0x46, 0xad, 0x56, 0x83, 0xe2, 0x88, 0xc1, 0x28, 0x57, 0x1f, 0x8c, 0xb8, + 0x51, 0x7d, 0xa2, 0xcd, 0x04, 0xf8, 0xa6, 0x3c, 0x0f, 0x93, 0x04, 0x04, 0xc3, 0xbc, 0xb2, 0x8f, + 0x97, 0x72, 0x3f, 0x5b, 0xa4, 0xd3, 0x8f, 0x22, 0x2c, 0xb8, 0xda, 0x7b, 0x9f, 0xa3, 0x50, 0x74, + 0x93, 0xb4, 0x43, 0xbd, 0x61, 0xb7, 0x9c, 0xd6, 0xfa, 0xb4, 0x3f, 0xe7, 0x99, 0x83, 0x94, 0xe3, + 0x19, 0x36, 0xa8, 0x19, 0xba, 0x45, 0xda, 0x38, 0x38, 0x86, 0x48, 0x49, 0xdb, 0x72, 0x26, 0xd6, + 0xa7, 0xfd, 0xa5, 0x26, 0x7e, 0xa8, 0x50, 0x84, 0x09, 0xbc, 0xaa, 0x88, 0xa0, 0x26, 0xe9, 0x06, + 0x69, 0x1f, 0x89, 0x14, 0x78, 0x2c, 0xed, 0x89, 0xca, 0x44, 0x9b, 0xa6, 0x67, 0x95, 0x14, 0xd4, + 0x08, 0x7d, 0x48, 0xa6, 0x12, 0x81, 0x45, 0x2e, 0xed, 0x1b, 0x15, 0x7c, 0xbb, 0x09, 0x3f, 0x2f, + 0x95, 0xc0, 0x00, 0xf4, 0x31, 0xe9, 0x70, 0x54, 0xe9, 0x51, 0x1a, 0xe9, 0x53, 0xda, 0x93, 0x95, + 0xc3, 0x6e, 0x3a, 0x0e, 0x1a, 0x40, 0x70, 0x15, 0x77, 0x5d, 0x32, 0x6b, 0x4e, 0xb8, 0x1f, 0x07, + 0x70, 0x5a, 0x80, 0x54, 0x74, 0x86, 0x58, 0x69, 0x5c, 0xdd, 0xc5, 0xad, 0xc0, 0x4a, 0x63, 0xf7, + 0x25, 0x99, 0xeb, 0x17, 0x6a, 0x08, 0x5c, 0x95, 0x36, 0xa8, 0xb1, 0x2e, 0xb9, 0x59, 0x48, 0x10, + 0x3c, 0x1c, 0x81, 0x81, 0x2f, 0xd7, 0xa5, 0x96, 0x87, 0x52, 0x9e, 0xa3, 0x88, 0x6d, 0x4b, 0x6b, + 0xf5, 0xda, 0x5d, 0x25, 0xed, 0x43, 0x90, 0x32, 0x45, 0x4e, 0xe7, 0xc9, 0xa4, 0xc2, 0x13, 0xe0, + 0xc6, 0xaf, 0x17, 0xfe, 0x37, 0x8b, 0xb4, 0x77, 0x75, 0x86, 0x68, 0x44, 0x3a, 0x4f, 0x21, 0x03, + 0x05, 0x66, 0x4a, 0xea, 0x78, 0x57, 0x53, 0xe6, 0x5d, 0x1f, 0xbf, 0x7b, 0xc7, 0xd3, 0x69, 0xf1, + 0xea, 0xb4, 0x78, 0x7b, 0x65, 0x5a, 0x5c, 0xfb, 0xd3, 0xf7, 0x1f, 0x5f, 0x2d, 0xda, 0x9b, 0x65, + 0x67, 0x3e, 0x33, 0xff, 0x26, 0xfb, 0x98, 0xc6, 0x17, 0xf4, 0x94, 0x74, 0x74, 0x16, 0xfe, 0xbf, + 0xc9, 0xca, 0x5f, 0x08, 0x5d, 0xc7, 0x5d, 0xad, 0x7a, 0x2d, 0xd1, 0xc5, 0xeb, 0xbd, 0x18, 0xe8, + 0xd0, 0x1d, 0x93, 0xc9, 0x17, 0x98, 0xa4, 0x9c, 0xae, 0x8d, 0x15, 0x1a, 0xbf, 0xea, 0xee, 0xe2, + 0x75, 0xc8, 0x5c, 0xa0, 0xbb, 0x56, 0xf5, 0x59, 0x71, 0xed, 0x66, 0x9f, 0xb0, 0x51, 0x61, 0xbb, + 0xd5, 0xdb, 0xf9, 0xd5, 0xfa, 0xd2, 0xff, 0xd9, 0xa2, 0x17, 0x64, 0xe1, 0xa0, 0xaa, 0xe2, 0x98, + 0x2a, 0x4e, 0xff, 0xf5, 0xbe, 0x73, 0xe6, 0xbb, 0xef, 0xc8, 0xbd, 0x37, 0x43, 0x70, 0x8c, 0x58, + 0xf6, 0x47, 0x21, 0x9d, 0x07, 0xce, 0x2e, 0x72, 0x25, 0xd2, 0x41, 0xa1, 0x50, 0x48, 0x7a, 0x7f, + 0xa8, 0x54, 0x2e, 0xb7, 0x19, 0xfb, 0xd7, 0xa3, 0xee, 0xce, 0x0f, 0x21, 0xcb, 0xf0, 0xc9, 0x1f, + 0xa1, 0xe4, 0xfc, 0x09, 0xdf, 0x7b, 0xd4, 0x6b, 0xb5, 0xfc, 0xd9, 0x30, 0xcf, 0x33, 0x13, 0x3d, + 0x76, 0x2c, 0x91, 0x6f, 0x8f, 0xed, 0x88, 0x1d, 0xb2, 0x66, 0x06, 0x91, 0x20, 0xce, 0x40, 0x5c, + 0x0e, 0x1b, 0x63, 0x54, 0x8c, 0x80, 0xeb, 0xd7, 0x4c, 0x97, 0xeb, 0x71, 0xae, 0xb6, 0x62, 0x31, + 0x46, 0xf2, 0x6d, 0xdb, 0x78, 0x06, 0x53, 0x55, 0x04, 0xb6, 0x7e, 0x07, 0x00, 0x00, 0xff, 0xff, + 0xba, 0xdd, 0x43, 0x43, 0xb1, 0x04, 0x00, 0x00, } diff --git a/console/console.proto b/console/console.proto index 9dfc01bc4..4f6d3e229 100644 --- a/console/console.proto +++ b/console/console.proto @@ -74,8 +74,14 @@ service Console { message AccountExport { // The user's account details. nakama.api.Account account = 1; + // The user's storage. + repeated nakama.api.StorageObject objects = 2; + // The user's friends. + repeated nakama.api.Friend friends = 3; // The user's groups. - repeated nakama.api.Group groups = 2; + repeated nakama.api.Group groups = 4; + // The user's notifications. + repeated nakama.api.Notification notifications = 5; } /** diff --git a/console/console.swagger.json b/console/console.swagger.json index 52a9c01ca..fec19ceb0 100644 --- a/console/console.swagger.json +++ b/console/console.swagger.json @@ -142,6 +142,21 @@ }, "description": "Send a device to the server. Used with authenticate/link/unlink and user." }, + "apiFriend": { + "type": "object", + "properties": { + "user": { + "$ref": "#/definitions/apiUser", + "description": "The user object." + }, + "state": { + "type": "integer", + "format": "int32", + "description": "The friend status." + } + }, + "description": "A friend of a user." + }, "apiGroup": { "type": "object", "properties": { @@ -196,6 +211,89 @@ }, "description": "A group in the server." }, + "apiNotification": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "ID of the Notification." + }, + "subject": { + "type": "string", + "description": "Subject of the notification." + }, + "content": { + "type": "string", + "description": "Content of the notification in JSON." + }, + "code": { + "type": "integer", + "format": "int32", + "description": "Category code for this notification." + }, + "sender_id": { + "type": "string", + "description": "ID of the sender, if a user. Otherwise 'null'." + }, + "create_time": { + "type": "string", + "format": "date-time", + "description": "The UNIX time when the notification was created." + }, + "persistent": { + "type": "boolean", + "format": "boolean", + "description": "True if this notification was persisted to the database." + } + }, + "description": "A notification in the server." + }, + "apiStorageObject": { + "type": "object", + "properties": { + "collection": { + "type": "string", + "description": "The collection which stores the object." + }, + "key": { + "type": "string", + "description": "The key of the object within the collection." + }, + "user_id": { + "type": "string", + "description": "The user owner of the object." + }, + "value": { + "type": "string", + "description": "The value of the object." + }, + "version": { + "type": "string", + "description": "The version hash of the object." + }, + "permission_read": { + "type": "integer", + "format": "int32", + "description": "The read access permissions for the object." + }, + "permission_write": { + "type": "integer", + "format": "int32", + "description": "The write access permissions for the object." + }, + "create_time": { + "type": "string", + "format": "date-time", + "description": "The UNIX time when the object was created." + }, + "update_time": { + "type": "string", + "format": "date-time", + "description": "The UNIX time when the object was last updated." + } + }, + "description": "An object within the storage engine." + }, "apiUser": { "type": "object", "properties": { @@ -277,12 +375,33 @@ "$ref": "#/definitions/apiAccount", "description": "The user's account details." }, + "objects": { + "type": "array", + "items": { + "$ref": "#/definitions/apiStorageObject" + }, + "description": "The user's storage." + }, + "friends": { + "type": "array", + "items": { + "$ref": "#/definitions/apiFriend" + }, + "description": "The user's friends." + }, "groups": { "type": "array", "items": { "$ref": "#/definitions/apiGroup" }, "description": "The user's groups." + }, + "notifications": { + "type": "array", + "items": { + "$ref": "#/definitions/apiNotification" + }, + "description": "The user's notifications." } }, "description": "*\nAn export of all information stored for a user account." diff --git a/main.go b/main.go index 57bc6e333..5c65dccba 100644 --- a/main.go +++ b/main.go @@ -27,6 +27,9 @@ import ( "syscall" "time" + "io/ioutil" + "path/filepath" + "github.com/golang/protobuf/jsonpb" "github.com/heroiclabs/nakama/ga" "github.com/heroiclabs/nakama/migrate" @@ -35,8 +38,6 @@ import ( _ "github.com/lib/pq" "github.com/satori/go.uuid" "go.uber.org/zap" - "io/ioutil" - "path/filepath" ) const cookieFilename = ".cookie" @@ -115,6 +116,8 @@ func main() { runtimePool := server.NewRuntimePool(jsonLogger, multiLogger, db, config, socialClient, sessionRegistry, matchRegistry, tracker, router, stdLibs, modules, regCallbacks, once) pipeline := server.NewPipeline(config, db, jsonpbMarshaler, jsonpbUnmarshaler, sessionRegistry, matchRegistry, tracker, router, runtimePool) metrics := server.NewMetrics(multiLogger, config) + + consoleServer := server.StartConsoleServer(jsonLogger, multiLogger, config, db) apiServer := server.StartApiServer(jsonLogger, multiLogger, db, jsonpbMarshaler, jsonpbUnmarshaler, config, socialClient, sessionRegistry, matchRegistry, tracker, router, pipeline, runtimePool) gaenabled := len(os.Getenv("NAKAMA_TELEMETRY")) < 1 @@ -136,6 +139,7 @@ func main() { // Gracefully stop server components. apiServer.Stop() + consoleServer.Stop() metrics.Stop(jsonLogger) matchRegistry.Stop() tracker.Stop() diff --git a/migrate/migrate-packr.go b/migrate/migrate-packr.go index 6a7174d59..6ed4af2e2 100644 --- a/migrate/migrate-packr.go +++ b/migrate/migrate-packr.go @@ -9,5 +9,5 @@ import "github.com/gobuffalo/packr" // Go binary. You can use the "packr clean" command to clean up this, // and any other packr generated files. func init() { - packr.PackJSONBytes("./sql", "20180103142001_initial_schema.sql", "\"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\"") -} + packr.PackJSONBytes("./sql", "20180103142001_initial_schema.sql", "\"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\"") + } diff --git a/migrate/sql/20180103142001_initial_schema.sql b/migrate/sql/20180103142001_initial_schema.sql index 91c5f7ce3..898e305b1 100644 --- a/migrate/sql/20180103142001_initial_schema.sql +++ b/migrate/sql/20180103142001_initial_schema.sql @@ -168,7 +168,15 @@ CREATE TABLE IF NOT EXISTS wallet_ledger ( update_time TIMESTAMPTZ DEFAULT now() NOT NULL ); +CREATE TABLE IF NOT EXISTS user_tombstone ( + PRIMARY KEY (create_time, user_id), + + user_id UUID UNIQUE NOT NULL, + create_time TIMESTAMPTZ DEFAULT now() NOT NULL +); + -- +migrate Down +DROP TABLE IF EXISTS user_tombstone; DROP TABLE IF EXISTS wallet_ledger; DROP TABLE IF EXISTS leaderboard_record; DROP TABLE IF EXISTS leaderboard; diff --git a/server/api.go b/server/api.go index 21893825b..18a530917 100644 --- a/server/api.go +++ b/server/api.go @@ -68,7 +68,7 @@ func StartApiServer(logger *zap.Logger, multiLogger *zap.Logger, db *sql.DB, jso serverOpts := []grpc.ServerOption{ grpc.StatsHandler(&ocgrpc.ServerHandler{IsPublicEndpoint: true}), grpc.MaxRecvMsgSize(int(config.GetSocket().MaxMessageSizeBytes)), - grpc.UnaryInterceptor(interceptorFunc(logger, config, runtimePool, jsonpbMarshaler, jsonpbUnmarshaler)), + grpc.UnaryInterceptor(apiInterceptorFunc(logger, config, runtimePool, jsonpbMarshaler, jsonpbUnmarshaler)), } if config.GetSocket().TLSCert != nil { serverOpts = append(serverOpts, grpc.Creds(credentials.NewServerTLSFromCert(&config.GetSocket().TLSCert[0]))) @@ -173,7 +173,7 @@ func (s *ApiServer) Healthcheck(ctx context.Context, in *empty.Empty) (*empty.Em return &empty.Empty{}, nil } -func interceptorFunc(logger *zap.Logger, config Config, runtimePool *RuntimePool, jsonpbMarshaler *jsonpb.Marshaler, jsonpbUnmarshaler *jsonpb.Unmarshaler) func(context.Context, interface{}, *grpc.UnaryServerInfo, grpc.UnaryHandler) (interface{}, error) { +func apiInterceptorFunc(logger *zap.Logger, config Config, runtimePool *RuntimePool, jsonpbMarshaler *jsonpb.Marshaler, jsonpbUnmarshaler *jsonpb.Unmarshaler) func(context.Context, interface{}, *grpc.UnaryServerInfo, grpc.UnaryHandler) (interface{}, error) { return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { ctx, err := securityInterceptorFunc(logger, config, ctx, req, info) if err != nil { diff --git a/server/config.go b/server/config.go index 2bd9cc488..5879a261f 100644 --- a/server/config.go +++ b/server/config.go @@ -25,6 +25,7 @@ import ( "github.com/heroiclabs/nakama/flags" "crypto/tls" + "github.com/go-yaml/yaml" "go.uber.org/zap" ) @@ -40,6 +41,7 @@ type Config interface { GetDatabase() *DatabaseConfig GetSocial() *SocialConfig GetRuntime() *RuntimeConfig + GetConsole() *ConsoleConfig } func ParseArgs(logger *zap.Logger, args []string) Config { @@ -165,6 +167,7 @@ type config struct { Database *DatabaseConfig `yaml:"database" json:"database" usage:"Database connection settings."` Social *SocialConfig `yaml:"social" json:"social" usage:"Properties for social provider integrations."` Runtime *RuntimeConfig `yaml:"runtime" json:"runtime" usage:"Script Runtime properties."` + Console *ConsoleConfig `yaml:"console" json:"console" usage:"Console settings."` } // NewConfig constructs a Config struct which represents server settings, and populates it with default values. @@ -183,6 +186,7 @@ func NewConfig(logger *zap.Logger) *config { Database: NewDatabaseConfig(), Social: NewSocialConfig(), Runtime: NewRuntimeConfig(), + Console: NewConsoleConfig(), } } @@ -222,6 +226,10 @@ func (c *config) GetRuntime() *RuntimeConfig { return c.Runtime } +func (c *config) GetConsole() *ConsoleConfig { + return c.Console +} + // LogConfig is configuration relevant to logging levels and output. type LogConfig struct { // By default, log all messages with Info and higher levels to a log file inside Data/Log/.log file. The content will be in JSON. @@ -364,3 +372,19 @@ func NewRuntimeConfig() *RuntimeConfig { HTTPKey: "defaultkey", } } + +// ConsoleConfig is configuration relevant to the embedded console. +type ConsoleConfig struct { + Port int `yaml:"port" json:"port" usage:"The port for accepting connections for the embedded console, listening on all interfaces."` + Username string `yaml:"username" json:"username" usage:"Username for the embedded console."` + Password string `yaml:"password" json:"password" usage:"Password for the embedded console."` +} + +// NewConsoleConfig creates a new ConsoleConfig struct. +func NewConsoleConfig() *ConsoleConfig { + return &ConsoleConfig{ + Port: 7351, + Username: "admin", + Password: "password", + } +} diff --git a/server/console.go b/server/console.go new file mode 100644 index 000000000..db6166277 --- /dev/null +++ b/server/console.go @@ -0,0 +1,159 @@ +// Copyright 2018 The Nakama Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package server + +import ( + "context" + "database/sql" + "fmt" + "net" + "net/http" + "time" + + "github.com/gorilla/handlers" + "github.com/gorilla/mux" + "github.com/grpc-ecosystem/grpc-gateway/runtime" + "github.com/heroiclabs/nakama/console" + "go.opencensus.io/plugin/ocgrpc" + "go.uber.org/zap" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" +) + +type ConsoleServer struct { + logger *zap.Logger + db *sql.DB + config Config + grpcServer *grpc.Server + grpcGatewayServer *http.Server +} + +func StartConsoleServer(logger *zap.Logger, multiLogger *zap.Logger, config Config, db *sql.DB) *ConsoleServer { + serverOpts := []grpc.ServerOption{ + grpc.StatsHandler(&ocgrpc.ServerHandler{IsPublicEndpoint: true}), + grpc.MaxRecvMsgSize(int(config.GetSocket().MaxMessageSizeBytes)), + grpc.UnaryInterceptor(consoleInterceptorFunc(logger, config)), + } + grpcServer := grpc.NewServer(serverOpts...) + + s := &ConsoleServer{ + logger: logger, + db: db, + config: config, + grpcServer: grpcServer, + } + + console.RegisterConsoleServer(grpcServer, s) + multiLogger.Info("Starting Console server for gRPC requests", zap.Int("port", config.GetSocket().Port-2)) + go func() { + listener, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", config.GetSocket().Port-2)) + if err != nil { + multiLogger.Fatal("Console server listener failed to start", zap.Error(err)) + } + + if err := grpcServer.Serve(listener); err != nil { + multiLogger.Fatal("Console server listener failed", zap.Error(err)) + } + }() + + ctx := context.Background() + grpcGateway := runtime.NewServeMux() + dialAddr := fmt.Sprintf("127.0.0.1:%d", config.GetSocket().Port-2) + dialOpts := []grpc.DialOption{ + //TODO (mo, zyro): Do we need to pass the statsHandler here as well? + grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(int(config.GetSocket().MaxMessageSizeBytes))), + grpc.WithInsecure(), + } + + if err := console.RegisterConsoleHandlerFromEndpoint(ctx, grpcGateway, dialAddr, dialOpts); err != nil { + multiLogger.Fatal("Console server gateway registration failed", zap.Error(err)) + } + + grpcGatewayRouter := mux.NewRouter() + grpcGatewayRouter.NewRoute().Handler(grpcGateway) + //TODO server HTML content here. + grpcGatewayRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) }).Methods("GET") + // Enable compression on gateway responses. + handlerWithGzip := handlers.CompressHandler(grpcGatewayRouter) + + // Enable CORS on all requests. + CORSHeaders := handlers.AllowedHeaders([]string{"Authorization", "Content-Type", "User-Agent"}) + CORSOrigins := handlers.AllowedOrigins([]string{"*"}) + CORSMethods := handlers.AllowedMethods([]string{"GET", "HEAD", "POST", "PUT", "DELETE", "PATCH"}) + handlerWithCORS := handlers.CORS(CORSHeaders, CORSOrigins, CORSMethods)(handlerWithGzip) + + // Set up and start GRPC Gateway server. + s.grpcGatewayServer = &http.Server{ + Addr: fmt.Sprintf(":%d", config.GetConsole().Port), + ReadTimeout: time.Millisecond * time.Duration(int64(config.GetSocket().ReadTimeoutMs)), + WriteTimeout: time.Millisecond * time.Duration(int64(config.GetSocket().WriteTimeoutMs)), + IdleTimeout: time.Millisecond * time.Duration(int64(config.GetSocket().IdleTimeoutMs)), + Handler: handlerWithCORS, + } + + multiLogger.Info("Starting Console server gateway for HTTP requests", zap.Int("port", config.GetConsole().Port)) + go func() { + if err := s.grpcGatewayServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { + multiLogger.Fatal("Console server gateway listener failed", zap.Error(err)) + } + }() + + return s +} + +func (s *ConsoleServer) Stop() { + // 1. Stop GRPC Gateway server first as it sits above GRPC server. + if err := s.grpcGatewayServer.Shutdown(context.Background()); err != nil { + s.logger.Error("API server gateway listener shutdown failed", zap.Error(err)) + } + // 2. Stop GRPC server. + s.grpcServer.GracefulStop() +} + +func consoleInterceptorFunc(logger *zap.Logger, config Config) func(context.Context, interface{}, *grpc.UnaryServerInfo, grpc.UnaryHandler) (interface{}, error) { + return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + logger.Error("Cannot extract metadata from incoming context") + return nil, status.Error(codes.FailedPrecondition, "Cannot extract metadata from incoming context") + } + auth, ok := md["authorization"] + if !ok { + auth, ok = md["grpcgateway-authorization"] + } + if !ok { + return nil, status.Error(codes.Unauthenticated, "Console authentication required.") + } + if len(auth) != 1 { + return nil, status.Error(codes.Unauthenticated, "Console authentication required.") + } + username, password, ok := parseBasicAuth(auth[0]) + if !ok { + return nil, status.Error(codes.Unauthenticated, "Console authentication invalid.") + } + if username != config.GetConsole().Username || password != config.GetConsole().Password { + return nil, status.Error(codes.Unauthenticated, "Console authentication invalid.") + } + + return handler(ctx, req) + } +} + +func (s *ConsoleServer) Login(context.Context, *console.AuthenticateRequest) (*console.Session, error) { + return nil, nil +} diff --git a/server/console_gdpr.go b/server/console_gdpr.go new file mode 100644 index 000000000..d6b5f1e9c --- /dev/null +++ b/server/console_gdpr.go @@ -0,0 +1,90 @@ +// Copyright 2018 The Nakama Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package server + +import ( + "context" + + "github.com/golang/protobuf/ptypes/empty" + "github.com/heroiclabs/nakama/console" + "github.com/satori/go.uuid" + "go.uber.org/zap" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func (s *ConsoleServer) DeleteAccount(ctx context.Context, in *console.AccountIdRequest) (*empty.Empty, error) { + userID := uuid.FromStringOrNil(in.Id) + if uuid.Equal(uuid.Nil, userID) { + return nil, status.Error(codes.InvalidArgument, "Invalid user ID was provided.") + } + + count, err := DeleteUser(s.db, userID) + if err != nil { + s.logger.Error("Could not delete user", zap.Error(err), zap.String("user_id", in.Id)) + return nil, status.Error(codes.Internal, "An error occurred while trying to delete the user.") + } else if count == 0 { + s.logger.Info("No user was found to delete. Skipping blacklist.", zap.String("user_id", in.Id)) + return &empty.Empty{}, nil + } + + if _, err = s.db.Exec(`INSERT INTO user_tombstone (user_id) VALUES ($1) ON CONFLICT(user_id) DO NOTHING`, userID); err != nil { + s.logger.Error("Could not insert user ID into tombstone", zap.Error(err), zap.String("user_id", in.Id)) + return nil, status.Error(codes.Internal, "An error occurred while trying to delete the user.") + } + + return &empty.Empty{}, nil +} + +func (s *ConsoleServer) ExportAccount(ctx context.Context, in *console.AccountIdRequest) (*console.AccountExport, error) { + userID := uuid.FromStringOrNil(in.Id) + if uuid.Equal(uuid.Nil, userID) { + return nil, status.Error(codes.InvalidArgument, "Invalid user ID was provided.") + } + + account, err := GetAccount(s.db, s.logger, nil, userID) + if err != nil { + s.logger.Error("Could not export account data", zap.Error(err), zap.String("user_id", in.Id)) + return nil, status.Error(codes.Internal, "An error occurred while trying to export user data.") + } + + friends, err := GetFriendIDs(s.logger, s.db, userID) + if err != nil { + s.logger.Error("Could not fetch friend IDs", zap.Error(err), zap.String("user_id", in.Id)) + return nil, status.Error(codes.Internal, "An error occurred while trying to export user data.") + } + + notifications, err := NotificationList(s.logger, s.db, userID, 0, "", nil) + if err != nil { + s.logger.Error("Could not fetch notifications", zap.Error(err), zap.String("user_id", in.Id)) + return nil, status.Error(codes.Internal, "An error occurred while trying to export user data.") + } + + storageObjects, err := StorageReadAllUserObjects(s.logger, s.db, userID) + if err != nil { + s.logger.Error("Could not fetch notifications", zap.Error(err), zap.String("user_id", in.Id)) + return nil, status.Error(codes.Internal, "An error occurred while trying to export user data.") + } + + // TODO(mo, zyro) add wallet, groups, chat messages, leaderboard and leaderboard records + export := &console.AccountExport{ + Account: account, + Objects: storageObjects, + Friends: friends.GetFriends(), + Notifications: notifications.GetNotifications(), + } + + return export, nil +} diff --git a/server/core_account.go b/server/core_account.go index 37e920bd1..acf8d0bb8 100644 --- a/server/core_account.go +++ b/server/core_account.go @@ -90,6 +90,11 @@ WHERE id = $1` verifyTimestamp = ×tamp.Timestamp{Seconds: verifyTime.Time.Unix()} } + online := false + if tracker != nil { + online = tracker.StreamExists(PresenceStream{Mode: StreamModeNotifications, Subject: userID}) + } + return &api.Account{ User: &api.User{ Id: userID.String(), @@ -107,7 +112,7 @@ WHERE id = $1` EdgeCount: int32(edge_count), CreateTime: ×tamp.Timestamp{Seconds: createTime.Time.Unix()}, UpdateTime: ×tamp.Timestamp{Seconds: updateTime.Time.Unix()}, - Online: tracker.StreamExists(PresenceStream{Mode: StreamModeNotifications, Subject: userID}), + Online: online, }, Wallet: wallet.String, Email: email.String, diff --git a/server/core_friend.go b/server/core_friend.go index 04d39c936..4c2e6b654 100644 --- a/server/core_friend.go +++ b/server/core_friend.go @@ -28,6 +28,47 @@ import ( "go.uber.org/zap" ) +func GetFriendIDs(logger *zap.Logger, db *sql.DB, userID uuid.UUID) (*api.Friends, error) { + query := ` +SELECT id, state +FROM users, user_edge WHERE id = destination_id AND source_id = $1` + + rows, err := db.Query(query, userID) + if err != nil { + logger.Error("Error retrieving friends.", zap.Error(err)) + return nil, err + } + defer rows.Close() + + friends := make([]*api.Friend, 0) + + for rows.Next() { + var id string + var state sql.NullInt64 + + if err = rows.Scan(&id, &state); err != nil { + logger.Error("Error retrieving friend IDs.", zap.Error(err)) + return nil, err + } + + friendID := uuid.FromStringOrNil(id) + user := &api.User{ + Id: friendID.String(), + } + + friends = append(friends, &api.Friend{ + User: user, + State: int32(state.Int64), + }) + } + if err = rows.Err(); err != nil { + logger.Error("Error retrieving friend IDs.", zap.Error(err)) + return nil, err + } + + return &api.Friends{Friends: friends}, nil +} + func GetFriends(logger *zap.Logger, db *sql.DB, tracker Tracker, userID uuid.UUID) (*api.Friends, error) { query := ` SELECT id, username, display_name, avatar_url, @@ -63,6 +104,11 @@ FROM users, user_edge WHERE id = destination_id AND source_id = $1` } friendID := uuid.FromStringOrNil(id) + online := false + if tracker != nil { + online = tracker.StreamExists(PresenceStream{Mode: StreamModeNotifications, Subject: friendID}) + } + user := &api.User{ Id: friendID.String(), Username: username.String, @@ -74,7 +120,7 @@ FROM users, user_edge WHERE id = destination_id AND source_id = $1` Metadata: string(metadata), CreateTime: ×tamp.Timestamp{Seconds: createTime.Time.Unix()}, UpdateTime: ×tamp.Timestamp{Seconds: updateTime.Time.Unix()}, - Online: tracker.StreamExists(PresenceStream{Mode: StreamModeNotifications, Subject: friendID}), + Online: online, } friends = append(friends, &api.Friend{ diff --git a/server/core_notification.go b/server/core_notification.go index e968405fa..a72853763 100644 --- a/server/core_notification.go +++ b/server/core_notification.go @@ -81,7 +81,14 @@ func NotificationSend(logger *zap.Logger, db *sql.DB, messageRouter MessageRoute } func NotificationList(logger *zap.Logger, db *sql.DB, userID uuid.UUID, limit int, cursor string, nc *notificationCacheableCursor) (*api.NotificationList, error) { - params := []interface{}{userID, limit} + params := []interface{}{userID} + + limitQuery := " " + if limit > 0 { + params = append(params, limit) + limitQuery = " LIMIT $2" + } + cursorQuery := " " if nc != nil && nc.NotificationID != nil { cursorQuery = " AND (user_id, create_time, id) > ($1::UUID, $3, $4::UUID)" @@ -92,8 +99,7 @@ func NotificationList(logger *zap.Logger, db *sql.DB, userID uuid.UUID, limit in SELECT id, subject, content, code, sender_id, create_time FROM notification WHERE user_id = $1`+cursorQuery+` -ORDER BY create_time ASC -LIMIT $2`, params...) +ORDER BY create_time ASC`+limitQuery, params...) if err != nil { logger.Error("Could not retrieve notifications.", zap.Error(err)) diff --git a/server/core_storage.go b/server/core_storage.go index 76a317e4a..b3bf7bc7c 100644 --- a/server/core_storage.go +++ b/server/core_storage.go @@ -181,6 +181,48 @@ LIMIT $3` return objects, err } +func StorageReadAllUserObjects(logger *zap.Logger, db *sql.DB, userID uuid.UUID) ([]*api.StorageObject, error) { + query := ` +SELECT collection, key, user_id, value, version, read, write, create_time, update_time +FROM storage +WHERE user_id = $1` + + rows, err := db.Query(query, userID) + if err != nil { + if err == sql.ErrNoRows { + return make([]*api.StorageObject, 0), nil + } else { + logger.Error("Could not read storage objects.", zap.Error(err), zap.String("user_id", userID.String())) + return nil, err + } + } + + defer rows.Close() + objects := make([]*api.StorageObject, 0) + for rows.Next() { + o := &api.StorageObject{CreateTime: ×tamp.Timestamp{}, UpdateTime: ×tamp.Timestamp{}} + var createTime pq.NullTime + var updateTime pq.NullTime + var userID sql.NullString + if err := rows.Scan(&o.Collection, &o.Key, &userID, &o.Value, &o.Version, &o.PermissionRead, &o.PermissionWrite, &createTime, &updateTime); err != nil { + return nil, err + } + + o.CreateTime.Seconds = createTime.Time.Unix() + o.UpdateTime.Seconds = updateTime.Time.Unix() + + o.UserId = userID.String + objects = append(objects, o) + } + + if rows.Err() != nil { + logger.Error("Could not read storage objects.", zap.Error(err), zap.String("user_id", userID.String())) + return nil, rows.Err() + } + + return objects, err +} + func storageListObjects(rows *sql.Rows, cursor string) (*api.StorageObjectList, error) { objects := make([]*api.StorageObject, 0) for rows.Next() { diff --git a/server/core_user.go b/server/core_user.go index 610575ee8..488bc3ea6 100644 --- a/server/core_user.go +++ b/server/core_user.go @@ -102,6 +102,15 @@ WHERE` return users, nil } +func DeleteUser(db *sql.DB, userID uuid.UUID) (int64, error) { + res, err := db.Exec("DELETE FROM users WHERE id = $1", userID) + if err != nil { + return 0, err + } + + return res.RowsAffected() +} + func convertUser(tracker Tracker, rows *sql.Rows) (*api.User, error) { var id string var displayName sql.NullString -- GitLab