From f50a0c696dec7a5dbec082c74b3e577b615f969a Mon Sep 17 00:00:00 2001 From: Christian Kurz Date: Thu, 15 Aug 2024 02:13:51 +0200 Subject: [PATCH 1/8] check for permission when checking users --- .../gropius/sync/github/GithubDataService.kt | 6 +- .../kotlin/gropius/sync/github/GithubSync.kt | 52 ++++++++++++--- .../gropius/sync/jira/JiraDataService.kt | 3 +- .../main/kotlin/gropius/sync/jira/JiraSync.kt | 43 +++++++++--- .../main/kotlin/gropius/sync/TokenManager.kt | 65 +++++++++++++++---- 5 files changed, 135 insertions(+), 34 deletions(-) diff --git a/github/src/main/kotlin/gropius/sync/github/GithubDataService.kt b/github/src/main/kotlin/gropius/sync/github/GithubDataService.kt index 307d2987..9029755f 100644 --- a/github/src/main/kotlin/gropius/sync/github/GithubDataService.kt +++ b/github/src/main/kotlin/gropius/sync/github/GithubDataService.kt @@ -216,7 +216,7 @@ class GithubDataService( * @return The selected user and the response for the mutation */ final suspend inline fun mutation( - imsProject: IMSProject, users: List, body: Mutation + imsProject: IMSProject, users: List, body: Mutation, owner: List ): Pair> { val imsConfig = IMSConfig(helper, imsProject.ims().value, imsProject.ims().value.template().value) val userList = users.toMutableList() @@ -228,7 +228,7 @@ class GithubDataService( userList.add(imsUser) } logger.info("Requesting with users: $userList") - return tokenManager.executeUntilWorking(imsProject.ims().value, userList) { token -> + return tokenManager.executeUntilWorking(imsProject, userList, owner) { token -> val apolloClient = ApolloClient.Builder().serverUrl(imsConfig.graphQLUrl.toString()) .addHttpHeader("Authorization", "Bearer ${token.token}").build() val res = apolloClient.mutation(body).execute() @@ -267,7 +267,7 @@ class GithubDataService( userList.add(imsUser) } logger.info("Requesting with users: $userList ") - return tokenManager.executeUntilWorking(imsProject.ims().value, userList) { token -> + return tokenManager.executeUntilWorking(imsProject, userList, listOf()) { token -> val apolloClient = ApolloClient.Builder().serverUrl(imsConfig.graphQLUrl.toString()) .addHttpHeader("Authorization", "Bearer ${token.token}").build() val res = apolloClient.query(body).execute() diff --git a/github/src/main/kotlin/gropius/sync/github/GithubSync.kt b/github/src/main/kotlin/gropius/sync/github/GithubSync.kt index a17a306c..0b16c5c0 100644 --- a/github/src/main/kotlin/gropius/sync/github/GithubSync.kt +++ b/github/src/main/kotlin/gropius/sync/github/GithubSync.kt @@ -6,6 +6,8 @@ import gropius.model.issue.Label import gropius.model.issue.timeline.IssueComment import gropius.model.template.IMSTemplate import gropius.model.template.IssueState +import gropius.model.user.GropiusUser +import gropius.model.user.IMSUser import gropius.model.user.User import gropius.sync.* import gropius.sync.github.config.IMSConfigManager @@ -155,12 +157,33 @@ final class GithubSync( return issuePileService.findByImsProjectAndHasUnsyncedData(imsProject.rawId!!, true) } + /** + * Map list of User to GropiusUser + * @param users The list of users mixed of IMSUser and GropiusUser + * @return The list of GropiusUser + */ + private suspend fun gropiusUserList(users: List): List { + val outputUsers = users.mapNotNull { + when (it) { + is GropiusUser -> it + is IMSUser -> it.gropiusUser().value + else -> null + } + } + if (outputUsers.isEmpty() && users.isNotEmpty()) { + throw IllegalStateException("No Gropius User left as owner") + } + return outputUsers + } + override suspend fun syncComment( imsProject: IMSProject, issueId: String, issueComment: IssueComment, users: List ): TimelineItemConversionInformation? { val body = issueComment.body if (body.isNullOrEmpty()) return null; - val response = githubDataService.mutation(imsProject, users, MutateCreateCommentMutation(issueId, body)).second + val response = githubDataService.mutation( + imsProject, users, MutateCreateCommentMutation(issueId, body), gropiusUserList(users) + ).second val item = response.data?.addComment?.commentEdge?.node?.asIssueTimelineItems() if (item != null) { return TODOTimelineItemConversionInformation(imsProject.rawId!!, item.id) @@ -180,8 +203,9 @@ final class GithubSync( //TODO("Create label on remote") return null } - val response = - githubDataService.mutation(imsProject, users, MutateAddLabelMutation(issueId, labelInfo.githubId)).second + val response = githubDataService.mutation( + imsProject, users, MutateAddLabelMutation(issueId, labelInfo.githubId), gropiusUserList(users) + ).second val item = response.data?.addLabelsToLabelable?.labelable?.asIssue()?.timelineItems?.nodes?.lastOrNull() if (item != null) { return TODOTimelineItemConversionInformation(imsProject.rawId!!, item.asNode()!!.id) @@ -194,8 +218,9 @@ final class GithubSync( override suspend fun syncTitleChange( imsProject: IMSProject, issueId: String, newTitle: String, users: List ): TimelineItemConversionInformation? { - val response = - githubDataService.mutation(imsProject, users, MutateChangeTitleMutation(issueId, newTitle)).second + val response = githubDataService.mutation( + imsProject, users, MutateChangeTitleMutation(issueId, newTitle), gropiusUserList(users) + ).second val item = response.data?.updateIssue?.issue?.timelineItems?.nodes?.lastOrNull() if (item != null) { return TODOTimelineItemConversionInformation(imsProject.rawId!!, item.asNode()!!.id) @@ -209,7 +234,9 @@ final class GithubSync( imsProject: IMSProject, issueId: String, newState: IssueState, users: List ): TimelineItemConversionInformation? { if (newState.isOpen) { - val response = githubDataService.mutation(imsProject, users, MutateReopenIssueMutation(issueId)).second + val response = githubDataService.mutation( + imsProject, users, MutateReopenIssueMutation(issueId), gropiusUserList(users) + ).second val item = response.data?.reopenIssue?.issue?.timelineItems?.nodes?.lastOrNull() if (item != null) { return TODOTimelineItemConversionInformation(imsProject.rawId!!, item.asNode()!!.id) @@ -218,7 +245,9 @@ final class GithubSync( //TODO("ERROR HANDLING") return null } else { - val response = githubDataService.mutation(imsProject, users, MutateCloseIssueMutation(issueId)).second + val response = githubDataService.mutation( + imsProject, users, MutateCloseIssueMutation(issueId), gropiusUserList(users) + ).second val item = response.data?.closeIssue?.issue?.timelineItems?.nodes?.lastOrNull() if (item != null) { return TODOTimelineItemConversionInformation(imsProject.rawId!!, item.asNode()!!.id) @@ -234,8 +263,9 @@ final class GithubSync( ): TimelineItemConversionInformation? { val labelInfo = githubDataService.labelInfoRepository.findByImsProjectAndNeo4jId(imsProject.rawId!!, label.rawId!!)!! - val response = - githubDataService.mutation(imsProject, users, MutateRemoveLabelMutation(issueId, labelInfo.githubId)).second + val response = githubDataService.mutation( + imsProject, users, MutateRemoveLabelMutation(issueId, labelInfo.githubId), gropiusUserList(users) + ).second val item = response.data?.removeLabelsFromLabelable?.labelable?.asIssue()?.timelineItems?.nodes?.lastOrNull() if (item != null) { return TODOTimelineItemConversionInformation(imsProject.rawId!!, item.asNode()!!.id) @@ -258,7 +288,9 @@ final class GithubSync( imsProject, listOf(issue.createdBy().value, issue.lastModifiedBy().value) + issue.timelineItems() .map { it.createdBy().value }, - MutateCreateIssueMutation(repoId, issue.title, issue.bodyBody) + MutateCreateIssueMutation(repoId, issue.title, issue.bodyBody), + gropiusUserList(listOf(issue.createdBy().value, issue.lastModifiedBy().value) + issue.timelineItems() + .map { it.createdBy().value }) ).second val item = response.data?.createIssue?.issue if (item != null) { diff --git a/jira/src/main/kotlin/gropius/sync/jira/JiraDataService.kt b/jira/src/main/kotlin/gropius/sync/jira/JiraDataService.kt index 3fd233fa..bc79b51f 100644 --- a/jira/src/main/kotlin/gropius/sync/jira/JiraDataService.kt +++ b/jira/src/main/kotlin/gropius/sync/jira/JiraDataService.kt @@ -271,6 +271,7 @@ class JiraDataService( imsProject: IMSProject, users: List, requestMethod: HttpMethod, + owner: List, body: T? = null, crossinline urlBuilder: URLBuilder .(URLBuilder) -> Unit ): Pair { @@ -286,7 +287,7 @@ class JiraDataService( } val userList = rawUserList.distinct() logger.info("Requesting with users: $userList") - return tokenManager.executeUntilWorking(imsProject.ims().value, userList) { + return tokenManager.executeUntilWorking(imsProject, userList, owner) { sendRequest( imsProject, requestMethod, body, urlBuilder, it ) diff --git a/jira/src/main/kotlin/gropius/sync/jira/JiraSync.kt b/jira/src/main/kotlin/gropius/sync/jira/JiraSync.kt index 1c2804f5..be27158f 100644 --- a/jira/src/main/kotlin/gropius/sync/jira/JiraSync.kt +++ b/jira/src/main/kotlin/gropius/sync/jira/JiraSync.kt @@ -6,6 +6,8 @@ import gropius.model.issue.Label import gropius.model.issue.timeline.IssueComment import gropius.model.template.IMSTemplate import gropius.model.template.IssueState +import gropius.model.user.GropiusUser +import gropius.model.user.IMSUser import gropius.model.user.User import gropius.sync.* import gropius.sync.jira.config.IMSConfigManager @@ -105,7 +107,7 @@ final class JiraSync( for (issueId in issueList) { var startAt = 0 while (true) { - val issueCommentList = jiraDataService.request(imsProject, listOf(), HttpMethod.Get) { + val issueCommentList = jiraDataService.request(imsProject, listOf(), HttpMethod.Get, listOf()) { appendPathSegments("issue") appendPathSegments(issueId) appendPathSegments("changelog") @@ -129,7 +131,7 @@ final class JiraSync( for (issueId in issueList) { var startAt = 0 while (true) { - val issueCommentList = jiraDataService.request(imsProject, listOf(), HttpMethod.Get) { + val issueCommentList = jiraDataService.request(imsProject, listOf(), HttpMethod.Get, listOf()) { appendPathSegments("issue") appendPathSegments(issueId) appendPathSegments("comment") @@ -163,7 +165,7 @@ final class JiraSync( var startAt = 0 while (true) { val imsProjectConfig = IMSProjectConfig(helper, imsProject) - val issueResponse = jiraDataService.request(imsProject, listOf(), HttpMethod.Get) { + val issueResponse = jiraDataService.request(imsProject, listOf(), HttpMethod.Get, listOf()) { appendPathSegments("search") parameters.append("jql", "project=${imsProjectConfig.repo}") parameters.append("expand", "names,schema,editmeta,changelog") @@ -182,6 +184,25 @@ final class JiraSync( return issueDataService.findByImsProject(imsProject.rawId!!) } + /** + * Map list of User to GropiusUser + * @param users The list of users mixed of IMSUser and GropiusUser + * @return The list of GropiusUser + */ + private suspend fun gropiusUserList(users: List): List { + val outputUsers = users.mapNotNull { + when (it) { + is GropiusUser -> it + is IMSUser -> it.gropiusUser().value + else -> null + } + } + if (outputUsers.isEmpty() && users.isNotEmpty()) { + throw IllegalStateException("No Gropius User left as owner") + } + return outputUsers + } + override suspend fun syncComment( imsProject: IMSProject, issueId: String, issueComment: IssueComment, users: List ): TimelineItemConversionInformation? { @@ -189,7 +210,11 @@ final class JiraSync( return null } val response = jiraDataService.request( - imsProject, users, HttpMethod.Post, JsonObject(mapOf("body" to JsonPrimitive(issueComment.body))) + imsProject, + users, + HttpMethod.Post, + gropiusUserList(users), + JsonObject(mapOf("body" to JsonPrimitive(issueComment.body))) ) { appendPathSegments("issue") appendPathSegments(issueId) @@ -203,7 +228,7 @@ final class JiraSync( imsProject: IMSProject, issueId: String, newTitle: String, users: List ): TimelineItemConversionInformation? { val response = jiraDataService.request( - imsProject, users, HttpMethod.Put, JsonObject( + imsProject, users, HttpMethod.Put, gropiusUserList(users), JsonObject( mapOf( "fields" to JsonObject( mapOf( @@ -233,7 +258,7 @@ final class JiraSync( return null; } jiraDataService.request( - imsProject, users, HttpMethod.Put, JsonObject( + imsProject, users, HttpMethod.Put, gropiusUserList(users), JsonObject( mapOf( "fields" to JsonObject( mapOf( @@ -256,7 +281,7 @@ final class JiraSync( imsProject: IMSProject, issueId: String, label: Label, users: List ): TimelineItemConversionInformation? { val response = jiraDataService.request( - imsProject, users, HttpMethod.Put, JsonObject( + imsProject, users, HttpMethod.Put, gropiusUserList(users), JsonObject( mapOf( "update" to JsonObject( mapOf( @@ -301,7 +326,7 @@ final class JiraSync( imsProject: IMSProject, issueId: String, label: Label, users: List ): TimelineItemConversionInformation? { val response = jiraDataService.request( - imsProject, users, HttpMethod.Put, JsonObject( + imsProject, users, HttpMethod.Put, gropiusUserList(users), JsonObject( mapOf( "update" to JsonObject( mapOf( @@ -340,6 +365,8 @@ final class JiraSync( listOf(issue.createdBy().value, issue.lastModifiedBy().value) + issue.timelineItems() .map { it.createdBy().value }, HttpMethod.Post, + gropiusUserList(listOf(issue.createdBy().value, issue.lastModifiedBy().value) + issue.timelineItems() + .map { it.createdBy().value }), IssueQueryRequest( IssueQueryRequestFields( issue.title, diff --git a/sync/src/main/kotlin/gropius/sync/TokenManager.kt b/sync/src/main/kotlin/gropius/sync/TokenManager.kt index 20e30bf5..a2c9f31c 100644 --- a/sync/src/main/kotlin/gropius/sync/TokenManager.kt +++ b/sync/src/main/kotlin/gropius/sync/TokenManager.kt @@ -1,6 +1,7 @@ package gropius.sync import gropius.model.architecture.IMS +import gropius.model.architecture.IMSProject import gropius.model.user.GropiusUser import gropius.model.user.IMSUser import gropius.model.user.User @@ -145,27 +146,63 @@ abstract class TokenManager( return ret } + /** + * Check if a user is allowed to be used for syncing + * + * @param imsProject The IMS to work with + * @param user The user to check + * @param owner The user that created the data + * @return true if the user is allowed + */ + private suspend fun isAllowed(imsProject: IMSProject, user: IMSUser, owner: List): Boolean { + return true // TODO: ignoring until @nk-coding does UI + val ownerSet = owner.toSet() + if ((owner.isEmpty() || ownerSet.contains(user.gropiusUser().value)) && imsProject.ims().value.syncSelfAllowedBy() + .contains(user.gropiusUser().value) + ) { + return true + } + if ((owner.isEmpty() || ownerSet.contains(user.gropiusUser().value)) && imsProject.syncSelfAllowedBy() + .contains(user.gropiusUser().value) + ) { + return true + } + if (imsProject.ims().value.syncOthersAllowedBy().contains(user.gropiusUser().value)) { + return true + } + if (imsProject.syncOthersAllowedBy().contains(user.gropiusUser().value)) { + return true + } + return false + } + /** * Attempt a query for a list of users until it works * * @param users The list of users, sorted with best first * @param executor The function to execute + * @param owner The user that created the data * * @return The user it worked with and the result of the executor */ private suspend fun executeUntilWorking( - users: List, executor: suspend (token: ResponseType) -> Optional + imsProject: IMSProject, + users: List, + executor: suspend (token: ResponseType) -> Optional, + owner: List ): Pair { for (user in users) { - val token = getUserToken(user) - if (token?.token != null) { - logger.trace("Trying token of user ${user.rawId}") - val ret = executor(token) - if (ret.isPresent) { - return user to ret.get() + if (isAllowed(imsProject, user, owner)) { + val token = getUserToken(user) + if (token?.token != null) { + logger.trace("Trying token of user ${user.rawId}") + val ret = executor(token) + if (ret.isPresent) { + return user to ret.get() + } + } else { + logger.trace("User ${user.rawId} had no token") } - } else { - logger.trace("User ${user.rawId} had no token") } } TODO("Error Message") @@ -177,15 +214,19 @@ abstract class TokenManager( * @param ims The IMS to work with * @param user The list of users, sorted with best first * @param executor The function to execute + * @param owner The user that created the data * * @return The user it worked with and the result of the executor */ suspend fun executeUntilWorking( - ims: IMS, user: List, executor: suspend (token: ResponseType) -> Optional + imsProject: IMSProject, + user: List, + owner: List, + executor: suspend (token: ResponseType) -> Optional ): Pair { - val users = user.map { getPossibleUsersForUser(ims, it) }.flatten().distinct() + val users = user.map { getPossibleUsersForUser(imsProject.ims().value, it) }.flatten().distinct() logger.info("Expanding ${user.map { "${it::class.simpleName}:${it.rawId}(${it.username})" }} to ${users.map { "${it::class.simpleName}:${it.rawId}(${it.username})" }}") - return executeUntilWorking(users, executor) + return executeUntilWorking(imsProject, users, executor, owner) } /** From 3eed9b9474b6dca31f939ca69d38d54bab00d8b2 Mon Sep 17 00:00:00 2001 From: nk-coding Date: Fri, 23 Aug 2024 06:09:38 +0200 Subject: [PATCH 2/8] more search queries --- core/src/main/kotlin/gropius/model/architecture/IMS.kt | 2 +- core/src/main/kotlin/gropius/model/architecture/IMSProject.kt | 2 +- core/src/main/kotlin/gropius/model/template/IMSTemplate.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/core/src/main/kotlin/gropius/model/architecture/IMS.kt b/core/src/main/kotlin/gropius/model/architecture/IMS.kt index a1781987..06a0971c 100644 --- a/core/src/main/kotlin/gropius/model/architecture/IMS.kt +++ b/core/src/main/kotlin/gropius/model/architecture/IMS.kt @@ -13,7 +13,7 @@ import gropius.model.user.permission.NodeWithPermissions import io.github.graphglue.model.* import org.springframework.data.neo4j.core.schema.CompositeProperty -@DomainNode("imss") +@DomainNode("imss", searchQueryName = "searchIMSs") @GraphQLDescription( """Entity which represents an issue management system (like GitHub, Jira, Redmine, ...). Trackables can be added to this via an IMSProject, so that their issues are synced to this IMS. diff --git a/core/src/main/kotlin/gropius/model/architecture/IMSProject.kt b/core/src/main/kotlin/gropius/model/architecture/IMSProject.kt index b8116671..8ae0b3a6 100644 --- a/core/src/main/kotlin/gropius/model/architecture/IMSProject.kt +++ b/core/src/main/kotlin/gropius/model/architecture/IMSProject.kt @@ -11,7 +11,7 @@ import gropius.model.user.permission.TrackablePermission import io.github.graphglue.model.* import org.springframework.data.neo4j.core.schema.CompositeProperty -@DomainNode +@DomainNode(searchQueryName = "searchIMSProjects") @GraphQLDescription( """Project on an IMS, represents a Trackable synced to an IMS. The representation on the IMS depends on the type of IMS, e.g. for GitHub, a project is a repository. diff --git a/core/src/main/kotlin/gropius/model/template/IMSTemplate.kt b/core/src/main/kotlin/gropius/model/template/IMSTemplate.kt index 0438ce1a..c33df524 100644 --- a/core/src/main/kotlin/gropius/model/template/IMSTemplate.kt +++ b/core/src/main/kotlin/gropius/model/template/IMSTemplate.kt @@ -6,7 +6,7 @@ import io.github.graphglue.model.Direction import io.github.graphglue.model.DomainNode import io.github.graphglue.model.NodeRelationship -@DomainNode("imsTemplates") +@DomainNode("imsTemplates", searchQueryName = "searchIMSTemplates") @GraphQLDescription( """Template for imss Defines templated fields with specific types (defined using JSON schema). From bfcdbbe14a0aa0153a357f3e34786b04cd4eb583 Mon Sep 17 00:00:00 2001 From: nk-coding Date: Sat, 24 Aug 2024 01:11:14 +0200 Subject: [PATCH 3/8] add fields to check if sync self/others is allowed --- .../gropius/graphql/GraphQLConfiguration.kt | 52 +++++++++++++++++++ .../architecture/SyncPermissionTarget.kt | 13 +++++ 2 files changed, 65 insertions(+) diff --git a/api-common/src/main/kotlin/gropius/graphql/GraphQLConfiguration.kt b/api-common/src/main/kotlin/gropius/graphql/GraphQLConfiguration.kt index 213c311d..588e398c 100644 --- a/api-common/src/main/kotlin/gropius/graphql/GraphQLConfiguration.kt +++ b/api-common/src/main/kotlin/gropius/graphql/GraphQLConfiguration.kt @@ -14,6 +14,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import graphql.Scalars import graphql.scalars.regex.RegexScalar import graphql.schema.* +import gropius.authorization.GropiusAuthorizationContext import gropius.authorization.checkPermission import gropius.authorization.gropiusAuthorizationContext import gropius.graphql.filter.* @@ -33,6 +34,7 @@ import io.github.graphglue.connection.filter.definition.scalars.StringFilterDefi import io.github.graphglue.definition.ExtensionFieldDefinition import io.github.graphglue.definition.NodeDefinition import io.github.graphglue.definition.NodeDefinitionCollection +import io.github.graphglue.graphql.extensions.authorizationContext import org.neo4j.cypherdsl.core.Cypher import org.neo4j.cypherdsl.core.Expression import org.neo4j.driver.Driver @@ -309,6 +311,56 @@ class GraphQLConfiguration { } } + /** + * Provides the [ExtensionFieldDefinition] for checking if the current user allows to sync their data to a target + */ + @Bean(SYNC_SELF_ALLOWED_FIELD_BEAN) + fun syncSelfAllowedField(): ExtensionFieldDefinition = syncAllowedField(true) + + /** + * Provides the [ExtensionFieldDefinition] for checking if the current user allows to sync other users' data to a target + */ + @Bean(SYNC_OTHERS_ALLOWED_FIELD_BEAN) + fun syncOthersAllowedField(): ExtensionFieldDefinition = syncAllowedField(false) + + /** + * Provides the [ExtensionFieldDefinition] for checking if the current user allows to sync data to a target + * + * @param self if the field is the self or other variant + * @return the generated field definition + */ + private fun syncAllowedField(self: Boolean): ExtensionFieldDefinition { + val field = GraphQLFieldDefinition.newFieldDefinition().name("isSync${if (self) "Self" else "Others"}Allowed") + .description("Checks if the current user allows to sync ${if (self) "their" else "other users'"} data to this target") + .type(GraphQLNonNull(Scalars.GraphQLBoolean)).build() + + return object : ExtensionFieldDefinition(field) { + override fun generateFetcher( + dfe: DataFetchingEnvironment, + arguments: Map, + node: org.neo4j.cypherdsl.core.Node, + nodeDefinition: NodeDefinition + ): Expression { + val authorizationContext = dfe.authorizationContext as? GropiusAuthorizationContext + return if (authorizationContext != null) { + Cypher.exists( + node.relationshipFrom( + authorizationContext.userNode, + if (self) GropiusUser.CAN_SYNC_SELF else GropiusUser.CAN_SYNC_OTHERS + ) + ) + } else { + Cypher.literalFalse() + } + } + + override fun transformResult(result: Value): Any { + return result.asBoolean() + } + + } + } + /** * Provides the [KotlinDataFetcherFactoryProvider] which generates a FunctionDataFetcher which handles * JSON input value injecting correctly. diff --git a/core/src/main/kotlin/gropius/model/architecture/SyncPermissionTarget.kt b/core/src/main/kotlin/gropius/model/architecture/SyncPermissionTarget.kt index 9eb5ff2b..4c20bb04 100644 --- a/core/src/main/kotlin/gropius/model/architecture/SyncPermissionTarget.kt +++ b/core/src/main/kotlin/gropius/model/architecture/SyncPermissionTarget.kt @@ -4,10 +4,23 @@ import com.expediagroup.graphql.generator.annotations.GraphQLDescription import gropius.model.common.NamedNode import gropius.model.user.GropiusUser import io.github.graphglue.model.Direction +import io.github.graphglue.model.ExtensionField import io.github.graphglue.model.FilterProperty import io.github.graphglue.model.NodeRelationship +/** + * Name of the bean which provides the sync allowed extension field + */ +const val SYNC_SELF_ALLOWED_FIELD_BEAN = "syncSelfAllowedFieldBean" + +/** + * Name of the bean which provides the sync allowed extension field + */ +const val SYNC_OTHERS_ALLOWED_FIELD_BEAN = "syncOthersAllowedFieldBean" + @GraphQLDescription("A target where users can configure how the sync should behave.") +@ExtensionField(SYNC_SELF_ALLOWED_FIELD_BEAN) +@ExtensionField(SYNC_OTHERS_ALLOWED_FIELD_BEAN) abstract class SyncPermissionTarget(name: String, description: String) : NamedNode(name, description) { @NodeRelationship(GropiusUser.CAN_SYNC_SELF, Direction.INCOMING) From 427b014b53bc82b7f0dca7006ad02a57eec61470 Mon Sep 17 00:00:00 2001 From: nk-coding Date: Sat, 24 Aug 2024 04:17:30 +0200 Subject: [PATCH 4/8] more order fields, make inputs for update sync permissions optional --- .../UpdateSyncPermissionsInput.kt | 4 ++-- .../gropius/model/architecture/IMSProject.kt | 1 + .../SyncPermissionTargetService.kt | 20 +++++++++++-------- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/core/src/main/kotlin/gropius/dto/input/architecture/UpdateSyncPermissionsInput.kt b/core/src/main/kotlin/gropius/dto/input/architecture/UpdateSyncPermissionsInput.kt index 778474ed..9f78f5f4 100644 --- a/core/src/main/kotlin/gropius/dto/input/architecture/UpdateSyncPermissionsInput.kt +++ b/core/src/main/kotlin/gropius/dto/input/architecture/UpdateSyncPermissionsInput.kt @@ -9,7 +9,7 @@ class UpdateSyncPermissionsInput( @GraphQLDescription("The SyncPermissionTarget to update the sync permissions for the current user") val id: ID, @GraphQLDescription("Whether the sync service is allowed to sync content of the user") - val canSyncSelf: Boolean, + val canSyncSelf: Boolean?, @GraphQLDescription("Whether the sync service is allowed to sync content of other users") - val canSyncOthers: Boolean + val canSyncOthers: Boolean? ) : Input() \ No newline at end of file diff --git a/core/src/main/kotlin/gropius/model/architecture/IMSProject.kt b/core/src/main/kotlin/gropius/model/architecture/IMSProject.kt index 8ae0b3a6..e317d4de 100644 --- a/core/src/main/kotlin/gropius/model/architecture/IMSProject.kt +++ b/core/src/main/kotlin/gropius/model/architecture/IMSProject.kt @@ -48,6 +48,7 @@ class IMSProject( @GraphQLDescription("The IMS this project is a part of.") @GraphQLNullable @FilterProperty + @OrderProperty val ims by NodeProperty() @NodeRelationship(IMSIssue.PROJECT, Direction.INCOMING) diff --git a/core/src/main/kotlin/gropius/service/architecture/SyncPermissionTargetService.kt b/core/src/main/kotlin/gropius/service/architecture/SyncPermissionTargetService.kt index 59c691ef..964db1f2 100644 --- a/core/src/main/kotlin/gropius/service/architecture/SyncPermissionTargetService.kt +++ b/core/src/main/kotlin/gropius/service/architecture/SyncPermissionTargetService.kt @@ -35,15 +35,19 @@ class SyncPermissionTargetService( val target = repository.findById(input.id) checkPermission(target, Permission(NodePermission.READ, authorizationContext), "use the SyncPermissionTarget") val user = getUser(authorizationContext) - if (input.canSyncSelf) { - target.syncSelfAllowedBy() += user - } else { - target.syncSelfAllowedBy() -= user + if (input.canSyncSelf != null) { + if (input.canSyncSelf) { + target.syncSelfAllowedBy() += user + } else { + target.syncSelfAllowedBy() -= user + } } - if (input.canSyncOthers) { - target.syncOthersAllowedBy() += user - } else { - target.syncOthersAllowedBy() -= user + if (input.canSyncOthers != null) { + if (input.canSyncOthers) { + target.syncOthersAllowedBy() += user + } else { + target.syncOthersAllowedBy() -= user + } } return repository.save(target).awaitSingle() } From 59808ea880a17da3dfd2ab6a3223f2a8bf8532ba Mon Sep 17 00:00:00 2001 From: Christian Kurz Date: Tue, 27 Aug 2024 12:20:28 +0200 Subject: [PATCH 5/8] docs & move gropiusUserList --- .../gropius/sync/github/GithubDataService.kt | 2 ++ .../kotlin/gropius/sync/github/GithubSync.kt | 21 +------------------ .../main/kotlin/gropius/sync/jira/JiraSync.kt | 21 ------------------- .../main/kotlin/gropius/sync/AbstractSync.kt | 20 ++++++++++++++++++ 4 files changed, 23 insertions(+), 41 deletions(-) diff --git a/github/src/main/kotlin/gropius/sync/github/GithubDataService.kt b/github/src/main/kotlin/gropius/sync/github/GithubDataService.kt index 9029755f..e318fc5d 100644 --- a/github/src/main/kotlin/gropius/sync/github/GithubDataService.kt +++ b/github/src/main/kotlin/gropius/sync/github/GithubDataService.kt @@ -213,6 +213,7 @@ class GithubDataService( * @param imsProject The IMSProject to work on * @param users The users sorted with best first * @param body The content of the mutation + * @param owner The user that created the data * @return The selected user and the response for the mutation */ final suspend inline fun mutation( @@ -248,6 +249,7 @@ class GithubDataService( * @param imsProject The IMSProject to work on * @param users The users sorted with best first * @param body The content of the query + * @param owner The user that created the data * @return The selected user and the response for the query */ final suspend inline fun query( diff --git a/github/src/main/kotlin/gropius/sync/github/GithubSync.kt b/github/src/main/kotlin/gropius/sync/github/GithubSync.kt index 0b16c5c0..1bcee93a 100644 --- a/github/src/main/kotlin/gropius/sync/github/GithubSync.kt +++ b/github/src/main/kotlin/gropius/sync/github/GithubSync.kt @@ -156,26 +156,7 @@ final class GithubSync( override suspend fun findUnsyncedIssues(imsProject: IMSProject): List { return issuePileService.findByImsProjectAndHasUnsyncedData(imsProject.rawId!!, true) } - - /** - * Map list of User to GropiusUser - * @param users The list of users mixed of IMSUser and GropiusUser - * @return The list of GropiusUser - */ - private suspend fun gropiusUserList(users: List): List { - val outputUsers = users.mapNotNull { - when (it) { - is GropiusUser -> it - is IMSUser -> it.gropiusUser().value - else -> null - } - } - if (outputUsers.isEmpty() && users.isNotEmpty()) { - throw IllegalStateException("No Gropius User left as owner") - } - return outputUsers - } - + override suspend fun syncComment( imsProject: IMSProject, issueId: String, issueComment: IssueComment, users: List ): TimelineItemConversionInformation? { diff --git a/jira/src/main/kotlin/gropius/sync/jira/JiraSync.kt b/jira/src/main/kotlin/gropius/sync/jira/JiraSync.kt index be27158f..2e3bf451 100644 --- a/jira/src/main/kotlin/gropius/sync/jira/JiraSync.kt +++ b/jira/src/main/kotlin/gropius/sync/jira/JiraSync.kt @@ -6,8 +6,6 @@ import gropius.model.issue.Label import gropius.model.issue.timeline.IssueComment import gropius.model.template.IMSTemplate import gropius.model.template.IssueState -import gropius.model.user.GropiusUser -import gropius.model.user.IMSUser import gropius.model.user.User import gropius.sync.* import gropius.sync.jira.config.IMSConfigManager @@ -184,25 +182,6 @@ final class JiraSync( return issueDataService.findByImsProject(imsProject.rawId!!) } - /** - * Map list of User to GropiusUser - * @param users The list of users mixed of IMSUser and GropiusUser - * @return The list of GropiusUser - */ - private suspend fun gropiusUserList(users: List): List { - val outputUsers = users.mapNotNull { - when (it) { - is GropiusUser -> it - is IMSUser -> it.gropiusUser().value - else -> null - } - } - if (outputUsers.isEmpty() && users.isNotEmpty()) { - throw IllegalStateException("No Gropius User left as owner") - } - return outputUsers - } - override suspend fun syncComment( imsProject: IMSProject, issueId: String, issueComment: IssueComment, users: List ): TimelineItemConversionInformation? { diff --git a/sync/src/main/kotlin/gropius/sync/AbstractSync.kt b/sync/src/main/kotlin/gropius/sync/AbstractSync.kt index e46f6c9e..2847c651 100644 --- a/sync/src/main/kotlin/gropius/sync/AbstractSync.kt +++ b/sync/src/main/kotlin/gropius/sync/AbstractSync.kt @@ -10,6 +10,7 @@ import gropius.model.template.IMSTemplate import gropius.model.template.IssueState import gropius.model.template.IssueType import gropius.model.user.GropiusUser +import gropius.model.user.IMSUser import gropius.model.user.User import gropius.repository.common.NodeRepository import gropius.repository.issue.IssueRepository @@ -885,4 +886,23 @@ abstract class AbstractSync( } logger.info("Finished Sync Cycle") } + + /** + * Map list of User to GropiusUser + * @param users The list of users mixed of IMSUser and GropiusUser + * @return The list of GropiusUser + */ + suspend fun gropiusUserList(users: List): List { + val outputUsers = users.mapNotNull { + when (it) { + is GropiusUser -> it + is IMSUser -> it.gropiusUser().value + else -> null + } + } + if (outputUsers.isEmpty() && users.isNotEmpty()) { + throw IllegalStateException("No Gropius User left as owner") + } + return outputUsers + } } From 57336cbc40b71b98c99e68ad31f646fca13c805c Mon Sep 17 00:00:00 2001 From: Christian Kurz Date: Tue, 27 Aug 2024 12:20:49 +0200 Subject: [PATCH 6/8] @nk-coding did UI --- sync/src/main/kotlin/gropius/sync/TokenManager.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/sync/src/main/kotlin/gropius/sync/TokenManager.kt b/sync/src/main/kotlin/gropius/sync/TokenManager.kt index a2c9f31c..c42ff05a 100644 --- a/sync/src/main/kotlin/gropius/sync/TokenManager.kt +++ b/sync/src/main/kotlin/gropius/sync/TokenManager.kt @@ -155,7 +155,6 @@ abstract class TokenManager( * @return true if the user is allowed */ private suspend fun isAllowed(imsProject: IMSProject, user: IMSUser, owner: List): Boolean { - return true // TODO: ignoring until @nk-coding does UI val ownerSet = owner.toSet() if ((owner.isEmpty() || ownerSet.contains(user.gropiusUser().value)) && imsProject.ims().value.syncSelfAllowedBy() .contains(user.gropiusUser().value) From ad4c838c7d3ed1945ec6fd206f354b1beb5135cc Mon Sep 17 00:00:00 2001 From: Christian Kurz Date: Tue, 27 Aug 2024 12:22:03 +0200 Subject: [PATCH 7/8] more dokka --- sync/src/main/kotlin/gropius/sync/TokenManager.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sync/src/main/kotlin/gropius/sync/TokenManager.kt b/sync/src/main/kotlin/gropius/sync/TokenManager.kt index c42ff05a..b686cf0e 100644 --- a/sync/src/main/kotlin/gropius/sync/TokenManager.kt +++ b/sync/src/main/kotlin/gropius/sync/TokenManager.kt @@ -178,6 +178,7 @@ abstract class TokenManager( /** * Attempt a query for a list of users until it works * + * @param imsProject The IMS to work with * @param users The list of users, sorted with best first * @param executor The function to execute * @param owner The user that created the data @@ -210,7 +211,7 @@ abstract class TokenManager( /** * Attempt a query for a list of users until it works * - * @param ims The IMS to work with + * @param imsProject The IMS to work with * @param user The list of users, sorted with best first * @param executor The function to execute * @param owner The user that created the data From d41ca9e0c667e2be509e81d25615033f61d68351 Mon Sep 17 00:00:00 2001 From: Christian Kurz Date: Tue, 27 Aug 2024 12:23:32 +0200 Subject: [PATCH 8/8] more text for owner semantics --- .../main/kotlin/gropius/sync/github/GithubDataService.kt | 4 ++-- sync/src/main/kotlin/gropius/sync/TokenManager.kt | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/github/src/main/kotlin/gropius/sync/github/GithubDataService.kt b/github/src/main/kotlin/gropius/sync/github/GithubDataService.kt index e318fc5d..9fcc4153 100644 --- a/github/src/main/kotlin/gropius/sync/github/GithubDataService.kt +++ b/github/src/main/kotlin/gropius/sync/github/GithubDataService.kt @@ -213,7 +213,7 @@ class GithubDataService( * @param imsProject The IMSProject to work on * @param users The users sorted with best first * @param body The content of the mutation - * @param owner The user that created the data + * @param owner The user that created the data, empty if fetching/other non-owned operations * @return The selected user and the response for the mutation */ final suspend inline fun mutation( @@ -249,7 +249,7 @@ class GithubDataService( * @param imsProject The IMSProject to work on * @param users The users sorted with best first * @param body The content of the query - * @param owner The user that created the data + * @param owner The user that created the data, empty if fetching/other non-owned operations * @return The selected user and the response for the query */ final suspend inline fun query( diff --git a/sync/src/main/kotlin/gropius/sync/TokenManager.kt b/sync/src/main/kotlin/gropius/sync/TokenManager.kt index b686cf0e..4cf171ee 100644 --- a/sync/src/main/kotlin/gropius/sync/TokenManager.kt +++ b/sync/src/main/kotlin/gropius/sync/TokenManager.kt @@ -151,7 +151,7 @@ abstract class TokenManager( * * @param imsProject The IMS to work with * @param user The user to check - * @param owner The user that created the data + * @param owner The user that created the data, empty if fetching/other non-owned operations * @return true if the user is allowed */ private suspend fun isAllowed(imsProject: IMSProject, user: IMSUser, owner: List): Boolean { @@ -181,7 +181,7 @@ abstract class TokenManager( * @param imsProject The IMS to work with * @param users The list of users, sorted with best first * @param executor The function to execute - * @param owner The user that created the data + * @param owner The user that created the data, empty if fetching/other non-owned operations * * @return The user it worked with and the result of the executor */ @@ -214,7 +214,7 @@ abstract class TokenManager( * @param imsProject The IMS to work with * @param user The list of users, sorted with best first * @param executor The function to execute - * @param owner The user that created the data + * @param owner The user that created the data, empty if fetching/other non-owned operations * * @return The user it worked with and the result of the executor */