//go:build e2e package e2e import ( "encoding/json" "fmt" "net/http" "os" "testing" "github.com/stretchr/testify/require" ) func TestPermission_Catalog(t *testing.T) { e2eStep(t, "P-01", "GET", "/api/v1/permissions/catalog", "讀全平台 Permission Catalog 樹狀結構") c := NewClient(t) env := c.DoExpectOK(t, http.MethodGet, "/api/v1/permissions/catalog?tree=true", nil, true) var data struct { Tree []map[string]any `json:"tree"` } require.NoError(t, json.Unmarshal(env.Data, &data)) require.NotEmpty(t, data.Tree) } func TestPermission_Me(t *testing.T) { e2eStep(t, "P-02", "GET", "/api/v1/permissions/me", "讀當前 user 的角色 + 權限樹") c := NewClient(t) env := c.DoExpectOK(t, http.MethodGet, "/api/v1/permissions/me?include_tree=true", nil, true) var data struct { UID string `json:"uid"` TenantID string `json:"tenant_id"` Roles []string `json:"roles"` Permissions map[string]string `json:"permissions"` Tree []map[string]any `json:"tree"` } require.NoError(t, json.Unmarshal(env.Data, &data)) require.Equal(t, c.Fixture.UID, data.UID) require.Equal(t, c.Fixture.TenantID, data.TenantID) require.Contains(t, data.Roles, c.Fixture.RoleKey) require.NotEmpty(t, data.Permissions) } func TestPermission_RoleCRUD(t *testing.T) { e2eStep(t, "P-03~P-06", "*", "/api/v1/permissions/roles", "租戶角色 CRUD:建立 → 列表 → 更新 display_name → 刪除") c := NewClient(t) createEnv := c.DoExpectOK(t, http.MethodPost, "/api/v1/permissions/roles", map[string]string{ "key": "e2e_custom_role", "display_name": "E2E Custom", "status": "open", }, true) var role struct { ID string `json:"id"` Key string `json:"key"` } require.NoError(t, json.Unmarshal(createEnv.Data, &role)) require.Equal(t, "e2e_custom_role", role.Key) require.NotEmpty(t, role.ID) // 避免 e2e-up 反覆跑時 role 殘留 → 後續 Create 撞 unique key t.Cleanup(func() { _, _ = c.Do(t, http.MethodDelete, "/api/v1/permissions/roles/"+role.ID, nil, true) }) listEnv := c.DoExpectOK(t, http.MethodGet, "/api/v1/permissions/roles", nil, true) var list struct { Roles []struct { Key string `json:"key"` } `json:"roles"` } require.NoError(t, json.Unmarshal(listEnv.Data, &list)) found := false for _, r := range list.Roles { if r.Key == "e2e_custom_role" { found = true break } } require.True(t, found, "created role should appear in list") patchEnv := c.DoExpectOK(t, http.MethodPatch, "/api/v1/permissions/roles/"+role.ID, map[string]string{ "display_name": "E2E Custom Renamed", }, true) var patched struct { DisplayName string `json:"display_name"` } require.NoError(t, json.Unmarshal(patchEnv.Data, &patched)) require.Equal(t, "E2E Custom Renamed", patched.DisplayName) c.DoExpectOK(t, http.MethodDelete, "/api/v1/permissions/roles/"+role.ID, nil, true) } func TestPermission_RolePermissions(t *testing.T) { e2eStep(t, "P-07/P-08", "PUT/GET", "/api/v1/permissions/roles/:id/permissions", "Role 全量替換 Permission(含 parent closure),再讀回比對") c := NewClient(t) permissionID := firstCatalogPermissionID(t, c) createEnv := c.DoExpectOK(t, http.MethodPost, "/api/v1/permissions/roles", map[string]string{ "key": "e2e_role_permissions", "display_name": "E2E Role Permissions", }, true) var role struct { ID string `json:"id"` } require.NoError(t, json.Unmarshal(createEnv.Data, &role)) require.NotEmpty(t, role.ID) t.Cleanup(func() { _, _ = c.Do(t, http.MethodDelete, "/api/v1/permissions/roles/"+role.ID, nil, true) }) c.DoExpectOK(t, http.MethodPut, "/api/v1/permissions/roles/"+role.ID+"/permissions", map[string][]string{ "permission_ids": {permissionID}, }, true) listEnv := c.DoExpectOK(t, http.MethodGet, "/api/v1/permissions/roles/"+role.ID+"/permissions", nil, true) var list struct { Permissions []struct { ID string `json:"id"` } `json:"permissions"` } require.NoError(t, json.Unmarshal(listEnv.Data, &list)) require.NotEmpty(t, list.Permissions) found := false for _, p := range list.Permissions { if p.ID == permissionID { found = true break } } require.True(t, found, "role should include requested permission") c.DoExpectOK(t, http.MethodDelete, "/api/v1/permissions/roles/"+role.ID, nil, true) } func TestPermission_AssignUserRole(t *testing.T) { e2eStep(t, "P-09~P-11", "*", "/api/v1/permissions/users/:uid/roles", "User ↔ Role 指派 / 列表 / 撤銷") c := NewClient(t) createEnv := c.DoExpectOK(t, http.MethodPost, "/api/v1/permissions/roles", map[string]string{ "key": "e2e_assign_role", "display_name": "E2E Assign", }, true) var role struct { ID string `json:"id"` } require.NoError(t, json.Unmarshal(createEnv.Data, &role)) t.Cleanup(func() { _, _ = c.Do(t, http.MethodDelete, "/api/v1/permissions/users/"+c.Fixture.UID+"/roles/"+role.ID, nil, true) _, _ = c.Do(t, http.MethodDelete, "/api/v1/permissions/roles/"+role.ID, nil, true) }) c.DoExpectOK(t, http.MethodPost, "/api/v1/permissions/users/"+c.Fixture.UID+"/roles", map[string]string{ "role_id": role.ID, }, true) listEnv := c.DoExpectOK(t, http.MethodGet, "/api/v1/permissions/users/"+c.Fixture.UID+"/roles", nil, true) var list struct { UserRoles []struct { RoleID string `json:"role_id"` } `json:"user_roles"` } require.NoError(t, json.Unmarshal(listEnv.Data, &list)) found := false for _, r := range list.UserRoles { if r.RoleID == role.ID { found = true break } } require.True(t, found) c.DoExpectOK(t, http.MethodDelete, "/api/v1/permissions/users/"+c.Fixture.UID+"/roles/"+role.ID, nil, true) c.DoExpectOK(t, http.MethodDelete, "/api/v1/permissions/roles/"+role.ID, nil, true) } func TestPermission_RoleMappingCRUD(t *testing.T) { e2eStep(t, "P-12", "*", "/api/v1/permissions/role-mappings", "外部 IdP group → 內部 Role.Key 對映 CRUD") c := NewClient(t) roleKey := "e2e_mapping_role" externalKey := fmt.Sprintf("e2e-group-%s", c.Fixture.UID) createEnv := c.DoExpectOK(t, http.MethodPost, "/api/v1/permissions/roles", map[string]string{ "key": roleKey, "display_name": "E2E Mapping Role", }, true) var role struct { ID string `json:"id"` } require.NoError(t, json.Unmarshal(createEnv.Data, &role)) require.NotEmpty(t, role.ID) t.Cleanup(func() { _, _ = c.Do(t, http.MethodDelete, "/api/v1/permissions/role-mappings", map[string]string{ "external_source": "zitadel", "external_key": externalKey, }, true) _, _ = c.Do(t, http.MethodDelete, "/api/v1/permissions/roles/"+role.ID, nil, true) }) upsertEnv := c.DoExpectOK(t, http.MethodPut, "/api/v1/permissions/role-mappings", map[string]string{ "external_source": "zitadel", "external_key": externalKey, "internal_role_key": roleKey, }, true) var mapping struct { ID string `json:"id"` ExternalSource string `json:"external_source"` ExternalKey string `json:"external_key"` InternalRoleID string `json:"internal_role_id"` InternalRoleKey string `json:"internal_role_key"` } require.NoError(t, json.Unmarshal(upsertEnv.Data, &mapping)) require.NotEmpty(t, mapping.ID) require.Equal(t, "zitadel", mapping.ExternalSource) require.Equal(t, externalKey, mapping.ExternalKey) require.Equal(t, role.ID, mapping.InternalRoleID) require.Equal(t, roleKey, mapping.InternalRoleKey) listEnv := c.DoExpectOK(t, http.MethodGet, "/api/v1/permissions/role-mappings?source=zitadel", nil, true) var list struct { Mappings []struct { ExternalKey string `json:"external_key"` } `json:"mappings"` } require.NoError(t, json.Unmarshal(listEnv.Data, &list)) found := false for _, item := range list.Mappings { if item.ExternalKey == externalKey { found = true break } } require.True(t, found, "created role mapping should appear in list") c.DoExpectOK(t, http.MethodDelete, "/api/v1/permissions/role-mappings", map[string]string{ "external_source": "zitadel", "external_key": externalKey, }, true) c.DoExpectOK(t, http.MethodDelete, "/api/v1/permissions/roles/"+role.ID, nil, true) } func TestPermission_CasbinRBAC(t *testing.T) { e2eStep(t, "P-13/P-14", "*", "/api/v1/permissions/{policy/reload,roles}", "Casbin enforcement:owner reload policy → no-role user GET /roles=403") if os.Getenv("E2E_CASBIN") != "1" { t.Skip("set E2E_CASBIN=1 and use e2e.casbin.yaml to enable Casbin enforcement") } owner := NewClient(t) reloadEnv := owner.DoExpectOK(t, http.MethodPost, "/api/v1/permissions/policy/reload", nil, true) var reload struct { Tenant string `json:"tenant"` TS int64 `json:"ts"` } require.NoError(t, json.Unmarshal(reloadEnv.Data, &reload)) require.Equal(t, owner.Fixture.TenantID, reload.Tenant) require.Positive(t, reload.TS) noRole := NewNoRoleClient(t) denied := noRole.DoExpectHTTP(t, http.MethodGet, "/api/v1/permissions/roles", nil, true, http.StatusForbidden) require.NotEqual(t, int64(successCode), denied.Code) } func firstCatalogPermissionID(t *testing.T, c *Client) string { t.Helper() env := c.DoExpectOK(t, http.MethodGet, "/api/v1/permissions/catalog?tree=false", nil, true) var data struct { List []struct { ID string `json:"id"` } `json:"list"` } require.NoError(t, json.Unmarshal(env.Data, &data)) require.NotEmpty(t, data.List) require.NotEmpty(t, data.List[0].ID) return data.List[0].ID }