diff --git a/api4/user.go b/api4/user.go index 43a40cfd09..770f3e5a9c 100644 --- a/api4/user.go +++ b/api4/user.go @@ -119,6 +119,19 @@ func getUser(c *Context, w http.ResponseWriter, r *http.Request) { return } + if c.IsSystemAdmin() || c.App.Session.UserId == user.Id { + userTermsOfService, err := c.App.GetUserTermsOfService(user.Id) + if err != nil && err.StatusCode != http.StatusNotFound { + c.Err = err + return + } + + if userTermsOfService != nil { + user.TermsOfServiceId = userTermsOfService.TermsOfServiceId + user.TermsOfServiceCreateAt = userTermsOfService.CreateAt + } + } + etag := user.Etag(*c.App.Config().PrivacySettings.ShowFullName, *c.App.Config().PrivacySettings.ShowEmailAddress) if c.HandleEtag(etag, "Get User", w, r) { @@ -149,6 +162,19 @@ func getUserByUsername(c *Context, w http.ResponseWriter, r *http.Request) { return } + if c.IsSystemAdmin() || c.App.Session.UserId == user.Id { + userTermsOfService, err := c.App.GetUserTermsOfService(user.Id) + if err != nil && err.StatusCode != http.StatusNotFound { + c.Err = err + return + } + + if userTermsOfService != nil { + user.TermsOfServiceId = userTermsOfService.TermsOfServiceId + user.TermsOfServiceCreateAt = userTermsOfService.CreateAt + } + } + etag := user.Etag(*c.App.Config().PrivacySettings.ShowFullName, *c.App.Config().PrivacySettings.ShowEmailAddress) if c.HandleEtag(etag, "Get User", w, r) { diff --git a/api4/user_test.go b/api4/user_test.go index 80dad8f33a..905b12b12c 100644 --- a/api4/user_test.go +++ b/api4/user_test.go @@ -417,6 +417,105 @@ func TestGetUser(t *testing.T) { } } +func TestGetUserWithAcceptedTermsOfServiceForOtherUser(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + user := th.CreateUser() + + tos, _ := th.App.CreateTermsOfService("Dummy TOS", user.Id) + + th.App.UpdateUser(user, false) + + ruser, resp := th.Client.GetUser(user.Id, "") + CheckNoError(t, resp) + CheckUserSanitization(t, ruser) + + if ruser.Email != user.Email { + t.Fatal("emails did not match") + } + + assert.Empty(t, ruser.TermsOfServiceId) + + th.App.SaveUserTermsOfService(user.Id, tos.Id, true) + + ruser, resp = th.Client.GetUser(user.Id, "") + CheckNoError(t, resp) + CheckUserSanitization(t, ruser) + + if ruser.Email != user.Email { + t.Fatal("emails did not match") + } + + // user TOS data cannot be fetched for other users by non-admin users + assert.Empty(t, ruser.TermsOfServiceId) +} + +func TestGetUserWithAcceptedTermsOfService(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + user := th.BasicUser + + tos, _ := th.App.CreateTermsOfService("Dummy TOS", user.Id) + + ruser, resp := th.Client.GetUser(user.Id, "") + CheckNoError(t, resp) + CheckUserSanitization(t, ruser) + + if ruser.Email != user.Email { + t.Fatal("emails did not match") + } + + assert.Empty(t, ruser.TermsOfServiceId) + + th.App.SaveUserTermsOfService(user.Id, tos.Id, true) + + ruser, resp = th.Client.GetUser(user.Id, "") + CheckNoError(t, resp) + CheckUserSanitization(t, ruser) + + if ruser.Email != user.Email { + t.Fatal("emails did not match") + } + + // a user can view their own TOS details + assert.Equal(t, tos.Id, ruser.TermsOfServiceId) +} + +func TestGetUserWithAcceptedTermsOfServiceWithAdminUser(t *testing.T) { + th := Setup().InitBasic() + th.LoginSystemAdmin() + defer th.TearDown() + + user := th.BasicUser + + tos, _ := th.App.CreateTermsOfService("Dummy TOS", user.Id) + + ruser, resp := th.SystemAdminClient.GetUser(user.Id, "") + CheckNoError(t, resp) + CheckUserSanitization(t, ruser) + + if ruser.Email != user.Email { + t.Fatal("emails did not match") + } + + assert.Empty(t, ruser.TermsOfServiceId) + + th.App.SaveUserTermsOfService(user.Id, tos.Id, true) + + ruser, resp = th.SystemAdminClient.GetUser(user.Id, "") + CheckNoError(t, resp) + CheckUserSanitization(t, ruser) + + if ruser.Email != user.Email { + t.Fatal("emails did not match") + } + + // admin can view anyone's TOS details + assert.Equal(t, tos.Id, ruser.TermsOfServiceId) +} + func TestGetBotUser(t *testing.T) { th := Setup().InitBasic() defer th.TearDown() @@ -501,6 +600,36 @@ func TestGetUserByUsername(t *testing.T) { } } +func TestGetUserByUsernameWithAcceptedTermsOfService(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + user := th.BasicUser + + ruser, resp := th.Client.GetUserByUsername(user.Username, "") + CheckNoError(t, resp) + CheckUserSanitization(t, ruser) + + if ruser.Email != user.Email { + t.Fatal("emails did not match") + } + + tos, _ := th.App.CreateTermsOfService("Dummy TOS", user.Id) + th.App.SaveUserTermsOfService(ruser.Id, tos.Id, true) + + ruser, resp = th.Client.GetUserByUsername(user.Username, "") + CheckNoError(t, resp) + CheckUserSanitization(t, ruser) + + if ruser.Email != user.Email { + t.Fatal("emails did not match") + } + + if ruser.TermsOfServiceId != tos.Id { + t.Fatal("Terms of service ID didn't match") + } +} + func TestGetUserByEmail(t *testing.T) { th := Setup().InitBasic() defer th.TearDown() diff --git a/model/user.go b/model/user.go index a864ebd4ed..d8948eadbd 100644 --- a/model/user.go +++ b/model/user.go @@ -54,33 +54,35 @@ const ( ) type User struct { - Id string `json:"id"` - CreateAt int64 `json:"create_at,omitempty"` - UpdateAt int64 `json:"update_at,omitempty"` - DeleteAt int64 `json:"delete_at"` - Username string `json:"username"` - Password string `json:"password,omitempty"` - AuthData *string `json:"auth_data,omitempty"` - AuthService string `json:"auth_service"` - Email string `json:"email"` - EmailVerified bool `json:"email_verified,omitempty"` - Nickname string `json:"nickname"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - Position string `json:"position"` - Roles string `json:"roles"` - AllowMarketing bool `json:"allow_marketing,omitempty"` - Props StringMap `json:"props,omitempty"` - NotifyProps StringMap `json:"notify_props,omitempty"` - LastPasswordUpdate int64 `json:"last_password_update,omitempty"` - LastPictureUpdate int64 `json:"last_picture_update,omitempty"` - FailedAttempts int `json:"failed_attempts,omitempty"` - Locale string `json:"locale"` - Timezone StringMap `json:"timezone"` - MfaActive bool `json:"mfa_active,omitempty"` - MfaSecret string `json:"mfa_secret,omitempty"` - LastActivityAt int64 `db:"-" json:"last_activity_at,omitempty"` - IsBot bool `db:"-" json:"is_bot,omitempty"` + Id string `json:"id"` + CreateAt int64 `json:"create_at,omitempty"` + UpdateAt int64 `json:"update_at,omitempty"` + DeleteAt int64 `json:"delete_at"` + Username string `json:"username"` + Password string `json:"password,omitempty"` + AuthData *string `json:"auth_data,omitempty"` + AuthService string `json:"auth_service"` + Email string `json:"email"` + EmailVerified bool `json:"email_verified,omitempty"` + Nickname string `json:"nickname"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Position string `json:"position"` + Roles string `json:"roles"` + AllowMarketing bool `json:"allow_marketing,omitempty"` + Props StringMap `json:"props,omitempty"` + NotifyProps StringMap `json:"notify_props,omitempty"` + LastPasswordUpdate int64 `json:"last_password_update,omitempty"` + LastPictureUpdate int64 `json:"last_picture_update,omitempty"` + FailedAttempts int `json:"failed_attempts,omitempty"` + Locale string `json:"locale"` + Timezone StringMap `json:"timezone"` + MfaActive bool `json:"mfa_active,omitempty"` + MfaSecret string `json:"mfa_secret,omitempty"` + LastActivityAt int64 `db:"-" json:"last_activity_at,omitempty"` + IsBot bool `db:"-" json:"is_bot,omitempty"` + TermsOfServiceId string `db:"-" json:"terms_of_service_id,omitempty"` + TermsOfServiceCreateAt int64 `db:"-" json:"terms_of_service_create_at,omitempty"` } type UserPatch struct { @@ -372,7 +374,7 @@ func (u *UserAuth) ToJson() string { // Generate a valid strong etag so the browser can cache the results func (u *User) Etag(showFullName, showEmail bool) string { - return Etag(u.Id, u.UpdateAt, showFullName, showEmail) + return Etag(u.Id, u.UpdateAt, u.TermsOfServiceId, u.TermsOfServiceCreateAt, showFullName, showEmail) } // Remove any private data from the user object