HH-437: harden production auth and tenant authorization (#84)

* HH-437 harden auth and account authorization

* HH-437 reject revoked platform access

---------

Co-authored-by: Rogee <rogee@ipao.vip>
This commit is contained in:
Rogee
2026-08-21 19:13:10 +08:00
committed by GitHub
co-authored by rogee
parent 04c1654086
commit cf263d10b4
22 changed files with 442 additions and 90 deletions
@@ -80,7 +80,7 @@ func (h *PlatformAccountHandler) List(c *gin.Context) {
// Reference: Chatwoot Platform::Api::V1::AccountsController#show
// Requires: Permissible verification
func (h *PlatformAccountHandler) Show(c *gin.Context) {
accountID, err := parseUintParam(c, "id")
accountID, err := parseUintAnyParam(c, "account_id", "id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account ID")
return
@@ -154,7 +154,7 @@ func (h *PlatformAccountHandler) Create(c *gin.Context) {
// Reference: Chatwoot Platform::Api::V1::AccountsController#update
// Requires: Permissible verification
func (h *PlatformAccountHandler) Update(c *gin.Context) {
accountID, err := parseUintParam(c, "id")
accountID, err := parseUintAnyParam(c, "account_id", "id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account ID")
return
@@ -212,7 +212,7 @@ func (h *PlatformAccountHandler) Update(c *gin.Context) {
// Reference: Chatwoot Platform::Api::V1::AccountsController#destroy
// Requires: Permissible verification
func (h *PlatformAccountHandler) Destroy(c *gin.Context) {
accountID, err := parseUintParam(c, "id")
accountID, err := parseUintAnyParam(c, "account_id", "id")
if err != nil {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, "invalid account ID")
return
@@ -92,6 +92,10 @@ func (h *PlatformAccountUserHandler) Create(c *gin.Context) {
response.AbortWithStatusError(c, http.StatusBadRequest, response.ErrBadRequest, err.Error())
return
}
if perm, err := h.permissibleRepo.FindByPlatformAppAndResource(c.Request.Context(), platformAppID, model.PermissibleTypeUser, req.UserID); err != nil || perm == nil {
response.AbortWithStatusError(c, http.StatusForbidden, response.ErrForbidden, "non permissible resource")
return
}
acctUser, err := h.accountRepo.UpsertAccountUser(c.Request.Context(), accountID, req.UserID, req.Role)
if err != nil {
@@ -93,10 +93,10 @@ func setupPlatformTokenTestE2E(t *testing.T) (*gin.Engine, *repository.Permissib
platformGroup.DELETE("/users/:id", platformUser.Destroy)
platformGroup.GET("/accounts", platformAccount.List)
platformGroup.GET("/accounts/:id", platformAccount.Show)
platformGroup.GET("/accounts/:account_id", platformAccount.Show)
platformGroup.POST("/accounts", platformAccount.Create)
platformGroup.PATCH("/accounts/:id", platformAccount.Update)
platformGroup.DELETE("/accounts/:id", platformAccount.Destroy)
platformGroup.PATCH("/accounts/:account_id", platformAccount.Update)
platformGroup.DELETE("/accounts/:account_id", platformAccount.Destroy)
platformGroup.GET("/agent_bots", platformAgentBot.List)
platformGroup.GET("/agent_bots/:id", platformAgentBot.Show)
@@ -105,11 +105,10 @@ func setupPlatformTokenTestE2E(t *testing.T) (*gin.Engine, *repository.Permissib
platformGroup.DELETE("/agent_bots/:id", platformAgentBot.Destroy)
platformGroup.POST("/agent_bots/:id/delete_avatar", platformAgentBot.DeleteAvatar)
// Gin wildcard constraint: nested routes under accounts/:id must reuse :id.
platformGroup.GET("/accounts/:id/account_users", platformAccountUser.Index)
platformGroup.POST("/accounts/:id/account_users", platformAccountUser.Create)
platformGroup.DELETE("/accounts/:id/account_users/destroy", platformAccountUser.Destroy)
platformGroup.DELETE("/accounts/:id/account_users/:user_id", platformAccountUser.Destroy)
platformGroup.GET("/accounts/:account_id/account_users", platformAccountUser.Index)
platformGroup.POST("/accounts/:account_id/account_users", platformAccountUser.Create)
platformGroup.DELETE("/accounts/:account_id/account_users/destroy", platformAccountUser.Destroy)
platformGroup.DELETE("/accounts/:account_id/account_users/:user_id", platformAccountUser.Destroy)
return engine, permissibleRepo, userRepo, accountRepo
}
@@ -334,6 +333,25 @@ func TestPlatformAccountE2E_Show(t *testing.T) {
assert.Equal(t, "Show Account", showData["name"])
}
func TestPlatformAccountE2E_Update(t *testing.T) {
engine, _, _, _ := setupPlatformTokenTestE2E(t)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/platform/api/v1/accounts", bytes.NewBufferString(`{"name":"Original Account"}`))
req.Header.Set("Content-Type", "application/json")
engine.ServeHTTP(w, req)
require.Equal(t, http.StatusCreated, w.Code)
accountID := parseID(t, unpackData(t, w.Body.Bytes()))
w = httptest.NewRecorder()
req = httptest.NewRequest(http.MethodPatch, "/platform/api/v1/accounts/"+accountID, bytes.NewBufferString(`{"name":"Updated Account"}`))
req.Header.Set("Content-Type", "application/json")
engine.ServeHTTP(w, req)
require.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "Updated Account", unpackData(t, w.Body.Bytes())["name"])
}
func TestPlatformAccountE2E_Destroy(t *testing.T) {
engine, _, _, _ := setupPlatformTokenTestE2E(t)
@@ -465,6 +483,27 @@ func TestPlatformAccountUserE2E_Create(t *testing.T) {
assert.Equal(t, "administrator", accountUser["role"])
}
func TestPlatformAccountUserE2E_CreateRejectsNonPermissibleUser(t *testing.T) {
engine, _, userRepo, _ := setupPlatformTokenTestE2E(t)
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/platform/api/v1/accounts", bytes.NewBufferString(`{"name":"Permissible Account"}`))
req.Header.Set("Content-Type", "application/json")
engine.ServeHTTP(w, req)
require.Equal(t, http.StatusCreated, w.Code)
accountID := parseID(t, unpackData(t, w.Body.Bytes()))
user := &model.User{Name: "Non-permissible User", Email: "non-permissible@example.com", Provider: "email", Active: true}
require.NoError(t, userRepo.Create(t.Context(), user))
w = httptest.NewRecorder()
req = httptest.NewRequest(http.MethodPost, "/platform/api/v1/accounts/"+accountID+"/account_users", bytes.NewBufferString(fmt.Sprintf(`{"user_id":%d}`, user.ID)))
req.Header.Set("Content-Type", "application/json")
engine.ServeHTTP(w, req)
assert.Equal(t, http.StatusForbidden, w.Code)
}
func TestPlatformAccountUserE2E_Index(t *testing.T) {
engine, _, _, _ := setupPlatformTokenTestE2E(t)