diff --git a/ontrack-extension-casc/src/test/java/net/nemerosa/ontrack/extension/casc/CascSecuritySettingsIT.kt b/ontrack-extension-casc/src/test/java/net/nemerosa/ontrack/extension/casc/CascSecuritySettingsIT.kt index c8db2e7c4bd..386e9e0376b 100644 --- a/ontrack-extension-casc/src/test/java/net/nemerosa/ontrack/extension/casc/CascSecuritySettingsIT.kt +++ b/ontrack-extension-casc/src/test/java/net/nemerosa/ontrack/extension/casc/CascSecuritySettingsIT.kt @@ -18,14 +18,16 @@ class CascSecuritySettingsIT : AbstractCascTestSupport() { fun `Security settings`() { withSettings { withNoGrantViewToAll { - casc(""" + casc( + """ ontrack: config: settings: security: grantProjectViewToAll: true grantProjectParticipationToAll: true - """.trimIndent()) + """.trimIndent() + ) // Checks the new settings val settings = cachedSettingsService.getCachedSettings(SecuritySettings::class.java) @@ -36,6 +38,32 @@ class CascSecuritySettingsIT : AbstractCascTestSupport() { } } + @Test + fun `Disabling dashboard rights`() { + withCleanSettings { + + val defaults = cachedSettingsService.getCachedSettings(SecuritySettings::class.java) + assertEquals(true, defaults.grantDashboardEditionToAll) + assertEquals(true, defaults.grantDashboardSharingToAll) + + casc( + """ + ontrack: + config: + settings: + security: + grantDashboardEditionToAll: false + grantDashboardSharingToAll: false + """.trimIndent() + ) + + // Checks the new settings + val settings = cachedSettingsService.getCachedSettings(SecuritySettings::class.java) + assertEquals(false, settings.grantDashboardEditionToAll) + assertEquals(false, settings.grantDashboardSharingToAll) + } + } + @Test fun `Rendering the settings`() { asAdmin { @@ -52,6 +80,8 @@ class CascSecuritySettingsIT : AbstractCascTestSupport() { "grantProjectViewToAll" to true, "grantProjectParticipationToAll" to true, "builtInAuthenticationEnabled" to true, + "grantDashboardEditionToAll" to true, + "grantDashboardSharingToAll" to true, ).asJson(), json ) @@ -72,13 +102,15 @@ class CascSecuritySettingsIT : AbstractCascTestSupport() { builtInAuthenticationEnabled = true, ) ) - casc(""" + casc( + """ ontrack: config: settings: security: builtInAuthenticationEnabled: false - """.trimIndent()) + """.trimIndent() + ) // Checks the new settings val settings = cachedSettingsService.getCachedSettings(SecuritySettings::class.java) diff --git a/ontrack-it-utils/src/main/java/net/nemerosa/ontrack/it/MockSecurityService.kt b/ontrack-it-utils/src/main/java/net/nemerosa/ontrack/it/MockSecurityService.kt index 39578098b0b..89cad86339e 100644 --- a/ontrack-it-utils/src/main/java/net/nemerosa/ontrack/it/MockSecurityService.kt +++ b/ontrack-it-utils/src/main/java/net/nemerosa/ontrack/it/MockSecurityService.kt @@ -22,6 +22,9 @@ class MockSecurityService : SecurityService { override val autoProjectFunctions: Set> get() = error("Not available in mock") + override val autoGlobalFunctions: Set> + get() = error("Not available in mock") + override val currentAccount: OntrackAuthenticatedUser? get() = error("Not available in mock") diff --git a/ontrack-model/src/main/java/net/nemerosa/ontrack/model/dashboards/DashboardGlobal.kt b/ontrack-model/src/main/java/net/nemerosa/ontrack/model/dashboards/DashboardGlobal.kt new file mode 100644 index 00000000000..594e9a4e8bf --- /dev/null +++ b/ontrack-model/src/main/java/net/nemerosa/ontrack/model/dashboards/DashboardGlobal.kt @@ -0,0 +1,6 @@ +package net.nemerosa.ontrack.model.dashboards + +/** + * Right to manage dashboards globally. + */ +interface DashboardGlobal: DashboardEdition, DashboardSharing diff --git a/ontrack-model/src/main/java/net/nemerosa/ontrack/model/security/Authorisations.kt b/ontrack-model/src/main/java/net/nemerosa/ontrack/model/security/Authorisations.kt index 1cff88215bb..1ba5b38595e 100644 --- a/ontrack-model/src/main/java/net/nemerosa/ontrack/model/security/Authorisations.kt +++ b/ontrack-model/src/main/java/net/nemerosa/ontrack/model/security/Authorisations.kt @@ -4,31 +4,53 @@ import java.io.Serializable import kotlin.reflect.KClass data class Authorisations( - private val projectFunctions: Set> = emptySet(), - private val globalRole: GlobalRole? = null, - private val projectRoleAssociations: Set = emptySet() + private val projectFunctions: Set> = emptySet(), + private val globalFunctions: Set> = emptySet(), + private val globalRole: GlobalRole? = null, + private val projectRoleAssociations: Set = emptySet() ) : AuthorisationsCheck, Serializable { companion object { @JvmStatic - fun none() = Authorisations(emptySet(), null, emptySet()) + fun none() = Authorisations( + projectFunctions = emptySet(), + globalFunctions = emptySet(), + globalRole = null, + projectRoleAssociations = emptySet(), + ) } - override fun isGranted(fn: Class) = globalRole != null && globalRole.isGlobalFunctionGranted(fn) + override fun isGranted(fn: Class) = + globalFunctions.map { it.java }.any { fn.isAssignableFrom(it) } || + (globalRole != null && globalRole.isGlobalFunctionGranted(fn)) override fun isGranted(projectId: Int, fn: Class) = - (globalRole != null && globalRole.isProjectFunctionGranted(fn)) - || projectFunctions.map { it.java }.any { fn.isAssignableFrom(it) } - || projectRoleAssociations.any { pa -> pa.projectId == projectId && pa.isGranted(fn) } + (globalRole != null && globalRole.isProjectFunctionGranted(fn)) + || projectFunctions.map { it.java }.any { fn.isAssignableFrom(it) } + || projectRoleAssociations.any { pa -> pa.projectId == projectId && pa.isGranted(fn) } fun withProjectFunctions(projectFunctions: Set>) = - Authorisations(projectFunctions, globalRole, projectRoleAssociations) + Authorisations(projectFunctions, globalFunctions, globalRole, projectRoleAssociations) - fun withGlobalRole(globalRole: GlobalRole?) = Authorisations(projectFunctions, globalRole, projectRoleAssociations) + fun withGlobalFunctions(globalFunctions: Set>) = + Authorisations(projectFunctions, globalFunctions, globalRole, projectRoleAssociations) + + fun withGlobalRole(globalRole: GlobalRole?) = + Authorisations(projectFunctions, globalFunctions, globalRole, projectRoleAssociations) fun withProjectRoles(projectRoleAssociations: Collection) = - Authorisations(projectFunctions, globalRole, this.projectRoleAssociations + projectRoleAssociations) + Authorisations( + projectFunctions, + globalFunctions, + globalRole, + this.projectRoleAssociations + projectRoleAssociations + ) fun withProjectRole(projectRoleAssociation: ProjectRoleAssociation) = - Authorisations(projectFunctions, globalRole, this.projectRoleAssociations + projectRoleAssociation) + Authorisations( + projectFunctions, + globalFunctions, + globalRole, + this.projectRoleAssociations + projectRoleAssociation + ) } diff --git a/ontrack-model/src/main/java/net/nemerosa/ontrack/model/security/RolesService.kt b/ontrack-model/src/main/java/net/nemerosa/ontrack/model/security/RolesService.kt index 5c6b8c6f459..bd4b9748224 100644 --- a/ontrack-model/src/main/java/net/nemerosa/ontrack/model/security/RolesService.kt +++ b/ontrack-model/src/main/java/net/nemerosa/ontrack/model/security/RolesService.kt @@ -1,6 +1,7 @@ package net.nemerosa.ontrack.model.security import net.nemerosa.ontrack.model.dashboards.DashboardEdition +import net.nemerosa.ontrack.model.dashboards.DashboardGlobal import net.nemerosa.ontrack.model.dashboards.DashboardSharing import net.nemerosa.ontrack.model.labels.LabelManagement import net.nemerosa.ontrack.model.labels.ProjectLabelManagement @@ -97,6 +98,7 @@ interface RolesService { ValidationStampBulkUpdate::class.java, DashboardEdition::class.java, DashboardSharing::class.java, + DashboardGlobal::class.java, ) /** diff --git a/ontrack-model/src/main/java/net/nemerosa/ontrack/model/security/SecurityService.kt b/ontrack-model/src/main/java/net/nemerosa/ontrack/model/security/SecurityService.kt index 2d16ddfa6f5..23058a7f17a 100644 --- a/ontrack-model/src/main/java/net/nemerosa/ontrack/model/security/SecurityService.kt +++ b/ontrack-model/src/main/java/net/nemerosa/ontrack/model/security/SecurityService.kt @@ -30,6 +30,11 @@ interface SecurityService { */ val autoProjectFunctions: Set> + /** + * List of [global functions][GlobalFunction] which are automatically assigned to authenticated users. + */ + val autoGlobalFunctions: Set> + /** * Returns the current logged account or `null` if none is logged. */ diff --git a/ontrack-model/src/main/java/net/nemerosa/ontrack/model/settings/SecuritySettings.kt b/ontrack-model/src/main/java/net/nemerosa/ontrack/model/settings/SecuritySettings.kt index 85ce900d66d..675ec4e604f 100644 --- a/ontrack-model/src/main/java/net/nemerosa/ontrack/model/settings/SecuritySettings.kt +++ b/ontrack-model/src/main/java/net/nemerosa/ontrack/model/settings/SecuritySettings.kt @@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonProperty import net.nemerosa.ontrack.model.annotations.APIDescription import net.nemerosa.ontrack.model.form.Form import net.nemerosa.ontrack.model.form.YesNo +import net.nemerosa.ontrack.model.form.yesNoField /** * General security settings. @@ -21,10 +22,16 @@ class SecuritySettings( val isGrantProjectParticipationToAll: Boolean, @APIDescription("Enabling the built-in authentication") val builtInAuthenticationEnabled: Boolean = DEFAULT_BUILTIN_AUTHENTICATION_ENABLED, + @APIDescription("Grants dashboard creation rights to all") + val grantDashboardEditionToAll: Boolean = DEFAULT_GRANT_DASHBOARD_EDITION, + @APIDescription("Grants dashboard sharing rights to all") + val grantDashboardSharingToAll: Boolean = DEFAULT_GRANT_DASHBOARD_SHARING, ) { companion object { const val DEFAULT_BUILTIN_AUTHENTICATION_ENABLED = true + const val DEFAULT_GRANT_DASHBOARD_EDITION = true + const val DEFAULT_GRANT_DASHBOARD_SHARING = true } fun form(): Form = @@ -48,4 +55,12 @@ class SecuritySettings( .help("Enabling the built-in authentication") .value(builtInAuthenticationEnabled) ) + .yesNoField( + SecuritySettings::grantDashboardEditionToAll, + grantDashboardEditionToAll + ) + .yesNoField( + SecuritySettings::grantDashboardSharingToAll, + grantDashboardSharingToAll + ) } diff --git a/ontrack-service/src/main/java/net/nemerosa/ontrack/service/dashboards/DashboardServiceImpl.kt b/ontrack-service/src/main/java/net/nemerosa/ontrack/service/dashboards/DashboardServiceImpl.kt index f9b2b93f5a0..c9e4be3499e 100644 --- a/ontrack-service/src/main/java/net/nemerosa/ontrack/service/dashboards/DashboardServiceImpl.kt +++ b/ontrack-service/src/main/java/net/nemerosa/ontrack/service/dashboards/DashboardServiceImpl.kt @@ -105,26 +105,45 @@ class DashboardServiceImpl( return dashboard } + /** + * Deletion of an existing dashboard. + * + * A built-in dashboard can never be deleted. + * + * A shared dashboard can be deleted if: + * + * * the user is granted the [DashboardGlobal] function + * * or the user owns this dashboard + * + * A private dashboard can be deleted if: + * + * * the user owns this dashboard + */ override fun deleteDashboard(uuid: String) { val existing = dashboardStorageService.findDashboardByUuid(uuid) ?: throw DashboardUuidNotFoundException(uuid) - when (existing.userScope) { + val account = securityService.currentAccount?.account + val ownsDashboard = account != null && dashboardStorageService.ownDashboard(uuid, account.id) + + val okToDelete: Boolean = when (existing.userScope) { DashboardContextUserScope.BUILT_IN -> throw DashboardCannotDeleteBuiltInException() - DashboardContextUserScope.SHARED -> securityService.checkGlobalFunction(DashboardSharing::class.java) - DashboardContextUserScope.PRIVATE -> securityService.checkGlobalFunction(DashboardEdition::class.java) + DashboardContextUserScope.SHARED -> ownsDashboard || securityService.isGlobalFunctionGranted() + DashboardContextUserScope.PRIVATE -> ownsDashboard } - val account = securityService.currentAccount?.account - if (account != null) { - val prefs = preferencesService.getPreferences(account) - if (prefs.dashboardUuid == uuid) { - prefs.dashboardUuid = null - preferencesService.setPreferences(account, prefs) + if (okToDelete) { + + if (account != null) { + val prefs = preferencesService.getPreferences(account) + if (prefs.dashboardUuid == uuid) { + prefs.dashboardUuid = null + preferencesService.setPreferences(account, prefs) + } } - } - dashboardStorageService.deleteDashboard(uuid) + dashboardStorageService.deleteDashboard(uuid) + } } override fun selectDashboard(uuid: String) { diff --git a/ontrack-service/src/main/java/net/nemerosa/ontrack/service/dashboards/DashboardStorageServiceImpl.kt b/ontrack-service/src/main/java/net/nemerosa/ontrack/service/dashboards/DashboardStorageServiceImpl.kt index f49531289a9..7dfbbac59c8 100644 --- a/ontrack-service/src/main/java/net/nemerosa/ontrack/service/dashboards/DashboardStorageServiceImpl.kt +++ b/ontrack-service/src/main/java/net/nemerosa/ontrack/service/dashboards/DashboardStorageServiceImpl.kt @@ -21,7 +21,7 @@ class DashboardStorageServiceImpl( store = STORE, type = StoredDashboard::class, size = MAX_DASHBOARDS, - query = "CAST(data->>'userId' as int) = :userId", + query = "CAST(data->>'userId' as int) = :userId AND data->'dashboard'->>'userScope' = 'PRIVATE'", queryVariables = mapOf("userId" to id.value) ).map { it.dashboard } @@ -30,20 +30,18 @@ class DashboardStorageServiceImpl( store = STORE, type = StoredDashboard::class, size = MAX_DASHBOARDS, - query = "data->>'userId' IS NULL", + query = "data->'dashboard'->>'userScope' = 'SHARED'", ).map { it.dashboard } override fun saveDashboard(dashboard: Dashboard): Dashboard { - val userId = if (dashboard.userScope == DashboardContextUserScope.PRIVATE) { - securityService.currentAccount?.id() - } else { - null + if (dashboard.userScope == DashboardContextUserScope.BUILT_IN) { + error("Cannot save built-in dashboards") } storageService.store( STORE, dashboard.uuid, StoredDashboard( - userId = userId, + userId = securityService.currentAccount?.id(), dashboard = dashboard, ) ) @@ -59,6 +57,12 @@ class DashboardStorageServiceImpl( storageService.delete(STORE, uuid) } + /** + * Stored dashboard + * + * @property userId Creator of the dashboard (null for backward compatibility) + * @property dashboard Dashboard definition + */ private data class StoredDashboard( val userId: Int?, val dashboard: Dashboard, diff --git a/ontrack-service/src/main/java/net/nemerosa/ontrack/service/security/AccountServiceImpl.kt b/ontrack-service/src/main/java/net/nemerosa/ontrack/service/security/AccountServiceImpl.kt index 8fae0cafdbc..148ed5957ab 100644 --- a/ontrack-service/src/main/java/net/nemerosa/ontrack/service/security/AccountServiceImpl.kt +++ b/ontrack-service/src/main/java/net/nemerosa/ontrack/service/security/AccountServiceImpl.kt @@ -1,6 +1,5 @@ package net.nemerosa.ontrack.service.security -import net.nemerosa.ontrack.common.getOrNull import net.nemerosa.ontrack.model.Ack import net.nemerosa.ontrack.model.exceptions.AccountDefaultAdminCannotDeleteException import net.nemerosa.ontrack.model.exceptions.AccountDefaultAdminCannotUpdateNameException @@ -18,6 +17,7 @@ import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import java.util.* import java.util.stream.Collectors +import kotlin.jvm.optionals.getOrNull @Service @Transactional @@ -39,6 +39,7 @@ class AccountServiceImpl( // Direct account authorisations val authorisations = Authorisations() .withProjectFunctions(securityService.autoProjectFunctions) + .withGlobalFunctions(securityService.autoGlobalFunctions) .withGlobalRole(roleRepository.findGlobalRoleByAccount(raw.accountId).getOrNull() ?.let { id: String -> rolesService.getGlobalRole(id).getOrNull() }) .withProjectRoles(roleRepository.findProjectRoleAssociationsByAccount(raw.accountId) { project: Int, roleId: String -> diff --git a/ontrack-service/src/main/java/net/nemerosa/ontrack/service/security/RolesServiceImpl.kt b/ontrack-service/src/main/java/net/nemerosa/ontrack/service/security/RolesServiceImpl.kt index e2560048c25..71994af97d3 100644 --- a/ontrack-service/src/main/java/net/nemerosa/ontrack/service/security/RolesServiceImpl.kt +++ b/ontrack-service/src/main/java/net/nemerosa/ontrack/service/security/RolesServiceImpl.kt @@ -1,6 +1,7 @@ package net.nemerosa.ontrack.service.security import net.nemerosa.ontrack.model.dashboards.DashboardEdition +import net.nemerosa.ontrack.model.dashboards.DashboardSharing import net.nemerosa.ontrack.model.labels.LabelManagement import net.nemerosa.ontrack.model.labels.ProjectLabelManagement import net.nemerosa.ontrack.model.security.* @@ -285,7 +286,8 @@ class RolesServiceImpl( registerGlobalRole(Roles.GLOBAL_PARTICIPANT, "Participant", "This role grants a read-only access to all projects and the right to comment on validation runs.", RolesService.readOnlyGlobalFunctions - + DashboardEdition::class.java, + + DashboardEdition::class.java + + DashboardSharing::class.java, RolesService.readOnlyProjectFunctions + ValidationRunStatusChange::class.java + ValidationRunStatusCommentEditOwn::class.java diff --git a/ontrack-service/src/main/java/net/nemerosa/ontrack/service/security/SecurityServiceImpl.kt b/ontrack-service/src/main/java/net/nemerosa/ontrack/service/security/SecurityServiceImpl.kt index 5807ddb2cc8..dfadd545de4 100644 --- a/ontrack-service/src/main/java/net/nemerosa/ontrack/service/security/SecurityServiceImpl.kt +++ b/ontrack-service/src/main/java/net/nemerosa/ontrack/service/security/SecurityServiceImpl.kt @@ -1,5 +1,7 @@ package net.nemerosa.ontrack.service.security +import net.nemerosa.ontrack.model.dashboards.DashboardEdition +import net.nemerosa.ontrack.model.dashboards.DashboardSharing import net.nemerosa.ontrack.model.security.* import net.nemerosa.ontrack.model.settings.CachedSettingsService import net.nemerosa.ontrack.model.settings.SecuritySettings @@ -58,11 +60,11 @@ class SecurityServiceImpl : SecurityService { return if (settings.isGrantProjectViewToAll) { if (settings.isGrantProjectParticipationToAll) { setOf( - ProjectView::class, - ValidationRunStatusChange::class, - ValidationRunStatusCommentEditOwn::class + ProjectView::class, + ValidationRunStatusChange::class, + ValidationRunStatusCommentEditOwn::class ) - } else { + } else { setOf(ProjectView::class) } } else { @@ -70,6 +72,19 @@ class SecurityServiceImpl : SecurityService { } } + override val autoGlobalFunctions: Set> + get() { + val settings = cachedSettingsService.getCachedSettings(SecuritySettings::class.java) + val functions = mutableSetOf>() + if (settings.grantDashboardEditionToAll) { + functions += DashboardEdition::class + if (settings.grantDashboardSharingToAll) { + functions += DashboardSharing::class + } + } + return functions.toSet() + } + override val currentAccount: OntrackAuthenticatedUser? get() { val context = SecurityContextHolder.getContext() diff --git a/ontrack-service/src/main/java/net/nemerosa/ontrack/service/settings/SecuritySettingsManager.kt b/ontrack-service/src/main/java/net/nemerosa/ontrack/service/settings/SecuritySettingsManager.kt index 32fbcb84e6d..44565abf2e9 100644 --- a/ontrack-service/src/main/java/net/nemerosa/ontrack/service/settings/SecuritySettingsManager.kt +++ b/ontrack-service/src/main/java/net/nemerosa/ontrack/service/settings/SecuritySettingsManager.kt @@ -21,6 +21,8 @@ class SecuritySettingsManager( settingsRepository.setBoolean(SecuritySettings::class.java, "grantProjectViewToAll", settings.isGrantProjectViewToAll) settingsRepository.setBoolean(SecuritySettings::class.java, "grantProjectParticipationToAll", settings.isGrantProjectParticipationToAll) settingsRepository.setBoolean(SecuritySettings::class.java, SecuritySettings::builtInAuthenticationEnabled.name, settings.builtInAuthenticationEnabled) + settingsRepository.setBoolean(SecuritySettings::class.java, SecuritySettings::grantDashboardEditionToAll.name, settings.grantDashboardEditionToAll) + settingsRepository.setBoolean(SecuritySettings::class.java, SecuritySettings::grantDashboardSharingToAll.name, settings.grantDashboardSharingToAll) } override fun getId(): String = "general-security" diff --git a/ontrack-service/src/main/java/net/nemerosa/ontrack/service/settings/SecuritySettingsProvider.kt b/ontrack-service/src/main/java/net/nemerosa/ontrack/service/settings/SecuritySettingsProvider.kt index 18105114173..d4689a2943f 100644 --- a/ontrack-service/src/main/java/net/nemerosa/ontrack/service/settings/SecuritySettingsProvider.kt +++ b/ontrack-service/src/main/java/net/nemerosa/ontrack/service/settings/SecuritySettingsProvider.kt @@ -16,6 +16,8 @@ class SecuritySettingsProvider( settingsRepository.getBoolean(SecuritySettings::class.java, "grantProjectViewToAll", true), settingsRepository.getBoolean(SecuritySettings::class.java, "grantProjectParticipationToAll", true), settingsRepository.getBoolean(SecuritySettings::class.java, SecuritySettings::builtInAuthenticationEnabled.name, SecuritySettings.DEFAULT_BUILTIN_AUTHENTICATION_ENABLED), + settingsRepository.getBoolean(SecuritySettings::class.java, SecuritySettings::grantDashboardEditionToAll.name, SecuritySettings.DEFAULT_GRANT_DASHBOARD_EDITION), + settingsRepository.getBoolean(SecuritySettings::class.java, SecuritySettings::grantDashboardSharingToAll.name, SecuritySettings.DEFAULT_GRANT_DASHBOARD_SHARING), ) override fun getSettingsClass(): Class = SecuritySettings::class.java diff --git a/ontrack-service/src/test/java/net/nemerosa/ontrack/service/dashboards/DashboardServiceIT.kt b/ontrack-service/src/test/java/net/nemerosa/ontrack/service/dashboards/DashboardServiceIT.kt index d2aeea14b0b..ee2826bdf9d 100644 --- a/ontrack-service/src/test/java/net/nemerosa/ontrack/service/dashboards/DashboardServiceIT.kt +++ b/ontrack-service/src/test/java/net/nemerosa/ontrack/service/dashboards/DashboardServiceIT.kt @@ -8,12 +8,16 @@ import net.nemerosa.ontrack.test.TestUtils.uid import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import kotlin.test.assertEquals +import kotlin.test.assertNull class DashboardServiceIT : AbstractDSLTestSupport() { @Autowired private lateinit var dashboardService: DashboardService + @Autowired + private lateinit var dashboardStorageService: DashboardStorageService + @Test fun `Default dashboard`() { withNoDashboard { @@ -92,6 +96,102 @@ class DashboardServiceIT : AbstractDSLTestSupport() { } } + @Test + fun `By default, any user can create and delete a private dashboard`() { + val name = uid("dash_") + asUser { + val dashboard = dashboardService.saveDashboard( + SaveDashboardInput( + uuid = null, + name = name, + userScope = DashboardContextUserScope.PRIVATE, + widgets = listOf( + WidgetInstanceInput( + uuid = null, + key = "home/LastActiveProjects", + config = mapOf("count" to 10).asJson(), + layout = WidgetLayout(x = 0, y = 0, w = 12, h = 1), + ) + ), + select = true, + ) + ) + val selectedDashboard = dashboardService.userDashboard() + assertEquals( + dashboard, + selectedDashboard + ) + // Deleting this dashboard + dashboardService.deleteDashboard(dashboard.uuid) + assertEquals( + DashboardContextUserScope.BUILT_IN, + dashboardService.userDashboard().userScope + ) + } + } + + @Test + fun `By default, any user can delete a shared dashboard if owned by them`() { + val name = uid("dash_") + asUser { + val dashboard = dashboardService.saveDashboard( + SaveDashboardInput( + uuid = null, + name = name, + userScope = DashboardContextUserScope.SHARED, + widgets = listOf( + WidgetInstanceInput( + uuid = null, + key = "home/LastActiveProjects", + config = mapOf("count" to 10).asJson(), + layout = WidgetLayout(x = 0, y = 0, w = 12, h = 1), + ) + ), + select = true, + ) + ) + // Deleting this dashboard + dashboardService.deleteDashboard(dashboard.uuid) + // Dashboard has been deleted + assertNull( + dashboardStorageService.findDashboardByUuid(dashboard.uuid), + "Dashboard has been deleted" + ) + } + } + + @Test + fun `Administrators can delete any dashboard`() { + val name = uid("dash_") + asUser { + val dashboard = dashboardService.saveDashboard( + SaveDashboardInput( + uuid = null, + name = name, + userScope = DashboardContextUserScope.SHARED, + widgets = listOf( + WidgetInstanceInput( + uuid = null, + key = "home/LastActiveProjects", + config = mapOf("count" to 10).asJson(), + layout = WidgetLayout(x = 0, y = 0, w = 12, h = 1), + ) + ), + select = true, + ) + ) + // Deleting this dashboard with an administrator account + asAccountWithGlobalRole(Roles.GLOBAL_ADMINISTRATOR) { + dashboardService.deleteDashboard(dashboard.uuid) + } + // Dashboard has been deleted + assertNull( + dashboardStorageService.findDashboardByUuid(dashboard.uuid), + "Dashboard has been deleted" + ) + } + } + @Test fun `Deleting a shared dashboard makes it unavailable as the default dashboard for all users`() { val participant = doCreateAccountWithGlobalRole(Roles.GLOBAL_PARTICIPANT) diff --git a/ontrack-web-core/components/dashboards/DashboardCommandMenu.js b/ontrack-web-core/components/dashboards/DashboardCommandMenu.js index 79b50be1ca4..c3c083b9f3a 100644 --- a/ontrack-web-core/components/dashboards/DashboardCommandMenu.js +++ b/ontrack-web-core/components/dashboards/DashboardCommandMenu.js @@ -24,8 +24,6 @@ export default function DashboardCommandMenu() { const user = useContext(UserContext) - const [messageApi, contextHolder] = message.useMessage() - const router = useRouter() const {environment} = useConnection() const context = useContext(DashboardContext) @@ -165,12 +163,14 @@ export default function DashboardCommandMenu() { } // Cloning current - menu.push({ - key: 'clone', - icon: , - label: "Clone current dashboard", - onClick: cloneDashboard, - }) + if (user.authorizations.dashboard?.edit) { + menu.push({ + key: 'clone', + icon: , + label: "Clone current dashboard", + onClick: cloneDashboard, + }) + } // Deleting current if (context?.dashboard && context.dashboard.authorizations?.delete) { @@ -183,16 +183,16 @@ export default function DashboardCommandMenu() { }) } - // Separator - menu.push({type: 'divider'}) - // New dashboard - menu.push({ - key: 'new', - icon: , - label: "Create a new dashboard", - onClick: createDashboard, - }) + if (user.authorizations.dashboard?.edit) { + menu.push({type: 'divider'}) + menu.push({ + key: 'new', + icon: , + label: "Create a new dashboard", + onClick: createDashboard, + }) + } // OK setItems(menu) @@ -201,7 +201,6 @@ export default function DashboardCommandMenu() { return ( <> - {contextHolder}