{"record":{"id":"1003b65c9fcd1fc1","repo":"toeverything/AFFiNE","slug":"action-forbidden-on-non-team-workspace","errorCode":"action_forbidden_on_non_team_workspace","errorMessage":"A Team workspace is required to perform this action.","messagePattern":"A Team workspace is required to perform this action\\.","errorType":"http","errorClass":"ActionForbiddenOnNonTeamWorkspace","httpStatus":403,"severity":"error","filePath":"packages/backend/server/src/core/workspaces/resolvers/member.ts","lineNumber":591,"sourceCode":"      .assert(\n        newRole === WorkspaceRole.Owner\n          ? 'Workspace.TransferOwner'\n          : 'Workspace.Users.Manage'\n      );\n\n    const role = await this.models.workspaceUser.get(workspaceId, userId);\n\n    if (!role) {\n      throw new MemberNotFoundInSpace({ spaceId: workspaceId });\n    }\n\n    if (newRole === WorkspaceRole.Owner) {\n      await this.models.workspaceUser.setOwner(workspaceId, userId);\n    } else {\n      // non-team workspace can only transfer ownership, but no detailed permission control\n      const isTeam = await this.workspaceService.isTeamWorkspace(workspaceId);\n      if (!isTeam) {\n        throw new ActionForbiddenOnNonTeamWorkspace();\n      }\n\n      await this.models.workspaceUser.set(workspaceId, userId, newRole);\n      if (role.status !== WorkspaceMemberStatus.Accepted) {\n        this.event.emit('workspace.members.updated', {\n          workspaceId,\n        });\n      }\n    }\n\n    return true;\n  }\n\n  @Throttle('strict')\n  @Public()\n  @Query(() => InvitationType, {\n    description: 'get workspace invitation info',\n  })","sourceCodeStart":573,"sourceCodeEnd":609,"githubUrl":"https://github.com/toeverything/AFFiNE/blob/26c515e050211269e911f7d9cfe162a26c83ed98/packages/backend/server/src/core/workspaces/resolvers/member.ts#L573-L609","documentation":"Thrown by grantMember when changing to a non-Owner role on a workspace that is not a Team workspace. Non-team workspaces only support transferring ownership; fine-grained per-member roles require a Team plan/workspace. The check runs after the membership lookup, only on the non-Owner branch. Reported as action_forbidden_on_non_team_workspace (action_forbidden, HTTP 403).","triggerScenarios":"Calling grantMember with newRole != WorkspaceRole.Owner on a workspace where workspaceService.isTeamWorkspace returns false.","commonSituations":"A role-management UI is shown on a non-team (free/personal) workspace; the client assumes all workspaces support arbitrary roles; attempting to demote a member on a basic workspace.","solutions":["On non-team workspaces, only call grantMember to transfer ownership (newRole === WorkspaceRole.Owner).","Upgrade the workspace to a Team plan to enable fine-grained role changes.","Hide non-owner role controls in the UI when isTeamWorkspace is false."],"exampleFix":"// before (non-team workspace)\ngrantMember(workspaceId, userId, WorkspaceRole.Collaborator);\n// after\nif (isTeamWorkspace) grantMember(workspaceId, userId, WorkspaceRole.Collaborator);\nelse grantMember(workspaceId, userId, WorkspaceRole.Owner); // transfer only","handlingStrategy":"validation","validationCode":"const isTeam = await isTeamWorkspace(workspaceId);\nif (!isTeam && newRole !== WorkspaceRole.Owner) {\n  throw new Error('Non-team workspaces only support transferring ownership');\n}","typeGuard":"function canGrantRole(isTeam: boolean, role: WorkspaceRole): boolean {\n  return isTeam || role === WorkspaceRole.Owner;\n}","tryCatchPattern":null,"preventionTips":["Hide non-owner role controls on non-team workspaces.","Upgrade to a Team plan to enable fine-grained roles.","On non-team workspaces, only call grantMember to transfer ownership."],"tags":["graphql","permissions","workspace","billing","validation"],"backgroundTag":null,"analyzedSha":"26c515e050211269e911f7d9cfe162a26c83ed98","analyzedAt":"2026-08-12T13:15:16.447Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}