Fix IM report and group permission regressions
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
//go:build mysql
|
||||
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/tinode/chat/server/store/types"
|
||||
)
|
||||
|
||||
func TestCanUseManagedReportsRequiresAllOrganizationVisibility(t *testing.T) {
|
||||
if !canUseManagedReports(organizationVisibility{Scope: orgVisibilityAll}) {
|
||||
t.Fatal("all visibility should be allowed to use managed report scope")
|
||||
}
|
||||
for _, scope := range []string{orgVisibilitySelf, orgVisibilityFriends, ""} {
|
||||
if canUseManagedReports(organizationVisibility{Scope: scope}) {
|
||||
t.Fatalf("scope %q should not be allowed to use managed report scope", scope)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReportMemberScopeWhereIncludesDepartmentAndVisibility(t *testing.T) {
|
||||
where, args := reportMemberScopeWhere(organizationVisibility{Scope: orgVisibilityFriends, UserID: 42}, "dep_ops")
|
||||
joined := strings.Join(where, " AND ")
|
||||
if !strings.Contains(joined, "m.department_id=?") {
|
||||
t.Fatalf("department filter missing from where: %s", joined)
|
||||
}
|
||||
if !strings.Contains(joined, "im_friendships") {
|
||||
t.Fatalf("friend visibility filter missing from where: %s", joined)
|
||||
}
|
||||
if len(args) != 4 || args[0] != "dep_ops" || args[1] != int64(42) || args[2] != int64(42) || args[3] != types.RelationshipFriend {
|
||||
t.Fatalf("unexpected args: %#v", args)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupAdminAccessModeDoesNotGrantOwner(t *testing.T) {
|
||||
mode := groupAdminAccessMode()
|
||||
if !mode.IsAdmin() {
|
||||
t.Fatalf("admin mode should grant admin access: %s", mode.String())
|
||||
}
|
||||
if mode.IsOwner() {
|
||||
t.Fatalf("admin mode must not grant owner access: %s", mode.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageSearchDoesNotClearScannedHead(t *testing.T) {
|
||||
source, err := os.ReadFile("tenant_adapter.go")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(string(source), "msg.Head = kvMapFromJSON(msg.Head)") {
|
||||
t.Fatal("message search must not clear Head after sqlx StructScan has already decoded it")
|
||||
}
|
||||
}
|
||||
@@ -2337,7 +2337,6 @@ func (a *adapter) TenantMessageSearch(id t.TenantID, topic string, uid t.Uid, qu
|
||||
return nil, "", err
|
||||
}
|
||||
msg := finalizeTenantMessage(id, row.Message, row.RecalledBy)
|
||||
msg.Head = kvMapFromJSON(msg.Head)
|
||||
if len(msgs) == limit {
|
||||
nextCursor = strconv.FormatInt(msg.SeqId, 10)
|
||||
continue
|
||||
@@ -2398,7 +2397,6 @@ func (a *adapter) tenantGroupMessageSearch(id t.TenantID, topic string, uid t.Ui
|
||||
return nil, "", err
|
||||
}
|
||||
msg := normalizeGroupMessage(id, row.Message, row.RecalledBy)
|
||||
msg.Head = kvMapFromJSON(msg.Head)
|
||||
if len(msgs) == limit {
|
||||
nextCursor = strconv.FormatInt(msg.SeqId, 10)
|
||||
continue
|
||||
@@ -4593,12 +4591,24 @@ func (a *adapter) TenantDailyReportList(id t.TenantID, uid t.Uid, scope, from, t
|
||||
if limit <= 0 || limit > 100 {
|
||||
limit = 50
|
||||
}
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
where := []string{"1=1"}
|
||||
args := []any{}
|
||||
if scope == "" || scope == "mine" {
|
||||
where = append(where, "author_id=?")
|
||||
args = append(args, decodeUID(uid))
|
||||
} else if scope != "managed" {
|
||||
} else if scope == "managed" {
|
||||
visibility, err := a.organizationVisibility(ctx, uid)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if !canUseManagedReports(visibility) {
|
||||
return nil, "", t.ErrPermissionDenied
|
||||
}
|
||||
} else {
|
||||
return nil, "", t.ErrMalformed
|
||||
}
|
||||
if from != "" {
|
||||
@@ -4622,10 +4632,6 @@ func (a *adapter) TenantDailyReportList(id t.TenantID, uid t.Uid, scope, from, t
|
||||
args = append(args, parsed)
|
||||
}
|
||||
args = append(args, limit+1)
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
rows, err := a.db.QueryxContext(ctx, `SELECT * FROM im_daily_reports WHERE `+strings.Join(where, " AND ")+` ORDER BY id DESC LIMIT ?`, args...)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
@@ -4769,14 +4775,14 @@ func (a *adapter) TenantDailyReportSubmit(id t.TenantID, uid t.Uid, reportID str
|
||||
return a.TenantDailyReportGet(id, uid, reportID)
|
||||
}
|
||||
|
||||
func (a *adapter) TenantDailyReportStats(id t.TenantID, uid t.Uid, date time.Time, _ string) (*t.DailyReportStats, error) {
|
||||
func (a *adapter) TenantDailyReportStats(id t.TenantID, uid t.Uid, date time.Time, departmentID string) (*t.DailyReportStats, error) {
|
||||
if !a.tenantScopedDB {
|
||||
tenantAdp, err := a.routedTenantAdapter(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tenantAdp != a {
|
||||
return tenantAdp.TenantDailyReportStats(id, uid, date, "")
|
||||
return tenantAdp.TenantDailyReportStats(id, uid, date, departmentID)
|
||||
}
|
||||
}
|
||||
if err := validTenant(id); err != nil || uid.IsZero() {
|
||||
@@ -4786,11 +4792,22 @@ func (a *adapter) TenantDailyReportStats(id t.TenantID, uid t.Uid, date time.Tim
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
stats := &t.DailyReportStats{}
|
||||
if err := a.db.QueryRowContext(ctx, `SELECT COUNT(DISTINCT user_id) FROM im_org_members WHERE employment_state='active'`).Scan(&stats.ExpectedCount); err != nil {
|
||||
visibility, err := a.organizationVisibility(ctx, uid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := a.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM im_daily_reports WHERE report_date=? AND status=?`, date.Format("2006-01-02"), t.DailyReportStatusSubmitted).Scan(&stats.SubmittedCount); err != nil {
|
||||
memberWhere, memberArgs := reportMemberScopeWhere(visibility, departmentID)
|
||||
stats := &t.DailyReportStats{}
|
||||
if err := a.db.QueryRowContext(ctx, `SELECT COUNT(DISTINCT m.user_id) FROM im_org_members AS m WHERE `+strings.Join(memberWhere, " AND "), memberArgs...).Scan(&stats.ExpectedCount); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
submittedWhere := []string{"r.report_date=?", "r.status=?"}
|
||||
submittedArgs := []any{date.Format("2006-01-02"), t.DailyReportStatusSubmitted}
|
||||
if len(memberWhere) > 0 {
|
||||
submittedWhere = append(submittedWhere, `EXISTS (SELECT 1 FROM im_org_members AS m WHERE m.user_id=r.author_id AND `+strings.Join(memberWhere, " AND ")+`)`)
|
||||
submittedArgs = append(submittedArgs, memberArgs...)
|
||||
}
|
||||
if err := a.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM im_daily_reports AS r WHERE `+strings.Join(submittedWhere, " AND "), submittedArgs...).Scan(&stats.SubmittedCount); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats.MissingCount = stats.ExpectedCount - stats.SubmittedCount
|
||||
@@ -4800,20 +4817,114 @@ func (a *adapter) TenantDailyReportStats(id t.TenantID, uid t.Uid, date time.Tim
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
func (a *adapter) TenantDailyReportRemind(id t.TenantID, uid t.Uid, _ time.Time, userIDs []t.Uid, _ string) ([]t.Uid, error) {
|
||||
func reportMemberScopeWhere(visibility organizationVisibility, departmentID string) ([]string, []any) {
|
||||
memberWhere := []string{"m.employment_state='active'"}
|
||||
memberArgs := []any{}
|
||||
if strings.TrimSpace(departmentID) != "" {
|
||||
memberWhere = append(memberWhere, "m.department_id=?")
|
||||
memberArgs = append(memberArgs, strings.TrimSpace(departmentID))
|
||||
}
|
||||
visibilitySQL, visibilityArgs := visibility.memberWhereSQL("m")
|
||||
if visibilitySQL != "" {
|
||||
memberWhere = append(memberWhere, visibilitySQL)
|
||||
memberArgs = append(memberArgs, visibilityArgs...)
|
||||
}
|
||||
return memberWhere, memberArgs
|
||||
}
|
||||
|
||||
func canUseManagedReports(visibility organizationVisibility) bool {
|
||||
return visibility.Scope == orgVisibilityAll
|
||||
}
|
||||
|
||||
func (a *adapter) TenantDailyReportRemind(id t.TenantID, uid t.Uid, date time.Time, userIDs []t.Uid, idempotencyKey string) ([]t.Uid, error) {
|
||||
if !a.tenantScopedDB {
|
||||
tenantAdp, err := a.routedTenantAdapter(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tenantAdp != a {
|
||||
return tenantAdp.TenantDailyReportRemind(id, uid, time.Time{}, userIDs, "")
|
||||
return tenantAdp.TenantDailyReportRemind(id, uid, date, userIDs, idempotencyKey)
|
||||
}
|
||||
}
|
||||
if err := validTenant(id); err != nil || uid.IsZero() {
|
||||
if err := validTenant(id); err != nil || uid.IsZero() || date.IsZero() || len(userIDs) == 0 || strings.TrimSpace(idempotencyKey) == "" {
|
||||
return nil, t.ErrMalformed
|
||||
}
|
||||
return userIDs, nil
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
visibility, err := a.organizationVisibility(ctx, uid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !canUseManagedReports(visibility) {
|
||||
return nil, t.ErrPermissionDenied
|
||||
}
|
||||
targetIDs := make([]int64, 0, len(userIDs))
|
||||
seen := map[int64]struct{}{}
|
||||
for _, target := range userIDs {
|
||||
if target.IsZero() {
|
||||
return nil, t.ErrMalformed
|
||||
}
|
||||
decoded := decodeUID(target)
|
||||
if decoded <= 0 {
|
||||
return nil, t.ErrMalformed
|
||||
}
|
||||
if _, ok := seen[decoded]; ok {
|
||||
continue
|
||||
}
|
||||
seen[decoded] = struct{}{}
|
||||
targetIDs = append(targetIDs, decoded)
|
||||
}
|
||||
query, args, err := sqlx.In(`SELECT DISTINCT m.user_id FROM im_org_members AS m
|
||||
WHERE m.employment_state='active' AND m.user_id IN (?) AND NOT EXISTS (
|
||||
SELECT 1 FROM im_daily_reports AS r
|
||||
WHERE r.author_id=m.user_id AND r.report_date=? AND r.status=?
|
||||
)`, targetIDs, date.Format("2006-01-02"), t.DailyReportStatusSubmitted)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query = a.db.Rebind(query)
|
||||
rows, err := a.db.QueryxContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
reminded := make([]t.Uid, 0, len(targetIDs))
|
||||
for rows.Next() {
|
||||
var raw int64
|
||||
if err = rows.Scan(&raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
target := store.EncodeUid(raw)
|
||||
reminded = append(reminded, target)
|
||||
}
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tx, err := a.db.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
committed := false
|
||||
defer func() {
|
||||
if !committed {
|
||||
_ = tx.Rollback()
|
||||
}
|
||||
}()
|
||||
for _, target := range reminded {
|
||||
eventID := t.StableEventID("daily_report_reminder", date.Format("2006-01-02"), target.UserId(), strings.TrimSpace(idempotencyKey))
|
||||
if err = insertSystemNotificationTx(tx, target, eventID, "daily_report.reminded", "日报提醒", date.Format("2006-01-02"), map[string]any{
|
||||
"date": date.Format("2006-01-02"),
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err = tx.Commit(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
committed = true
|
||||
return reminded, nil
|
||||
}
|
||||
|
||||
func scanDailyReport(rows *sqlx.Rows) (*t.DailyReport, error) {
|
||||
@@ -4869,12 +4980,24 @@ func (a *adapter) TenantMonthlyReportList(id t.TenantID, uid t.Uid, scope, fromM
|
||||
if limit <= 0 || limit > 100 {
|
||||
limit = 50
|
||||
}
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
where := []string{"1=1"}
|
||||
args := []any{}
|
||||
if scope == "" || scope == "mine" {
|
||||
where = append(where, "author_id=?")
|
||||
args = append(args, decodeUID(uid))
|
||||
} else if scope != "managed" {
|
||||
} else if scope == "managed" {
|
||||
visibility, err := a.organizationVisibility(ctx, uid)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
if !canUseManagedReports(visibility) {
|
||||
return nil, "", t.ErrPermissionDenied
|
||||
}
|
||||
} else {
|
||||
return nil, "", t.ErrMalformed
|
||||
}
|
||||
if fromMonth != "" {
|
||||
@@ -4898,10 +5021,6 @@ func (a *adapter) TenantMonthlyReportList(id t.TenantID, uid t.Uid, scope, fromM
|
||||
args = append(args, parsed)
|
||||
}
|
||||
args = append(args, limit+1)
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
rows, err := a.db.QueryxContext(ctx, `SELECT * FROM im_monthly_reports WHERE `+strings.Join(where, " AND ")+` ORDER BY id DESC LIMIT ?`, args...)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
@@ -5076,14 +5195,14 @@ func (a *adapter) TenantMonthlyReportReopen(id t.TenantID, uid t.Uid, reportID s
|
||||
return a.TenantMonthlyReportGet(id, uid, reportID)
|
||||
}
|
||||
|
||||
func (a *adapter) TenantMonthlyReportStats(id t.TenantID, uid t.Uid, month, _ string) (*t.MonthlyReportStats, error) {
|
||||
func (a *adapter) TenantMonthlyReportStats(id t.TenantID, uid t.Uid, month, departmentID string) (*t.MonthlyReportStats, error) {
|
||||
if !a.tenantScopedDB {
|
||||
tenantAdp, err := a.routedTenantAdapter(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tenantAdp != a {
|
||||
return tenantAdp.TenantMonthlyReportStats(id, uid, month, "")
|
||||
return tenantAdp.TenantMonthlyReportStats(id, uid, month, departmentID)
|
||||
}
|
||||
}
|
||||
if err := validTenant(id); err != nil || uid.IsZero() {
|
||||
@@ -5093,11 +5212,22 @@ func (a *adapter) TenantMonthlyReportStats(id t.TenantID, uid t.Uid, month, _ st
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
stats := &t.MonthlyReportStats{}
|
||||
if err := a.db.QueryRowContext(ctx, `SELECT COUNT(DISTINCT user_id) FROM im_org_members WHERE employment_state='active'`).Scan(&stats.ExpectedCount); err != nil {
|
||||
visibility, err := a.organizationVisibility(ctx, uid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := a.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM im_monthly_reports WHERE report_month=? AND status=?`, month, t.DailyReportStatusSubmitted).Scan(&stats.SubmittedCount); err != nil {
|
||||
memberWhere, memberArgs := reportMemberScopeWhere(visibility, departmentID)
|
||||
stats := &t.MonthlyReportStats{}
|
||||
if err := a.db.QueryRowContext(ctx, `SELECT COUNT(DISTINCT m.user_id) FROM im_org_members AS m WHERE `+strings.Join(memberWhere, " AND "), memberArgs...).Scan(&stats.ExpectedCount); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
submittedWhere := []string{"r.report_month=?", "r.status=?"}
|
||||
submittedArgs := []any{month, t.DailyReportStatusSubmitted}
|
||||
if len(memberWhere) > 0 {
|
||||
submittedWhere = append(submittedWhere, `EXISTS (SELECT 1 FROM im_org_members AS m WHERE m.user_id=r.author_id AND `+strings.Join(memberWhere, " AND ")+`)`)
|
||||
submittedArgs = append(submittedArgs, memberArgs...)
|
||||
}
|
||||
if err := a.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM im_monthly_reports AS r WHERE `+strings.Join(submittedWhere, " AND "), submittedArgs...).Scan(&stats.SubmittedCount); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stats.MissingCount = stats.ExpectedCount - stats.SubmittedCount
|
||||
|
||||
@@ -1170,8 +1170,8 @@ func (a *adapter) TenantGroupRoleUpdate(id t.TenantID, topic string, actor, targ
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
res, err := tx.ExecContext(ctx, "UPDATE im_subscriptions SET modegiven=?,updatedat=? WHERE topic=? AND userid=? AND deletedat IS NULL",
|
||||
modeGiven.String(), now, topic, decodeUID(target))
|
||||
res, err := tx.ExecContext(ctx, "UPDATE im_subscriptions SET modegiven=?,modewant=?,updatedat=? WHERE topic=? AND userid=? AND deletedat IS NULL",
|
||||
modeGiven.String(), modeGiven.String(), now, topic, decodeUID(target))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1194,6 +1194,10 @@ func (a *adapter) TenantGroupRoleUpdate(id t.TenantID, topic string, actor, targ
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func groupAdminAccessMode() t.AccessMode {
|
||||
return t.ModeCPublic | t.ModeApprove
|
||||
}
|
||||
|
||||
func (a *adapter) TenantGroupOwnerTransfer(id t.TenantID, topic string, previousOwner, newOwner t.Uid,
|
||||
version int, now time.Time) error {
|
||||
if tenantAdp, err := a.routeGroupTenant(id); err != nil {
|
||||
@@ -1214,10 +1218,10 @@ func (a *adapter) TenantGroupOwnerTransfer(id t.TenantID, topic string, previous
|
||||
uid t.Uid
|
||||
mode t.AccessMode
|
||||
role string
|
||||
}{{previousOwner, t.ModeCAdmin | t.ModeCPublic, t.GroupMemberRoleAdmin}, {newOwner, t.ModeCFull, t.GroupMemberRoleOwner}}
|
||||
}{{previousOwner, groupAdminAccessMode(), t.GroupMemberRoleAdmin}, {newOwner, t.ModeCFull, t.GroupMemberRoleOwner}}
|
||||
for _, update := range updates {
|
||||
res, execErr := tx.ExecContext(ctx, "UPDATE im_subscriptions SET modegiven=?,updatedat=? WHERE topic=? AND userid=? AND deletedat IS NULL",
|
||||
update.mode.String(), now, topic, decodeUID(update.uid))
|
||||
res, execErr := tx.ExecContext(ctx, "UPDATE im_subscriptions SET modegiven=?,modewant=?,updatedat=? WHERE topic=? AND userid=? AND deletedat IS NULL",
|
||||
update.mode.String(), update.mode.String(), now, topic, decodeUID(update.uid))
|
||||
if execErr != nil {
|
||||
return execErr
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user