Skip to content

Commit

Permalink
GH-241 Fixed writing to USS file with no access rights (#309)
Browse files Browse the repository at this point in the history
Signed-off-by: Katsiaryna Tsytsenia <vrednaya.chan@gmail.com>
  • Loading branch information
ktsytsen authored Feb 28, 2025
1 parent 2866c9d commit ecda8e2
Show file tree
Hide file tree
Showing 3 changed files with 216 additions and 21 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,22 @@ data class RemoteUssAttributes(
charset = DEFAULT_BINARY_CHARSET
)

/** Get file mode access number basing on the file owner and the available requesters to use the file */
private fun getAvailableFileModeAccessNum(): Int {
val hasFileOwnerInRequesters = requesters.any { requester ->
val savedOwner = CredentialService.getOwner(requester.connectionConfig)
val ownerOrUsername =
if (savedOwner == "") CredentialService.getUsername(requester.connectionConfig)
else savedOwner
/** Get file mode access number basing on the file owner and the available requesters/connection to use the file */
private fun getAvailableFileModeAccessNum(connConfig: ConnectionConfig? = null): Int {
val hasFileOwnerInRequesters = if (connConfig == null) {
requesters.any { requester ->
val savedOwner = CredentialService.getOwner(requester.connectionConfig)
val ownerOrUsername =
if (savedOwner == "") CredentialService.getUsername(requester.connectionConfig)
else savedOwner
ownerOrUsername.equals(owner, ignoreCase = true)
}
} else {
val savedOwner = CredentialService.getOwner(connConfig)
val ownerOrUsername = if (savedOwner == "")
CredentialService.getUsername(connConfig)
else
savedOwner
ownerOrUsername.equals(owner, ignoreCase = true)
}
return if (fileMode != null) {
Expand All @@ -130,16 +139,24 @@ data class RemoteUssAttributes(

val isWritable: Boolean
get() {
val mode = getAvailableFileModeAccessNum()
return mode == FileModeValue.WRITE.mode
|| mode == FileModeValue.WRITE_EXECUTE.mode
|| mode == FileModeValue.READ_WRITE.mode
|| mode == FileModeValue.READ_WRITE_EXECUTE.mode
return isWritableForConnection()
}

fun isWritableForConnection(connConfig: ConnectionConfig? = null): Boolean {
val mode = getAvailableFileModeAccessNum(connConfig)
return mode == FileModeValue.WRITE.mode
|| mode == FileModeValue.WRITE_EXECUTE.mode
|| mode == FileModeValue.READ_WRITE.mode
|| mode == FileModeValue.READ_WRITE_EXECUTE.mode
}

val isReadable: Boolean
get() {
val mode = getAvailableFileModeAccessNum()
return isReadableForConnection()
}

fun isReadableForConnection(connConfig: ConnectionConfig? = null): Boolean {
val mode = getAvailableFileModeAccessNum(connConfig)
return mode == FileModeValue.READ.mode
|| mode == FileModeValue.READ_WRITE.mode
|| mode == FileModeValue.READ_EXECUTE.mode
Expand Down
41 changes: 41 additions & 0 deletions src/main/kotlin/org/zowe/explorer/explorer/ui/UssFileNode.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ package org.zowe.explorer.explorer.ui

import com.intellij.ide.projectView.PresentationData
import com.intellij.ide.util.treeView.AbstractTreeNode
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.application.runReadAction
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Iconable
import com.intellij.ui.AnimatedIcon
Expand All @@ -25,6 +28,9 @@ import org.zowe.explorer.common.message
import org.zowe.explorer.config.connect.ConnectionConfig
import org.zowe.explorer.dataops.DataOpsManager
import org.zowe.explorer.dataops.attributes.RemoteUssAttributes
import org.zowe.explorer.dataops.content.service.SyncProcessService
import org.zowe.explorer.dataops.content.synchronizer.DocumentedSyncProvider
import org.zowe.explorer.dataops.content.synchronizer.SaveStrategy
import org.zowe.explorer.dataops.getAttributesService
import org.zowe.explorer.dataops.sort.SortQueryKeys
import org.zowe.explorer.explorer.ExplorerUnit
Expand Down Expand Up @@ -71,6 +77,41 @@ class UssFileNode(
)
}

/**
* This method is required to set the correct permissions to open USS file.
* @see ExplorerTreeNode.navigate
*/
override fun navigate(requestFocus: Boolean) {
val file = virtualFile ?: return
if (file.isWritable) {
val syncProvider = DocumentedSyncProvider(file, SaveStrategy.default(project))
val contentSynchronizer = DataOpsManager.getService().getContentSynchronizer(file)
val currentContent = runReadAction { syncProvider.retrieveCurrentContent() }
val previousContent = contentSynchronizer?.successfulContentStorage(syncProvider)
val needToUpload = contentSynchronizer?.isFileUploadNeeded(syncProvider) == true
if (
!(currentContent contentEquals previousContent)
&& needToUpload
&& !SyncProcessService.getService().isFileSyncingNow(file)
) {
runCatching {
runInEdt {
FileEditorManager.getInstance(project).closeFile(file)
}
}
}
}

val originConnectionConfig = unit.connectionConfig
val attributes = DataOpsManager.getService().tryToGetAttributes(file)
if (attributes is RemoteUssAttributes) {
file.isReadable = attributes.isReadableForConnection(originConnectionConfig)
file.isWritable = attributes.isWritableForConnection(originConnectionConfig)
}

super.navigate(requestFocus)
}

override fun getVirtualFile(): MFVirtualFile {
return value
}
Expand Down
153 changes: 145 additions & 8 deletions src/test/kotlin/org/zowe/explorer/explorer/ui/UssFileNodeTestSpec.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,16 @@ import com.intellij.ide.util.treeView.AbstractTreeNode
import com.intellij.ide.util.treeView.TreeAnchorizer
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.fileEditor.impl.FileDocumentManagerBase.HARD_REF_TO_DOCUMENT_KEY
import com.intellij.openapi.fileTypes.FileTypes
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.Messages
import com.intellij.openapi.ui.showYesNoDialog
import com.intellij.openapi.vfs.VirtualFile
import io.kotest.assertions.assertSoftly
import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.shouldContain
import io.mockk.*
import org.zowe.explorer.config.connect.ConnectionConfig
import org.zowe.explorer.config.connect.ConnectionConfigBase
import org.zowe.explorer.dataops.DataOpsManager
Expand All @@ -43,11 +49,8 @@ import org.zowe.explorer.utils.isBeingEditingNow
import org.zowe.explorer.utils.runInEdtAndWait
import org.zowe.explorer.vfs.MFVirtualFile
import org.zowe.explorer.vfs.MFVirtualFileSystem
import io.kotest.assertions.assertSoftly
import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.shouldContain
import io.mockk.*
import org.zowe.kotlinsdk.FileMode
import org.zowe.kotlinsdk.UssFile
import java.time.LocalDateTime
import javax.swing.Icon
import javax.swing.tree.TreePath
Expand All @@ -70,19 +73,33 @@ class UssFileNodeTestSpec : WithApplicationShouldSpec({
lateinit var dataOpsManagerService: TestDataOpsManagerImpl
lateinit var uiComponentManagerService: TestUIComponentManager
lateinit var ussFileNode: UssFileNode
lateinit var rootNode: ExplorerTreeNode<ConnectionConfig, Any>
var firstNode: ExplorerTreeNode<ConnectionConfig, Any>

beforeEach {
mockkObject(MFVirtualFileSystem)
every { MFVirtualFileSystem.instance } returns mockk()

projectMock = mockk<Project>()

fileMock = mockk()
every { fileMock.isDirectory } returns false
every { fileMock.isReadable } returns true
var isReadable = true
every { fileMock.isReadable } returns isReadable
every { fileMock setProperty "isReadable" value any<Boolean>() } propertyType Boolean::class answers {
isReadable = value
}
var isWritable = true
every { fileMock.isWritable } returns isWritable
every { fileMock.isWritable = any<Boolean>() } answers { isWritable = firstArg() }
every { fileMock.isValid } returns true
every { fileMock.fileType } returns FileTypes.UNKNOWN
every { fileMock.detectedLineSeparator } returns "\n"
every { fileMock.name } returns "navigate test"
every { fileMock.getUserData(HARD_REF_TO_DOCUMENT_KEY) } returns null
mockkStatic(VirtualFile::isBeingEditingNow)
every { fileMock.isBeingEditingNow() } returns false

projectMock = mockk()

treeStructureMock = mockk()
every { treeStructureMock.registerNode(any()) } returns mockk()
Expand All @@ -99,14 +116,50 @@ class UssFileNodeTestSpec : WithApplicationShouldSpec({
}
}

explorerTreeNodeMock = mockk()

explorer = mockk()
every { explorer.componentManager } returns ApplicationManager.getApplication()

explorerUnitMock = mockk()
every { explorerUnitMock.explorer } returns explorer

rootNode =
object : ExplorerTreeNode<ConnectionConfig, Any>("rootNode", projectMock, null, mockk(), treeStructureMock) {
override fun update(presentation: PresentationData) {
TODO("Not yet implemented")
}

override fun getChildren(): MutableCollection<out AbstractTreeNode<*>> {
TODO("Not yet implemented")
}
}

every { explorerUnitMock.connectionConfig } returns ConnectionConfig()
firstNode = object : ExplorerUnitTreeNodeBase<ConnectionConfig, Any, ExplorerUnit<ConnectionConfig>>(
"firstNode",
projectMock,
rootNode,
explorerUnitMock,
treeStructureMock
) {
override fun update(presentation: PresentationData) {
TODO("Not yet implemented")
}

override fun getChildren(): MutableCollection<out AbstractTreeNode<*>> {
TODO("Not yet implemented")
}
}

explorerTreeNodeMock = spyk(
UssFileNode(
fileMock, projectMock,
firstNode,
explorerUnitMock, treeStructureMock
),
recordPrivateCalls = true
)
every { explorerTreeNodeMock.parent } answers { firstNode }

dataOpsManagerService = DataOpsManager.getService() as TestDataOpsManagerImpl

ussFileNode = spyk(
Expand All @@ -129,6 +182,12 @@ class UssFileNodeTestSpec : WithApplicationShouldSpec({
dataOpsManagerService.testInstance = object : TestDataOpsManagerImpl() {
override fun getContentSynchronizer(file: VirtualFile): ContentSynchronizer {
val contentSynchronizerMock = mockk<ContentSynchronizer>()
every { contentSynchronizerMock.isFileUploadNeeded(any()) } answers {
false
}
every { contentSynchronizerMock.successfulContentStorage(any()) } answers {
byteArrayOf(Byte.MIN_VALUE, -1, 0, 1, Byte.MAX_VALUE)
}
every { contentSynchronizerMock.synchronizeWithRemote(any(), any()) } answers {
isSyncWithRemotePerformed = true
val syncProvider = firstArg() as SyncProvider
Expand All @@ -154,9 +213,28 @@ class UssFileNodeTestSpec : WithApplicationShouldSpec({
}
should("perform navigate on file with failure due to permission denied") {
var isSyncWithRemotePerformed = false
val ussFileMock = mockk<UssFile>()
every { ussFileMock.name } returns "USSFileName"
every { ussFileMock.isDirectory } returns false
every { ussFileMock.size } returns 100
every { ussFileMock.uid } returns 500
every { ussFileMock.user } returns "user"
every { ussFileMock.gid } returns 110
every { ussFileMock.groupId } returns "guid"
every { ussFileMock.modificationTime } returns "modTime"
every { ussFileMock.target } returns "target"
every { ussFileMock.fileMode } returns FileMode(7, 5, 5, "")
dataOpsManagerService.testInstance = object : TestDataOpsManagerImpl() {

override fun tryToGetAttributes(file: VirtualFile): FileAttributes {
return RemoteUssAttributes("rootPath", ussFileMock, "URL", ConnectionConfig())
}
override fun getContentSynchronizer(file: VirtualFile): ContentSynchronizer {
val contentSynchronizerMock = mockk<ContentSynchronizer>()
every { contentSynchronizerMock.isFileUploadNeeded(any()) } answers { false }
every { contentSynchronizerMock.successfulContentStorage(any()) } answers {
byteArrayOf(Byte.MIN_VALUE, -1, 0, 1, Byte.MAX_VALUE)
}
every { contentSynchronizerMock.synchronizeWithRemote(any(), any()) } answers {
isSyncWithRemotePerformed = true
val syncProvider = firstArg() as SyncProvider
Expand Down Expand Up @@ -189,8 +267,15 @@ class UssFileNodeTestSpec : WithApplicationShouldSpec({
should("perform navigate on file with failure due to client is not authorized") {
var isSyncWithRemotePerformed = false
dataOpsManagerService.testInstance = object : TestDataOpsManagerImpl() {
override fun tryToGetAttributes(file: VirtualFile): FileAttributes {
return RemoteUssAttributes("test", false, null, "test", mutableListOf())
}
override fun getContentSynchronizer(file: VirtualFile): ContentSynchronizer {
val contentSynchronizerMock = mockk<ContentSynchronizer>()
every { contentSynchronizerMock.isFileUploadNeeded(any()) } answers { false }
every { contentSynchronizerMock.successfulContentStorage(any()) } answers {
byteArrayOf(Byte.MIN_VALUE, -1, 0, 1, Byte.MAX_VALUE)
}
every { contentSynchronizerMock.synchronizeWithRemote(any(), any()) } answers {
isSyncWithRemotePerformed = true
val syncProvider = firstArg() as SyncProvider
Expand Down Expand Up @@ -218,6 +303,9 @@ class UssFileNodeTestSpec : WithApplicationShouldSpec({
}
should("exit 'navigate' when 'getContentSynchronizer' returns null") {
dataOpsManagerService.testInstance = object : TestDataOpsManagerImpl() {
override fun tryToGetAttributes(file: VirtualFile): FileAttributes {
return RemoteUssAttributes("test", false, null, "test", mutableListOf())
}
override fun getContentSynchronizer(file: VirtualFile): ContentSynchronizer? {
return null
}
Expand Down Expand Up @@ -258,8 +346,15 @@ class UssFileNodeTestSpec : WithApplicationShouldSpec({

var isNavigateContinued = false
dataOpsManagerService.testInstance = object : TestDataOpsManagerImpl() {
override fun tryToGetAttributes(file: VirtualFile): FileAttributes {
return RemoteUssAttributes("test", false, null, "test", mutableListOf())
}
override fun getContentSynchronizer(file: VirtualFile): ContentSynchronizer {
val contentSynchronizerMock = mockk<ContentSynchronizer>()
every { contentSynchronizerMock.isFileUploadNeeded(any()) } answers { false }
every { contentSynchronizerMock.successfulContentStorage(any()) } answers {
byteArrayOf(Byte.MIN_VALUE, -1, 0, 1, Byte.MAX_VALUE)
}
every { contentSynchronizerMock.synchronizeWithRemote(any(), any()) } answers {
isNavigateContinued = true
val syncProvider = firstArg() as SyncProvider
Expand All @@ -274,10 +369,52 @@ class UssFileNodeTestSpec : WithApplicationShouldSpec({
assertSoftly { isNavigateContinued shouldBe false }
}
should("perform navigate on a file that was already opened") {
every { fileMock.isWritable } returns false
firstNode = object : ExplorerTreeNode<ConnectionConfig, Any>(
"nullConnectionNode",
projectMock,
rootNode,
mockk<FileExplorer>(),
treeStructureMock
) {
override fun update(presentation: PresentationData) {
TODO("Not yet implemented")
}
override fun getChildren(): MutableCollection<out AbstractTreeNode<*>> {
TODO("Not yet implemented")
}
}

explorerTreeNodeMock = spyk(
UssFileNode(
fileMock, projectMock,
firstNode,
explorerUnitMock, treeStructureMock
),
recordPrivateCalls = true
)
every { explorerTreeNodeMock.parent } answers { firstNode }

ussFileNode = spyk(
UssFileNode(
fileMock,
projectMock,
explorerTreeNodeMock,
explorerUnitMock,
treeStructureMock
)
)
every { ussFileNode.update() } returns false
every { ussFileNode.virtualFile } returns fileMock

var isSyncWithRemotePerformed = false
dataOpsManagerService.testInstance = object : TestDataOpsManagerImpl() {
override fun getContentSynchronizer(file: VirtualFile): ContentSynchronizer {
val contentSynchronizerMock = mockk<ContentSynchronizer>()
every { contentSynchronizerMock.isFileUploadNeeded(any()) } answers { true }
every { contentSynchronizerMock.successfulContentStorage(any()) } answers {
byteArrayOf(Byte.MIN_VALUE, -1, 0, 1, Byte.MAX_VALUE)
}
every { contentSynchronizerMock.synchronizeWithRemote(any(), any()) } answers {
isSyncWithRemotePerformed = true
val syncProvider = firstArg() as SyncProvider
Expand Down

0 comments on commit ecda8e2

Please sign in to comment.