{"record":{"id":"b2d19c33b7404f1c","repo":"theonedev/onedev","slug":"issue-schedule-permission-required-to-set-own-esti-b2d19c","errorCode":null,"errorMessage":"Issue schedule permission required to set own estimated time","messagePattern":"Issue schedule permission required to set own estimated time","errorType":"http","errorClass":"UnauthorizedException","httpStatus":403,"severity":"error","filePath":"server-core/src/main/java/io/onedev/server/ai/TodResource.java","lineNumber":628,"sourceCode":"                throw new UnauthorizedException(\"No permission to update issue description\");\n            issueChangeService.changeDescription(user, issue, (String) data.remove(\"description\"));\n        }\n\n        var confidential = (Boolean) data.remove(\"confidential\");\n        if (confidential != null) {\n            if (!SecurityUtils.canModifyIssue(subject, issue))\n                throw new UnauthorizedException(\"No permission to update issue confidential\");\n            issueChangeService.changeConfidential(user, issue, confidential);\n        }\n\n        Integer ownEstimatedTime = (Integer) data.remove(\"ownEstimatedTime\");\n        if (ownEstimatedTime != null) {\n            if (!subscriptionService.isSubscriptionActive())\n                throw new NotAcceptableException(\"An active subscription is required for this feature\");\n            if (!issue.getProject().isTimeTracking())\n                throw new NotAcceptableException(\"Time tracking needs to be enabled for the project\");\n            if (!SecurityUtils.canScheduleIssues(subject, issue.getProject()))\n                throw new UnauthorizedException(\"Issue schedule permission required to set own estimated time\");\n            issueChangeService.changeOwnEstimatedTime(user, issue, ownEstimatedTime*60);\n        }\n\n        @SuppressWarnings(\"unchecked\")\n        List<String> iterationNames = (List<String>) data.remove(\"iterations\");\n        if (iterationNames != null) {\n            if (!SecurityUtils.canScheduleIssues(subject, issue.getProject()))\n                throw new UnauthorizedException(\"Issue schedule permission required to set iterations\");\n            var iterations = new ArrayList<Iteration>();\n            for (var iterationName : iterationNames) {\n                var iteration = iterationService.findInHierarchy(issue.getProject(), iterationName);\n                if (iteration == null)\n                    throw new NotFoundException(\"Iteration '\" + iterationName + \"' not found\");\n                iterations.add(iteration);\n            }\n            issueChangeService.changeIterations(user, issue, iterations);\n        }\n","sourceCodeStart":610,"sourceCodeEnd":646,"githubUrl":"https://github.com/theonedev/onedev/blob/d44925c47c37992c828ea673a5f9620539bc3ff2/server-core/src/main/java/io/onedev/server/ai/TodResource.java#L610-L646","documentation":"After subscription and time-tracking checks pass, editIssue verifies SecurityUtils.canScheduleIssues(subject, issue.getProject()); failing that it throws UnauthorizedException(\"Issue schedule permission required to set own estimated time\"). This is a distinct scheduling authorization separate from general issue-modify rights.","triggerScenarios":"Sending \"ownEstimatedTime\" in the edit-issue payload with an authenticated user who can modify the issue but lacks the schedule-issues permission on the project (subscription active, time tracking enabled).","commonSituations":"Developers with edit rights but without scheduling role trying to log estimates; service tokens with narrow grants; role changes removing schedule permission while tools still attempt time updates.","solutions":["Grant the user the permission that allows scheduling issues on the project (project role/authorizations covering schedule issues).","Use a token of an account that already has schedule permission for time-tracking updates.","Remove \"ownEstimatedTime\" from automated payloads if schedule permission cannot be granted.","Verify the permission in the UI (if the estimated-time field is editable there, the API call should succeed)."],"exampleFix":"// before\neditIssue(project, ref, {ownEstimatedTime: 2}) // 403 schedule perm\n// after\n// admin grants schedule-issues permission to the user, then:\neditIssue(project, ref, {ownEstimatedTime: 2})","handlingStrategy":"validation","validationCode":"// check schedule permission before sending estimated time\nconst canSchedule = await userCanScheduleIssues(projectPath); // via permission API or role config\nif ('ownEstimatedTime' in payload && !canSchedule) {\n  delete payload.ownEstimatedTime;\n  throw new Error('User lacks schedule-issues permission on project');\n}","typeGuard":null,"tryCatchPattern":"try { await editIssue(projectPath, ref, {ownEstimatedTime}); } catch (e) { if (e.status === 403 && /schedule/i.test(e.message)) { requestPermissionGrant(); } else throw e; }","preventionTips":["Confirm the estimated-time field is editable in the UI for the same user before API calls","Grant schedule-issues permission to automation accounts explicitly","Audit roles after permission changes that may break time-tracking integrations","Separate time-tracking writes from other issue edits so 403s are isolated"],"tags":["authorization","permissions","http-403","time-tracking"],"backgroundTag":"insufficient-permissions","analyzedSha":"d44925c47c37992c828ea673a5f9620539bc3ff2","analyzedAt":"2026-09-06T07:18:27.995Z","contentChangedAt":"2026-09-06T07:18:27.995Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}