fix: scope login audit records by tenant
This commit is contained in:
@@ -1,8 +1,9 @@
|
|||||||
-- MySQL 8.0. Admin message governance and authentication audit tables.
|
-- MySQL 8.0. Admin message governance and authentication audit tables.
|
||||||
-- Execute this script in each tenant business database.
|
-- Execute this script in the shared meeting_im database; tenant_id provides tenant isolation.
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS im_user_login_logs (
|
CREATE TABLE IF NOT EXISTS im_user_login_logs (
|
||||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||||
|
tenant_id BIGINT NOT NULL,
|
||||||
user_id BIGINT NOT NULL,
|
user_id BIGINT NOT NULL,
|
||||||
username VARCHAR(64) NOT NULL DEFAULT '',
|
username VARCHAR(64) NOT NULL DEFAULT '',
|
||||||
account_type VARCHAR(24) NOT NULL DEFAULT '',
|
account_type VARCHAR(24) NOT NULL DEFAULT '',
|
||||||
@@ -24,6 +25,7 @@ CREATE TABLE IF NOT EXISTS im_user_login_logs (
|
|||||||
metadata JSON NULL,
|
metadata JSON NULL,
|
||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
PRIMARY KEY (id),
|
PRIMARY KEY (id),
|
||||||
|
INDEX idx_im_user_login_tenant_time (tenant_id, login_at DESC, id DESC),
|
||||||
INDEX idx_im_user_login_user_time (user_id, login_at DESC, id DESC),
|
INDEX idx_im_user_login_user_time (user_id, login_at DESC, id DESC),
|
||||||
INDEX idx_im_user_login_ip_time (ip, login_at DESC, id DESC),
|
INDEX idx_im_user_login_ip_time (ip, login_at DESC, id DESC),
|
||||||
INDEX idx_im_user_login_device_time (device_id(64), login_at DESC),
|
INDEX idx_im_user_login_device_time (device_id(64), login_at DESC),
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ package mysql
|
|||||||
func ensureIMAdminGovernanceTables(exec sqlExecutor) error {
|
func ensureIMAdminGovernanceTables(exec sqlExecutor) error {
|
||||||
statements := []string{
|
statements := []string{
|
||||||
`CREATE TABLE IF NOT EXISTS im_user_login_logs (
|
`CREATE TABLE IF NOT EXISTS im_user_login_logs (
|
||||||
id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL,
|
id BIGINT NOT NULL AUTO_INCREMENT, tenant_id BIGINT NOT NULL, user_id BIGINT NOT NULL,
|
||||||
username VARCHAR(64) NOT NULL DEFAULT '', account_type VARCHAR(24) NOT NULL DEFAULT '',
|
username VARCHAR(64) NOT NULL DEFAULT '', account_type VARCHAR(24) NOT NULL DEFAULT '',
|
||||||
account_hash CHAR(64) NOT NULL DEFAULT '', account_mask VARCHAR(128) NOT NULL DEFAULT '',
|
account_hash CHAR(64) NOT NULL DEFAULT '', account_mask VARCHAR(128) NOT NULL DEFAULT '',
|
||||||
login_type VARCHAR(24) NOT NULL, event_type VARCHAR(24) NOT NULL DEFAULT 'login',
|
login_type VARCHAR(24) NOT NULL, event_type VARCHAR(24) NOT NULL DEFAULT 'login',
|
||||||
@@ -16,7 +16,8 @@ func ensureIMAdminGovernanceTables(exec sqlExecutor) error {
|
|||||||
user_agent VARCHAR(512) NOT NULL DEFAULT '', risk_level VARCHAR(16) NOT NULL DEFAULT 'low',
|
user_agent VARCHAR(512) NOT NULL DEFAULT '', risk_level VARCHAR(16) NOT NULL DEFAULT 'low',
|
||||||
request_id VARCHAR(64) NOT NULL DEFAULT '', login_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
request_id VARCHAR(64) NOT NULL DEFAULT '', login_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
logout_at DATETIME(3), metadata JSON, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
logout_at DATETIME(3), metadata JSON, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||||
PRIMARY KEY(id), INDEX idx_im_user_login_user_time(user_id, login_at DESC, id DESC),
|
PRIMARY KEY(id), INDEX idx_im_user_login_tenant_time(tenant_id, login_at DESC, id DESC),
|
||||||
|
INDEX idx_im_user_login_user_time(user_id, login_at DESC, id DESC),
|
||||||
INDEX idx_im_user_login_ip_time(ip, login_at DESC, id DESC),
|
INDEX idx_im_user_login_ip_time(ip, login_at DESC, id DESC),
|
||||||
INDEX idx_im_user_login_device_time(device_id(64), login_at DESC),
|
INDEX idx_im_user_login_device_time(device_id(64), login_at DESC),
|
||||||
INDEX idx_im_user_login_request(request_id)
|
INDEX idx_im_user_login_request(request_id)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestEnsureIMAdminGovernanceTablesUsesSelectedTenantDatabase(t *testing.T) {
|
func TestEnsureIMAdminGovernanceTablesDefinesLoginAuditTenantScope(t *testing.T) {
|
||||||
exec := &recordingGroupSchemaExecutor{}
|
exec := &recordingGroupSchemaExecutor{}
|
||||||
if err := ensureIMAdminGovernanceTables(exec); err != nil {
|
if err := ensureIMAdminGovernanceTables(exec); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -27,7 +27,15 @@ func TestEnsureIMAdminGovernanceTablesUsesSelectedTenantDatabase(t *testing.T) {
|
|||||||
t.Fatalf("hot login state table must not be created: %s", table)
|
t.Fatalf("hot login state table must not be created: %s", table)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if strings.Contains(joined, "tinode.") || strings.Contains(joined, "create database") || strings.Contains(joined, "tenant_id") {
|
if strings.Contains(joined, "tinode.") || strings.Contains(joined, "create database") {
|
||||||
t.Fatalf("admin governance schema must stay inside the selected tenant database: %s", joined)
|
t.Fatalf("admin governance schema must use the active shared database: %s", joined)
|
||||||
|
}
|
||||||
|
start := strings.Index(joined, "create table if not exists im_user_login_logs")
|
||||||
|
if start < 0 {
|
||||||
|
t.Fatal("missing im_user_login_logs")
|
||||||
|
}
|
||||||
|
end := strings.Index(joined[start:], ") engine=")
|
||||||
|
if end < 0 || !strings.Contains(joined[start:start+end], "tenant_id bigint not null") {
|
||||||
|
t.Fatal("im_user_login_logs must declare tenant_id")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,9 +28,9 @@ func (a *adapter) TenantLoginAuditCreate(id t.TenantID, event *t.LoginAuditEvent
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err = a.db.Exec(`INSERT INTO im_user_login_logs
|
_, err = a.db.Exec(`INSERT INTO im_user_login_logs
|
||||||
(user_id,username,account_type,account_hash,account_mask,login_type,event_type,session_id,device_id,platform,app_version,ip,region,user_agent,risk_level,request_id,login_at,logout_at,metadata)
|
(tenant_id,user_id,username,account_type,account_hash,account_mask,login_type,event_type,session_id,device_id,platform,app_version,ip,region,user_agent,risk_level,request_id,login_at,logout_at,metadata)
|
||||||
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
|
||||||
decodeUID(event.UserID), event.Username, event.AccountType, event.AccountHash, event.AccountMask,
|
int64(id), decodeUID(event.UserID), event.Username, event.AccountType, event.AccountHash, event.AccountMask,
|
||||||
event.LoginType, event.EventType, event.SessionID, event.DeviceID, event.Platform, event.AppVersion,
|
event.LoginType, event.EventType, event.SessionID, event.DeviceID, event.Platform, event.AppVersion,
|
||||||
event.IP, event.Region, event.UserAgent, event.RiskLevel, event.RequestID, event.LoginAt, event.LogoutAt, nullableJSON(metadata))
|
event.IP, event.Region, event.UserAgent, event.RiskLevel, event.RequestID, event.LoginAt, event.LogoutAt, nullableJSON(metadata))
|
||||||
return err
|
return err
|
||||||
@@ -93,7 +93,7 @@ func (a *adapter) TenantLoginAuditClose(id t.TenantID, sessionID string, at time
|
|||||||
if tenantAdp != a {
|
if tenantAdp != a {
|
||||||
return tenantAdp.TenantLoginAuditClose(id, sessionID, at)
|
return tenantAdp.TenantLoginAuditClose(id, sessionID, at)
|
||||||
}
|
}
|
||||||
_, err = a.db.Exec("UPDATE im_user_login_logs SET logout_at=? WHERE session_id=? AND logout_at IS NULL", at, sessionID)
|
_, err = a.db.Exec("UPDATE im_user_login_logs SET logout_at=? WHERE tenant_id=? AND session_id=? AND logout_at IS NULL", at, int64(id), sessionID)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
//go:build mysql
|
||||||
|
|
||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLoginAuditQueriesAreTenantScoped(t *testing.T) {
|
||||||
|
source, err := os.ReadFile("tenant_security_audit.go")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
body := string(source)
|
||||||
|
for _, required := range []string{
|
||||||
|
"(tenant_id,user_id,username",
|
||||||
|
"(tenant_id,user_id,account_type",
|
||||||
|
"WHERE tenant_id=? AND session_id=?",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(body, required) {
|
||||||
|
t.Fatalf("login audit query is missing tenant scope %q", required)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user