1
0
Fork 0
mirror of https://github.com/anyproto/anytype-kotlin.git synced 2025-06-08 05:47:05 +09:00

DROID-2192 Tech | Object permissions (#1986)

This commit is contained in:
Konstantin Ivanov 2025-01-13 12:48:44 +01:00 committed by GitHub
parent c1b0263dfa
commit 210b7e70a3
Signed by: github
GPG key ID: B5690EEEBB952194
5 changed files with 509 additions and 2 deletions

View file

@ -86,6 +86,7 @@ object Relations {
const val SPACE_LOCAL_STATUS = "spaceLocalStatus"
const val IDENTITY_PROFILE_LINK = "identityProfileLink"
const val PROFILE_OWNER_IDENTITY = "profileOwnerIdentity"
const val PARTICIPANT_STATUS = "participantStatus"
const val PARTICIPANT_PERMISSIONS = "participantPermissions"

View file

@ -0,0 +1,6 @@
package com.anytypeio.anytype.core_models.permissions
sealed class EditBlocksPermission {
data object Edit : EditBlocksPermission()
data object ReadOnly: EditBlocksPermission()
}

View file

@ -0,0 +1,187 @@
package com.anytypeio.anytype.core_models.permissions
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.core_models.ObjectTypeIds
import com.anytypeio.anytype.core_models.ObjectView
import com.anytypeio.anytype.core_models.Relations
import com.anytypeio.anytype.core_models.SupportedLayouts
import com.anytypeio.anytype.core_models.getSingleValue
import com.anytypeio.anytype.core_models.restrictions.ObjectRestriction
/**
* Represents a set of user permissions for a given object.
*
* @property canArchive Indicates whether this object can be archived.
* @property canDelete Indicates whether this object can be permanently deleted.
* @property canChangeType Indicates whether the object's type can be changed.
* @property canTemplateSetAsDefault Indicates whether this template object can be set as the default for a type.
* @property canApplyTemplates Indicates whether templates can be applied to this object.
* @property canMakeAsTemplate Indicates whether this object can be turned into a template.
* @property canDuplicate Indicates whether this object can be duplicated.
* @property canUndoRedo Indicates whether undo and redo operations are allowed for this object.
* @property canFavorite Indicates whether this object can be marked as a favorite.
* @property canLinkItself Indicates whether this object can be linked to itself (if applicable).
* @property canLock Indicates whether this object can be locked.
* @property canChangeIcon Indicates whether the object icon can be changed.
* @property canChangeCover Indicates whether the object cover can be changed.
* @property canChangeLayout Indicates whether the layout of the object can be changed.
* @property canEditRelationValues Indicates whether relation values on this object can be edited.
* @property canEditRelationsList Indicates whether the list of relations for this object can be edited.
* @property canEditBlocks Indicates whether blocks in this object can be edited.
* @property canEditDetails Indicates whether general details on this object can be edited.
* @property editBlocks Specifies the permission level regarding block editing (e.g., read-only vs. editable).
* @property canCreateObjectThisType Indicates whether object with this type can be created.
*/
data class ObjectPermissions(
val canArchive: Boolean = false,
val canDelete: Boolean = false,
val canChangeType: Boolean = false,
val canTemplateSetAsDefault: Boolean = false,
val canApplyTemplates: Boolean = false,
val canMakeAsTemplate: Boolean = false,
val canDuplicate: Boolean = false,
val canUndoRedo: Boolean = false,
val canFavorite: Boolean = false,
val canLinkItself: Boolean = false,
val canLock: Boolean = false,
val canChangeIcon: Boolean = false,
val canChangeCover: Boolean = false,
val canChangeLayout: Boolean = false,
val canEditRelationValues: Boolean = false,
val canEditRelationsList: Boolean = false,
val canEditBlocks: Boolean = false,
val canEditDetails: Boolean = false,
val editBlocks: EditBlocksPermission,
val canCreateObjectThisType: Boolean = false
)
/**
* Converts this [ObjectView] instance into an [ObjectPermissions] by inspecting
* its current state (e.g., archived, locked) and the user's participant edit rights.
*
* @param participantCanEdit Flag indicating whether the participant can perform edits.
* @return An [ObjectPermissions] instance that encapsulates the allowed actions.
*/
fun ObjectView.toObjectPermissions(
participantCanEdit: Boolean
): ObjectPermissions {
val rootBlock = blocks.find { it.id == root }
val isLocked = rootBlock?.fields?.isLocked == true
val isArchived = details[root]?.getSingleValue<Boolean>(Relations.IS_ARCHIVED) == true
val objTypeId = details[root]?.getSingleValue<String>(Relations.TYPE)
val typeUniqueKey = if (objTypeId != null) {
details[objTypeId]?.getSingleValue<Id>(Relations.TYPE_UNIQUE_KEY)
} else {
null
}
val isTemplateObject = (typeUniqueKey == ObjectTypeIds.TEMPLATE)
val currentLayout = when (val value = details[root]?.getOrDefault(Relations.LAYOUT, null)) {
is Double -> ObjectType.Layout.entries.singleOrNull { layout ->
layout.code == value.toInt()
}
else -> null
}
val canEditRelations = !isLocked && !isArchived && participantCanEdit
val canEdit = canEditRelations && !SupportedLayouts.isFileLayout(currentLayout)
val canApplyUneditableActions = !isArchived && participantCanEdit
val isProfileOwnerIdentity =
details[root]?.getSingleValue<String>(Relations.PROFILE_OWNER_IDENTITY)
val canEditDetails = !objectRestrictions.contains(ObjectRestriction.DETAILS)
val editBlocksPermission = when {
isLocked -> EditBlocksPermission.ReadOnly
isArchived -> EditBlocksPermission.ReadOnly
!participantCanEdit -> EditBlocksPermission.ReadOnly
objectRestrictions.contains(ObjectRestriction.BLOCKS) -> EditBlocksPermission.ReadOnly
else -> EditBlocksPermission.Edit
}
return ObjectPermissions(
canArchive = participantCanEdit && !objectRestrictions.contains(ObjectRestriction.DELETE) && !isArchived,
canDelete = participantCanEdit && !objectRestrictions.contains(ObjectRestriction.DELETE),
canChangeType = canEdit &&
!isTemplateObject &&
!objectRestrictions.contains(ObjectRestriction.TYPE_CHANGE),
canTemplateSetAsDefault = canEdit && isTemplateObject,
canApplyTemplates = canEdit && !isTemplateObject,
canMakeAsTemplate = templatesAllowedLayouts.contains(currentLayout) &&
!isTemplateObject &&
isProfileOwnerIdentity.isNullOrEmpty() &&
!objectRestrictions.contains(ObjectRestriction.TEMPLATE) &&
canApplyUneditableActions,
canDuplicate = canApplyUneditableActions && !objectRestrictions.contains(ObjectRestriction.DUPLICATE),
canUndoRedo = canEdit && undoRedoLayouts.contains(currentLayout),
canFavorite = canApplyUneditableActions && !isTemplateObject,
canLinkItself = canApplyUneditableActions && !isTemplateObject,
canLock = lockLayouts.contains(currentLayout) &&
canApplyUneditableActions &&
!isTemplateObject,
canChangeIcon = canEditDetails && layoutsWithIcon.contains(currentLayout) && canEdit,
canChangeCover = canEditDetails && layoutsWithCover.contains(currentLayout) && canEdit,
canChangeLayout = canEditDetails &&
possibleToChangeLayoutLayouts.contains(currentLayout) &&
canEdit,
canEditRelationValues = canEditRelations && canEditDetails,
canEditRelationsList = canEditRelations &&
canEditDetails &&
!objectRestrictions.contains(ObjectRestriction.RELATIONS),
canEditBlocks = (editBlocksPermission == EditBlocksPermission.Edit),
canEditDetails = canEditDetails && canEdit,
editBlocks = editBlocksPermission,
canCreateObjectThisType = !objectRestrictions.contains(ObjectRestriction.CREATE_OBJECT_OF_THIS_TYPE) && canApplyUneditableActions
)
}
private val templatesAllowedLayouts = listOf(
ObjectType.Layout.BASIC,
ObjectType.Layout.PROFILE,
ObjectType.Layout.TODO
)
private val undoRedoLayouts = listOf(
ObjectType.Layout.BASIC,
ObjectType.Layout.PROFILE,
ObjectType.Layout.TODO,
ObjectType.Layout.NOTE,
ObjectType.Layout.BOOKMARK
)
private val lockLayouts = listOf(
ObjectType.Layout.BASIC,
ObjectType.Layout.PROFILE,
ObjectType.Layout.TODO,
ObjectType.Layout.NOTE,
ObjectType.Layout.BOOKMARK
)
private val layoutsWithIcon = listOf(
ObjectType.Layout.FILE,
ObjectType.Layout.IMAGE,
ObjectType.Layout.VIDEO,
ObjectType.Layout.AUDIO,
ObjectType.Layout.PDF,
ObjectType.Layout.SET,
ObjectType.Layout.COLLECTION,
ObjectType.Layout.BASIC,
ObjectType.Layout.PROFILE
)
private val layoutsWithCover = layoutsWithIcon + listOf(
ObjectType.Layout.TODO,
ObjectType.Layout.BOOKMARK
)
private val possibleToChangeLayoutLayouts = listOf(
ObjectType.Layout.BASIC,
ObjectType.Layout.PROFILE,
ObjectType.Layout.TODO,
ObjectType.Layout.NOTE
)

View file

@ -0,0 +1,312 @@
package com.anytypeio.anytype.presentation.objects
import com.anytypeio.anytype.core_models.Block
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.core_models.Relations
import com.anytypeio.anytype.core_models.StubObjectView
import com.anytypeio.anytype.core_models.StubSmartBlock
import com.anytypeio.anytype.core_models.permissions.EditBlocksPermission
import com.anytypeio.anytype.core_models.permissions.toObjectPermissions
import com.anytypeio.anytype.core_models.restrictions.ObjectRestriction
import kotlin.test.Test
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class ObjectPermissionsTest {
@Test
fun `Unlocked, not archived, participant can edit, BASIC layout`() {
// GIVEN: An ObjectView with:
// - root block is unlocked
// - BASIC layout
// - no object restrictions
// - participant can edit
val rootBlockId = "root-block"
val objectView = StubObjectView(
blocks = listOf(
StubSmartBlock(
id = rootBlockId
)
),
root = rootBlockId,
details = mapOf(
rootBlockId to mapOf(
Relations.ID to rootBlockId,
Relations.LAYOUT to ObjectType.Layout.BASIC.code.toDouble()
)
)
)
// WHEN
val permissions = objectView.toObjectPermissions(participantCanEdit = true)
// THEN
assertTrue(permissions.canArchive)
assertTrue(permissions.canDelete)
assertTrue(permissions.canChangeType)
assertTrue(permissions.canUndoRedo)
assertTrue(permissions.canChangeLayout)
assertTrue(permissions.canEditRelationValues)
assertTrue(permissions.canEditRelationsList)
assertTrue(permissions.canEditBlocks)
assertTrue(permissions.canEditDetails)
assertTrue(permissions.editBlocks is EditBlocksPermission.Edit)
assertTrue(permissions.canDuplicate)
assertTrue(permissions.canLinkItself)
assertTrue(permissions.canLock)
assertTrue(permissions.canChangeIcon)
assertTrue(permissions.canChangeCover)
assertTrue(permissions.canMakeAsTemplate)
assertTrue(permissions.canApplyTemplates)
assertTrue(permissions.canFavorite)
}
@Test
fun `Unlocked, not archived, participant cannot edit`() {
// GIVEN: An ObjectView with:
// - root block is unlocked
// - BASIC layout
// - no object restrictions
// - participant can edit
val rootBlockId = "root-block"
val objectView = StubObjectView(
blocks = listOf(
StubSmartBlock(
id = rootBlockId
)
),
root = rootBlockId,
details = mapOf(
rootBlockId to mapOf(
Relations.ID to rootBlockId,
Relations.LAYOUT to ObjectType.Layout.BASIC.code.toDouble()
)
)
)
// WHEN
val permissions = objectView.toObjectPermissions(participantCanEdit = false)
// THEN
assertFalse(permissions.canArchive)
assertFalse(permissions.canDelete)
assertFalse(permissions.canChangeType)
assertFalse(permissions.canUndoRedo)
assertFalse(permissions.canChangeLayout)
assertFalse(permissions.canEditRelationValues)
assertFalse(permissions.canEditRelationsList)
assertFalse(permissions.canEditBlocks)
assertFalse(permissions.canEditDetails)
assertTrue(permissions.editBlocks == EditBlocksPermission.ReadOnly)
assertFalse(permissions.canDuplicate)
assertFalse(permissions.canLinkItself)
assertFalse(permissions.canLock)
assertFalse(permissions.canChangeIcon)
assertFalse(permissions.canChangeCover)
assertFalse(permissions.canMakeAsTemplate)
assertFalse(permissions.canApplyTemplates)
assertFalse(permissions.canFavorite)
}
@Test
fun `Locked, not archived, participant can edit, BASIC layout`() {
// GIVEN: An ObjectView with:
// - root block is locked
// - BASIC layout
// - participant can edit
// - no additional object restrictions
val rootBlockId = "root-block"
val objectView = StubObjectView(
blocks = listOf(
StubSmartBlock(
id = rootBlockId,
fields = Block.Fields(
mapOf(
Block.Fields.IS_LOCKED_KEY to true
)
)
)
),
root = rootBlockId,
details = mapOf(
rootBlockId to mapOf(
Relations.ID to rootBlockId,
Relations.LAYOUT to ObjectType.Layout.BASIC.code.toDouble(),
)
)
)
// WHEN
val permissions = objectView.toObjectPermissions(participantCanEdit = true)
// THEN
// Because the block is locked, editing blocks is read-only, but other actions may be allowed.
assertTrue(permissions.canArchive)
assertTrue(permissions.canDelete)
assertFalse(permissions.canEditBlocks)
assertTrue(permissions.editBlocks == EditBlocksPermission.ReadOnly)
assertFalse(permissions.canChangeType)
assertFalse(permissions.canChangeIcon)
assertFalse(permissions.canChangeCover)
assertTrue(permissions.canFavorite)
}
@Test
fun `Archived, participant can edit, BASIC layout`() {
// GIVEN: An ObjectView with:
// - root block is unlocked
// - BASIC layout
// - object is archived
// - participant can edit
val rootBlockId = "root-block"
val objectView = StubObjectView(
blocks = listOf(
StubSmartBlock(id = rootBlockId)
),
root = rootBlockId,
details = mapOf(
rootBlockId to mapOf(
Relations.ID to rootBlockId,
Relations.LAYOUT to ObjectType.Layout.BASIC.code.toDouble(),
Relations.IS_ARCHIVED to true
)
)
)
// WHEN
val permissions = objectView.toObjectPermissions(participantCanEdit = true)
// THEN
// An archived object can often be deleted, but not re-archived or edited.
assertFalse(permissions.canArchive, "Cannot archive an already archived object.")
assertTrue(permissions.canDelete, "Archived object can typically be deleted if user can edit.")
assertFalse(permissions.canEditBlocks, "Archived objects are read-only for blocks.")
assertTrue(permissions.editBlocks == EditBlocksPermission.ReadOnly)
assertFalse(permissions.canChangeLayout, "Changing layout is disallowed if object is archived.")
assertFalse(permissions.canChangeIcon, "Changing icon is disallowed if object is archived.")
assertFalse(permissions.canMakeAsTemplate, "Cannot make an archived object into a template.")
}
@Test
fun `Archived, participant cannot edit, BASIC layout`() {
// GIVEN: An ObjectView with:
// - root block is unlocked
// - BASIC layout
// - object is archived
// - participant cannot edit
val rootBlockId = "root-block"
val objectView = StubObjectView(
blocks = listOf(
StubSmartBlock(id = rootBlockId)
),
root = rootBlockId,
details = mapOf(
rootBlockId to mapOf(
Relations.ID to rootBlockId,
Relations.LAYOUT to ObjectType.Layout.BASIC.code.toDouble(),
Relations.IS_ARCHIVED to true
)
)
)
// WHEN
val permissions = objectView.toObjectPermissions(participantCanEdit = false)
// THEN
// Participant can't edit => effectively all edit actions are false.
assertFalse(permissions.canArchive, "Already archived and user can't edit => no re-archiving.")
assertFalse(permissions.canDelete, "Cannot delete if participant can't edit.")
assertFalse(permissions.canEditBlocks, "Can't edit blocks if participant can't edit overall.")
assertTrue(permissions.editBlocks == EditBlocksPermission.ReadOnly)
assertFalse(permissions.canMakeAsTemplate, "Cannot make a template if you have no edit rights.")
assertFalse(permissions.canChangeIcon, "No editing privileges => cannot change icon.")
assertFalse(permissions.canChangeCover, "No editing privileges => cannot change cover.")
assertFalse(permissions.canDuplicate, "No editing privileges => cannot duplicate.")
}
@Test
fun `Unlocked, not archived, participant can edit, NOTE layout`() {
// GIVEN: An ObjectView with:
// - root block is unlocked
// - NOTE layout
// - participant can edit
val rootBlockId = "root-block"
val objectView = StubObjectView(
blocks = listOf(
StubSmartBlock(id = rootBlockId)
),
root = rootBlockId,
details = mapOf(
rootBlockId to mapOf(
Relations.ID to rootBlockId,
Relations.LAYOUT to ObjectType.Layout.NOTE.code.toDouble(),
)
)
)
// WHEN
val permissions = objectView.toObjectPermissions(participantCanEdit = true)
// THEN
// NOTE layout is part of undoRedoLayouts, so undo/redo is allowed if unlocked & not archived.
assertTrue(permissions.canArchive, "Unlocked + participant can edit => can archive.")
assertTrue(permissions.canDelete, "archived or not => can't delete.")
assertTrue(permissions.canUndoRedo, "NOTE layout supports undo/redo.")
assertTrue(permissions.canEditBlocks, "Not locked + not archived + can edit => can edit blocks.")
assertTrue(permissions.canEditDetails, "No restrictions => can edit details.")
assertTrue(permissions.canFavorite, "Favoriting is typically allowed if participant can edit.")
// etc.
}
@Test
fun `Unlocked, not archived, participant can edit, BASIC layout, with DELETE & RELATIONS & BLOCKS restrictions`() {
// GIVEN: An ObjectView with:
// - root block is unlocked
// - BASIC layout
// - objectRestrictions contains DELETE, RELATIONS, and BLOCKS
// - participant can edit
val rootBlockId = "root-block"
val objectView = StubObjectView(
blocks = listOf(
StubSmartBlock(
id = rootBlockId
)
),
root = rootBlockId,
details = mapOf(
rootBlockId to mapOf(
Relations.ID to rootBlockId,
Relations.LAYOUT to ObjectType.Layout.BASIC.code.toDouble(),
Relations.IS_ARCHIVED to false
)
),
objectRestrictions = listOf(
ObjectRestriction.DELETE,
ObjectRestriction.RELATIONS,
ObjectRestriction.BLOCKS,
ObjectRestriction.DETAILS
)
)
// WHEN
val permissions = objectView.toObjectPermissions(participantCanEdit = true)
// THEN
// Because of DELETE restriction => cannot archive or delete.
assertFalse(permissions.canArchive, "DELETE restriction => cannot archive.")
assertFalse(permissions.canDelete, "DELETE restriction => cannot delete.")
// Because of BLOCKS restriction => cannot edit blocks.
assertFalse(permissions.canEditBlocks, "BLOCKS restriction => cannot edit blocks.")
assertTrue(permissions.editBlocks == EditBlocksPermission.ReadOnly)
// Because of RELATIONS restriction => cannot edit relations.
assertFalse(permissions.canEditRelationsList)
// Because of DETAILS restriction => cannot edit relations values.
assertFalse(permissions.canEditRelationValues)
// However, participant can still do other changes if not restricted:
assertTrue(permissions.canChangeType, "Still allowed to change type unless restricted specifically.")
assertTrue(permissions.canUndoRedo, "BASIC layout => supports undo/redo if user can edit.")
assertTrue(permissions.canFavorite, "Favoriting is not restricted by DELETE, BLOCKS, or RELATIONS.")
}
}

View file

@ -254,11 +254,12 @@ fun StubBookmark(
fun StubSmartBlock(
id: Id = MockDataFactory.randomString(),
children: List<Id> = emptyList()
children: List<Id> = emptyList(),
fields: Block.Fields = Block.Fields.empty(),
): Block = Block(
id = id,
children = children,
fields = Block.Fields.empty(),
fields = fields,
content = Block.Content.Smart
)