Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions config-raw.json
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,16 @@
"default_value": "false",
"comment": "Enables the public team feature. If enabled, it is possible to configure teams to be public, which makes them\ndiscoverable when sharing a project, therefore not only showing teams the user is member of."
},
{
"key": "enablereadercomments",
"default_value": "true",
"comment": "Whether to enable comments for users with read access."
},
{
"key": "enableassigneeedit",
"default_value": "true",
"comment": "Whether to enable task editing for users with read access who are assigned to the task."
},
{
"key": "bcryptrounds",
"default_value": "11",
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/components/project/views/ProjectKanban.vue
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,9 @@ const bucketDraggableComponentData = computed(() => ({
}))
const project = computed(() => projectId.value ? projectStore.projects[projectId.value] : null)
const view = computed(() => project.value?.views.find(v => v.id === props.viewId) as IProjectView || null)
const canWrite = computed(() => baseStore.currentProject?.maxPermission > Permissions.READ && view.value.bucketConfigurationMode === 'manual')
const canWrite = computed(() => {
return baseStore.currentProject?.maxPermission > Permissions.READ && view.value.bucketConfigurationMode === 'manual'
})
const canCreateTasks = computed(() => canWrite.value && projectId.value > 0)
const buckets = computed(() => kanbanStore.buckets)
const loading = computed(() => kanbanStore.isLoading)
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/stores/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ export interface ConfigState {
},
},
publicTeamsEnabled: boolean,
readerCommentsEnabled: boolean,
assigneeEditEnabled: boolean,
}

export const useConfigStore = defineStore('config', () => {
Expand Down Expand Up @@ -81,6 +83,8 @@ export const useConfigStore = defineStore('config', () => {
},
},
publicTeamsEnabled: false,
readerCommentsEnabled: false,
assigneeEditEnabled: false,
})

const migratorsEnabled = computed(() => state.availableMigrators?.length > 0)
Expand Down
33 changes: 28 additions & 5 deletions frontend/src/views/tasks/TaskDetailView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,7 @@

<!-- Comments -->
<Comments
:can-write="canWrite"
:can-write="canComment"
:task-id="taskId"
:project-id="task.projectId"
:initial-comments="task.comments"
Expand Down Expand Up @@ -668,6 +668,7 @@ import {useKanbanStore} from '@/stores/kanban'
import {useProjectStore} from '@/stores/projects'
import {useAuthStore} from '@/stores/auth'
import {useBaseStore} from '@/stores/base'
import {useConfigStore} from '@/stores/config'

import {useTitle} from '@/composables/useTitle'

Expand All @@ -694,6 +695,7 @@ const taskStore = useTaskStore()
const kanbanStore = useKanbanStore()
const authStore = useAuthStore()
const baseStore = useBaseStore()
const configStore = useConfigStore()

const task = ref<ITask>(new TaskModel())
const taskNotFound = ref(false)
Expand Down Expand Up @@ -786,10 +788,31 @@ const projectRoute = computed(() => ({
hash: route.hash,
}))

const canWrite = computed(() => (
task.value.maxPermission !== null &&
task.value.maxPermission > PERMISSIONS.READ
))
const canWrite = computed(() => {
if (task.value.maxPermission !== null && task.value.maxPermission > PERMISSIONS.READ) {
return true
}

if (configStore.assigneeEditEnabled && task.value.maxPermission !== null && task.value.maxPermission >= PERMISSIONS.READ) {
// Executor feature: Creator or Assignee can edit if enabled
const userId = authStore.info?.id
if (userId && task.value.id) {
const isCreator = task.value.createdBy?.id === userId
const isAssignee = task.value.assignees?.some(a => a.id === userId)
if (isCreator || isAssignee) {
return true
}
}
}
return false
})

const canComment = computed(() => {
if (canWrite.value) {
return true
}
return configStore.readerCommentsEnabled && task.value.maxPermission !== null && task.value.maxPermission >= PERMISSIONS.READ
})

const color = computed(() => {
const color = task.value.getHexColor
Expand Down
4 changes: 4 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ const (
ServiceEnablePublicTeams Key = `service.enablepublicteams`
ServiceBcryptRounds Key = `service.bcryptrounds`
ServiceEnableOpenIDTeamUserOnlySearch Key = `service.enableopenidteamusersearch`
ServiceEnableReaderComments Key = `service.enablereadercomments`
ServiceEnableAssigneeEdit Key = `service.enableassigneeedit`

SentryEnabled Key = `sentry.enabled`
SentryDsn Key = `sentry.dsn`
Expand Down Expand Up @@ -356,6 +358,8 @@ func InitDefaultConfig() {
ServiceEnablePublicTeams.setDefault(false)
ServiceBcryptRounds.setDefault(11)
ServiceEnableOpenIDTeamUserOnlySearch.setDefault(false)
ServiceEnableReaderComments.setDefault(false)
ServiceEnableAssigneeEdit.setDefault(false)

// Sentry
SentryDsn.setDefault("https://[email protected]/4504254983634944")
Expand Down
11 changes: 11 additions & 0 deletions pkg/models/task_comment_permissions.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package models

import (
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/web"
"xorm.io/xorm"
)
Expand Down Expand Up @@ -67,5 +68,15 @@ func (tc *TaskComment) CanUpdate(s *xorm.Session, a web.Auth) (bool, error) {
// CanCreate checks if a user can create a new comment
func (tc *TaskComment) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
t := Task{ID: tc.TaskID}
if config.ServiceEnableReaderComments.GetBool() {
canRead, _, err := t.CanRead(s, a)
if err != nil {
return false, err
}
if canRead {
return true, nil
}
}
return t.CanWrite(s, a)

}
56 changes: 54 additions & 2 deletions pkg/models/tasks_permissions.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package models

import (
"code.vikunja.io/api/pkg/config"
"code.vikunja.io/api/pkg/web"
"xorm.io/xorm"
)
Expand All @@ -28,14 +29,65 @@ func (t *Task) CanDelete(s *xorm.Session, a web.Auth) (bool, error) {

// CanUpdate determines if a user has the permission to update a project task
func (t *Task) CanUpdate(s *xorm.Session, a web.Auth) (bool, error) {
return t.canDoTask(s, a)
authorized, err := t.canDoTask(s, a)
if err != nil {
return false, err
}
if authorized {
return true, nil
}

if config.ServiceEnableAssigneeEdit.GetBool() {
ot, err := GetTaskByIDSimple(s, t.ID)
if err != nil {
return false, err
}

if t.ProjectID != 0 && t.ProjectID != ot.ProjectID {
return false, nil
}

if ot.CreatedByID == a.GetID() {
l := &Project{ID: ot.ProjectID}
canRead, _, err := l.CanRead(s, a)
if err != nil {
return false, err
}
return canRead, nil
}

// Check if the user is assigned to the task
exists, err := s.Where("task_id = ? AND user_id = ?", t.ID, a.GetID()).Exist(&TaskAssginee{})
if err != nil {
return false, err
}
if exists {
l := &Project{ID: ot.ProjectID}
canRead, _, err := l.CanRead(s, a)
if err != nil {
return false, err
}
return canRead, nil
}

}

return false, nil
}

// CanCreate determines if a user has the permission to create a project task
func (t *Task) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
// A user can do a task if he has write acces to its project
l := &Project{ID: t.ProjectID}
return l.CanWrite(s, a)
canWrite, err := l.CanWrite(s, a)
if err != nil {
return false, err
}
if canWrite {
return true, nil
}

return false, nil
}

// CanRead determines if a user can read a task
Expand Down
4 changes: 4 additions & 0 deletions pkg/routes/api/v1/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ type vikunjaInfos struct {
DemoModeEnabled bool `json:"demo_mode_enabled"`
WebhooksEnabled bool `json:"webhooks_enabled"`
PublicTeamsEnabled bool `json:"public_teams_enabled"`
ReaderCommentsEnabled bool `json:"reader_comments_enabled"`
AssigneeEditEnabled bool `json:"assignee_edit_enabled"`
}

type authInfo struct {
Expand Down Expand Up @@ -103,6 +105,8 @@ func Info(c echo.Context) error {
DemoModeEnabled: config.ServiceDemoMode.GetBool(),
WebhooksEnabled: config.WebhooksEnabled.GetBool(),
PublicTeamsEnabled: config.ServiceEnablePublicTeams.GetBool(),
ReaderCommentsEnabled: config.ServiceEnableReaderComments.GetBool(),
AssigneeEditEnabled: config.ServiceEnableAssigneeEdit.GetBool(),
AvailableMigrators: []string{
(&vikunja_file.FileMigrator{}).Name(),
(&ticktick.Migrator{}).Name(),
Expand Down