Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into feature/jiraSyncOnlyU…
Browse files Browse the repository at this point in the history
…seful
  • Loading branch information
chriku committed Aug 27, 2024
2 parents 8cf3ebd + d1d1a8b commit 894930b
Show file tree
Hide file tree
Showing 13 changed files with 203 additions and 50 deletions.
52 changes: 52 additions & 0 deletions api-common/src/main/kotlin/gropius/graphql/GraphQLConfiguration.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand All @@ -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
Expand Down Expand Up @@ -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<String, Any?>,
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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
2 changes: 1 addition & 1 deletion core/src/main/kotlin/gropius/model/architecture/IMS.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -48,6 +48,7 @@ class IMSProject(
@GraphQLDescription("The IMS this project is a part of.")
@GraphQLNullable
@FilterProperty
@OrderProperty
val ims by NodeProperty<IMS>()

@NodeRelationship(IMSIssue.PROJECT, Direction.INCOMING)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion core/src/main/kotlin/gropius/model/template/IMSTemplate.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,10 +213,11 @@ 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, empty if fetching/other non-owned operations
* @return The selected user and the response for the mutation
*/
final suspend inline fun <reified D : Mutation.Data> mutation(
imsProject: IMSProject, users: List<User>, body: Mutation<D>
imsProject: IMSProject, users: List<User>, body: Mutation<D>, owner: List<GropiusUser>
): Pair<IMSUser, ApolloResponse<D>> {
val imsConfig = IMSConfig(helper, imsProject.ims().value, imsProject.ims().value.template().value)
val userList = users.toMutableList()
Expand All @@ -228,7 +229,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()
Expand All @@ -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, empty if fetching/other non-owned operations
* @return The selected user and the response for the query
*/
final suspend inline fun <reified D : Query.Data> query(
Expand All @@ -267,7 +269,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()
Expand Down
35 changes: 24 additions & 11 deletions github/src/main/kotlin/gropius/sync/github/GithubSync.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -154,13 +156,15 @@ final class GithubSync(
override suspend fun findUnsyncedIssues(imsProject: IMSProject): List<IncomingIssue> {
return issuePileService.findByImsProjectAndHasUnsyncedData(imsProject.rawId!!, true)
}

override suspend fun syncComment(
imsProject: IMSProject, issueId: String, issueComment: IssueComment, users: List<User>
): 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)
Expand All @@ -180,8 +184,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)
Expand All @@ -194,8 +199,9 @@ final class GithubSync(
override suspend fun syncTitleChange(
imsProject: IMSProject, issueId: String, newTitle: String, users: List<User>
): 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)
Expand All @@ -209,7 +215,9 @@ final class GithubSync(
imsProject: IMSProject, issueId: String, newState: IssueState, users: List<User>
): 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)
Expand All @@ -218,7 +226,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)
Expand All @@ -234,8 +244,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)
Expand All @@ -258,7 +269,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) {
Expand Down
3 changes: 2 additions & 1 deletion jira/src/main/kotlin/gropius/sync/jira/JiraDataService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -296,11 +296,12 @@ class JiraDataService(
imsProject: IMSProject,
users: List<User>,
requestMethod: HttpMethod,
owner: List<GropiusUser>,
body: T? = null,
crossinline urlBuilder: URLBuilder .(URLBuilder) -> Unit
): Pair<IMSUser, HttpResponse> {
val userList = collectRequestUsers(imsProject, users)
return tokenManager.executeUntilWorking(imsProject.ims().value, userList) {
return tokenManager.executeUntilWorking(imsProject, userList, owner) {
sendRequest<T>(
imsProject, requestMethod, body, urlBuilder, it
)
Expand Down
Loading

0 comments on commit 894930b

Please sign in to comment.