fix: close platform access re-review findings

This commit is contained in:
2026-07-27 13:12:43 +08:00
parent 169656cc9b
commit 60a12a9654
11 changed files with 316 additions and 32 deletions

View File

@@ -157,6 +157,7 @@ func TestPlatformRoleStatusAndArchiveReturnNotFoundWhenUpdateAffectsZeroRows(t *
mock.ExpectCommit()
ctx, recorder := updateContext(test.method, "/roles/role-a", "role-a", []byte(test.body))
ctx.Set("Auth", &types.JwtClaims{Role: "root"})
test.handler(ctx)
assertResponseCode(t, recorder, int32(status.Code(errcode.ErrRecordNotFound)))
@@ -470,6 +471,105 @@ func TestExplicitPreciseScopeRetainsCoordinatesButStillMasksPII(t *testing.T) {
}
}
func TestPublicResponseProjectionRemovesCredentialAndAttachmentSecrets(t *testing.T) {
ctx, _ := updateContext(http.MethodGet, "/staff/credential/credential-a", "credential-a", nil)
ctx.Set("Auth", &types.JwtClaims{Extend: map[string]string{"location_scope": "precise"}})
response := protectPreciseLocation(ctx, &models.StaffCredential{}, map[string]any{
"identity": "credential-a",
"credential_type": "installer",
"credential_no": "CERT-123456",
"evidence_uri": "private://evidence",
"file_uri": "private://file",
"attachment_uri": "private://attachment",
})
encoded, err := json.Marshal(response)
if err != nil {
t.Fatal(err)
}
body := string(encoded)
if !strings.Contains(body, `"identity":"credential-a"`) || !strings.Contains(body, `"credential_type":"installer"`) {
t.Fatalf("safe credential fields were removed: %s", body)
}
for _, forbidden := range []string{"credential_no", "evidence_uri", "file_uri", "attachment_uri", "CERT-123456", "private://"} {
if strings.Contains(body, forbidden) {
t.Fatalf("credential response leaked %s: %s", forbidden, body)
}
}
}
func TestPlatformAccountDetailMasksDisplayNameAndAvatarWithPreciseScope(t *testing.T) {
_, mock := setupPlatformRoleDatabase(t)
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "platfrom_account" WHERE identity = $1 ORDER BY "platfrom_account"."id" LIMIT $2`)).
WithArgs("account-a", 1).
WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "created_at", "updated_at", "status", "version", "username", "display_name", "avatar", "password_hash", "platform_role_code", "phone"}).
AddRow(uint64(1), "account-a", nil, nil, "enabled", 1, "operator", "张三", "private://avatar", "hash", "finance_operator", "13800138000"))
ctx, recorder := updateContext(http.MethodGet, "/platform/platfrom_account/account-a", "account-a", nil)
ctx.Set("Auth", &types.JwtClaims{Extend: map[string]string{"location_scope": "precise"}})
GetPlatfromAccount(ctx)
assertResponseCode(t, recorder, 0)
body := recorder.Body.String()
if !strings.Contains(body, `"display_name_masked"`) || !strings.Contains(body, `"phone_masked":"138****8000"`) {
t.Fatalf("platform account detail omitted masked PII: %s", body)
}
for _, forbidden := range []string{`"display_name":`, `"avatar":`, "张三", "private://avatar"} {
if strings.Contains(body, forbidden) {
t.Fatalf("platform account detail leaked %s: %s", forbidden, body)
}
}
assertMockExpectations(t, mock)
}
func TestPlatformAccountListMasksDisplayNameAndAvatar(t *testing.T) {
_, mock := setupPlatformRoleDatabase(t)
mock.ExpectQuery(regexp.QuoteMeta(`SELECT count(*) FROM "platfrom_account"`)).
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1))
mock.ExpectQuery(regexp.QuoteMeta(`SELECT * FROM "platfrom_account" ORDER BY created_at desc LIMIT $1`)).
WithArgs(20).
WillReturnRows(sqlmock.NewRows([]string{"id", "identity", "created_at", "updated_at", "status", "version", "username", "display_name", "avatar", "password_hash", "platform_role_code", "phone"}).
AddRow(uint64(1), "account-a", nil, nil, "enabled", 1, "operator", "张三", "private://avatar", "hash", "finance_operator", "13800138000"))
ctx, recorder := updateContext(http.MethodGet, "/platform/platfrom_account", "", nil)
ListPlatfromAccount(ctx)
assertResponseCode(t, recorder, 0)
body := recorder.Body.String()
if !strings.Contains(body, `"display_name_masked"`) || strings.Contains(body, `"display_name":`) || strings.Contains(body, `"avatar":`) {
t.Fatalf("platform account list did not apply the masked projection: %s", body)
}
assertMockExpectations(t, mock)
}
func TestNonRootCannotManagePlatformRolesOrMenus(t *testing.T) {
tests := []struct {
name string
handler gin.HandlerFunc
method string
target string
identity string
body string
}{
{"create role", CreatePlatformRole, http.MethodPost, "/platform/platform_role", "", `{"role_code":"auditor","name":"Auditor"}`},
{"create menu", CreatePlatformMenu, http.MethodPost, "/platform/platform_menu", "", `{"menu_code":"audit","name":"Audit","path":"/audit"}`},
{"update menu status", UpdatePlatformMenuStatus, http.MethodPatch, "/platform/platform_menu/menu-a/status", "menu-a", `{"status":"disabled"}`},
{"archive menu", ArchivePlatformMenu, http.MethodDelete, "/platform/platform_menu/menu-a", "menu-a", ``},
{"replace role menus", ReplacePlatformRoleMenus, http.MethodPut, "/platform/platform_role/role-a/menu", "role-a", `{"menu_identities":[]}`},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, mock := setupPlatformRoleDatabase(t)
ctx, recorder := updateContext(test.method, test.target, test.identity, []byte(test.body))
ctx.Set("Auth", &types.JwtClaims{Role: "platform_operator"})
test.handler(ctx)
assertResponseCode(t, recorder, int32(status.Code(errcode.ErrPermissionDenied)))
assertMockExpectations(t, mock)
})
}
}
func TestPlatformMenuAllowsOnlyAssignedDomain(t *testing.T) {
menus := []models.PlatformMenu{
{MenuCode: "finance", Path: "/finance"},
@@ -495,6 +595,7 @@ func TestCreatePlatformAccountRequiresAssignableNonRootRole(t *testing.T) {
t.Run(test.name, func(t *testing.T) {
_, mock := setupPlatformRoleDatabase(t)
ctx, recorder := updateContext(http.MethodPost, "/platform/platfrom_account", "", []byte(test.body))
ctx.Set("Auth", &types.JwtClaims{Role: "root"})
CreatePlatfromAccount(ctx)
@@ -504,6 +605,31 @@ func TestCreatePlatformAccountRequiresAssignableNonRootRole(t *testing.T) {
}
}
func TestNonRootCannotAssignPlatformAccountRole(t *testing.T) {
tests := []struct {
name string
handler gin.HandlerFunc
method string
identity string
body string
}{
{"create account", CreatePlatfromAccount, http.MethodPost, "", `{"username":"operator","password":"secure-password","platform_role_code":"auditor"}`},
{"change account role", UpdatePlatfromAccount, http.MethodPut, "account-a", `{"platform_role_code":"auditor"}`},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, mock := setupPlatformRoleDatabase(t)
ctx, recorder := updateContext(test.method, "/platform/platfrom_account/"+test.identity, test.identity, []byte(test.body))
ctx.Set("Auth", &types.JwtClaims{Role: "platform_operator"})
test.handler(ctx)
assertResponseCode(t, recorder, int32(status.Code(errcode.ErrPermissionDenied)))
assertMockExpectations(t, mock)
})
}
}
func TestListSafetyEventDisposalsReturnsOnlyTheRequestedEventHistory(t *testing.T) {
_, mock := setupPlatformRoleDatabase(t)
mock.ExpectQuery(regexp.QuoteMeta(`SELECT count(*) FROM "saf_event_disposal" WHERE saf_event_identity = $1`)).
@@ -708,6 +834,7 @@ func TestReplacePlatformRoleMenusAllowsAnEmptySetToClearAssignmentsTransactional
mock.ExpectCommit()
ctx, recorder := updateContext(http.MethodPut, "/roles/role-a/menus", "role-a", []byte(`{"menu_identities":[]}`))
ctx.Set("Auth", &types.JwtClaims{Role: "root"})
ReplacePlatformRoleMenus(ctx)
assertResponseCode(t, recorder, 0)
@@ -723,6 +850,7 @@ func TestReplacePlatformRoleMenusRejectsSystemRoleBeforeChangingRelations(t *tes
mock.ExpectRollback()
ctx, recorder := updateContext(http.MethodPut, "/roles/root-role/menus", "root-role", []byte(`{"menu_identities":[]}`))
ctx.Set("Auth", &types.JwtClaims{Role: "root"})
ReplacePlatformRoleMenus(ctx)
assertResponseCode(t, recorder, int32(status.Code(errcode.ErrInvalidArgument)))