Skip to content

Commit

Permalink
Merge pull request #8892 from rundeck/RSE-955-update-existing-webhook…
Browse files Browse the repository at this point in the history
…s-at-project-import-based-on-RSE-979

RSE-955 FIX: Allow to update existing webhooks at project import
  • Loading branch information
Jesus-Osuna-M committed Feb 23, 2024
2 parents 5b3c79c + 3f89805 commit 9faa6ec
Show file tree
Hide file tree
Showing 12 changed files with 104 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Call<ProjectImportStatus> importProjectArchive(
@Query("importScm") Boolean importScm,
@Query("importWebhooks") Boolean importWebhooks,
@Query("whkRegenAuthTokens") Boolean whkRegenAuthTokens,
@Query("whkRegenUuid") Boolean whkRegenUuid,
@Query("importNodesSources") Boolean importNodesSources,
@QueryMap Map<String,String> params,
@Body RequestBody body
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public Response<ProjectImportStatus> importProjectArchive(
Boolean importScm,
Boolean importWebhooks,
Boolean whkRegenAuthTokens,
Boolean whkRegenUuid,
Boolean importNodesSources,
Map<String,String> params,
RequestBody requestBody
Expand All @@ -63,6 +64,7 @@ public Response<ProjectImportStatus> importProjectArchive(
importScm,
importWebhooks,
whkRegenAuthTokens,
whkRegenUuid,
importNodesSources,
params,
requestBody
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ class ProjectExportSpec extends SeleniumBase {
then:
projectExportPage.checkBoxes.size() >= 9
projectExportPage.checkBoxes.count {it.getAttribute("checked") == "true" } >= 8
projectExportPage.checkBoxes.count {it.getAttribute("checked") == null } == 1
projectExportPage.checkBoxes.count {it.getAttribute("checked") == null } == 2
}

def "form checkbox labels work"() {
Expand All @@ -60,7 +60,7 @@ class ProjectExportSpec extends SeleniumBase {
projectExportPage.checkBoxLabel checkBoxId click()
}
expect:
projectExportPage.checkBoxes.count {it.getAttribute("checked") == "true" } == 1
projectExportPage.checkBoxes.count {it.getAttribute("checked") == "true" } == 2
projectExportPage.checkBoxes.count {it.getAttribute("checked") == null } >= 8
}

Expand Down
30 changes: 17 additions & 13 deletions grails-webhooks/grails-app/services/webhooks/WebhookService.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -182,9 +182,9 @@ class WebhookService {
def saveHook(UserAndRolesAuthContext authContext, def hookData) {
RdWebhook hook = null
SaveWebhookRequest saveWebhookRequest = mapperSaveRequest(hookData)
boolean shouldUpdate = false
boolean shouldUpdate = hookData.update ?: false
if(saveWebhookRequest.id) {
hook = webhookDataProvider.getWebhookByUuid(saveWebhookRequest.uuid)
hook = webhookDataProvider.findByUuidAndProject(saveWebhookRequest.uuid,saveWebhookRequest.project)
if (!hook) return [err: "Webhook not found"]

shouldUpdate = true
Expand All @@ -197,15 +197,10 @@ class WebhookService {
}
}
} else {
int countByNameInProject = webhookDataProvider.countByNameAndProject(saveWebhookRequest.name, saveWebhookRequest.project)
if(countByNameInProject > 0) return [err: "A Webhook by that name already exists in this project"]
String checkUser = hookData.user ?: authContext.username
if (!hookData.importData && !userService.validateUserExists(checkUser)) return [err: "Webhook user '${checkUser}' not found"]
saveWebhookRequest.setUuid(UUID.randomUUID().toString())
}
def whsFound = webhookDataProvider.findAllByNameAndProjectAndUuidNotEqual(saveWebhookRequest.name, saveWebhookRequest.project, saveWebhookRequest.uuid)
if( whsFound.size() > 0) {
return [err: "A Webhook by that name already exists in this project"]
saveWebhookRequest.setUuid(hookData.uuid ?: UUID.randomUUID().toString())

}
String generatedSecureString = null
if(hookData.useAuth == true && hookData.regenAuth == true) {
Expand Down Expand Up @@ -258,10 +253,15 @@ class WebhookService {
}
saveWebhookRequest.authToken = hookData.authToken
}
if(hookData.regenUuid){
saveWebhookRequest.setUuid(UUID.randomUUID().toString())
}
SaveWebhookResponse saveWebhookResponse
if(shouldUpdate){
saveWebhookResponse = webhookDataProvider.updateWebhook(saveWebhookRequest)
}else{
if (shouldUpdate) {
saveWebhookResponse = hookData.regenUuid
? webhookDataProvider.createWebhook(saveWebhookRequest)
: webhookDataProvider.updateWebhook(saveWebhookRequest)
} else {
saveWebhookResponse = webhookDataProvider.createWebhook(saveWebhookRequest)
}

Expand Down Expand Up @@ -343,11 +343,12 @@ class WebhookService {
}
}

def importWebhook(UserAndRolesAuthContext authContext, Map hook, boolean regenAuthTokens) {
def importWebhook(UserAndRolesAuthContext authContext, Map hook, boolean regenAuthTokens,boolean regenUuid) {

RdWebhook existing = webhookDataProvider.findByUuidAndProject(hook.uuid, hook.project)
if(existing) {
hook.id = existing.id
hook.update = true
} else {
hook.id = null
}
Expand All @@ -358,6 +359,9 @@ class WebhookService {
} else {
hook.authToken = null
}
if(regenUuid){
hook.regenUuid = true
}

try {
def msg = saveHook(authContext, hook)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,22 @@ class WebhooksProjectExporter implements ProjectDataExporter {
private static final ObjectMapper mapper = new ObjectMapper()
public static final String INLUDE_AUTH_TOKENS = 'inludeAuthTokens'
public static final String WEBHOOKS_YAML_FILE = "webhooks.yaml"
public static final String WHK_REGEN_UUID = 'regenUuid'

def webhookService

final List<Property> exportProperties = [
PropertyBuilder.builder().
booleanType(INLUDE_AUTH_TOKENS).
title('Include Webhook Auth Tokens').
description('If not included, tokens will be regenerated upon import.').
build()
PropertyBuilder.builder().with{
booleanType(INLUDE_AUTH_TOKENS).
title('Include Webhook Auth Tokens').
description('If not included, tokens will be regenerated upon import.'
)}.build(),
PropertyBuilder.builder().with {
booleanType(WHK_REGEN_UUID).
title('Remove UUIDs').
description('Strip UUIDs for exported Webhook'
)}.
build()
]

@Override
Expand All @@ -62,6 +69,9 @@ class WebhooksProjectExporter implements ProjectDataExporter {
if (exportOptions[INLUDE_AUTH_TOKENS] == 'true') {
data.authToken = hk.authToken
}
if (exportOptions[WHK_REGEN_UUID] == 'true') {
data.uuid = UUID.randomUUID().toString()
}
export.webhooks.add(data)
}
zipBuilder.file(WEBHOOKS_YAML_FILE) { writer ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,26 @@ class WebhooksProjectImporter implements ProjectDataImporter {

private static final Logger logger = LoggerFactory.getLogger(WebhooksProjectImporter)
public static final String WHK_REGEN_AUTH_TOKENS = 'regenAuthTokens'
public static final String WHK_REGEN_UUID = 'regenUuid'

def webhookService

final List<String> importFilePatterns = [WebhooksProjectExporter.WEBHOOKS_YAML_FILE]

final List<Property> importProperties = [
PropertyBuilder.builder().
PropertyBuilder.builder().with {
booleanType(WHK_REGEN_AUTH_TOKENS).
title('Create and overwrite a new Webhook Auth Token').
description(
'Regenerate all webhook auth tokens. If unchecked only webhooks without defined auth tokens will have' +
' their auth tokens regenerated.'
).
)}.
build(),
PropertyBuilder.builder().with {
booleanType(WHK_REGEN_UUID).
title('Remove UUIDs').
description('Strip UUIDs from imported Webhook'
)}.
build()
]

Expand Down Expand Up @@ -71,7 +78,8 @@ class WebhooksProjectImporter implements ProjectDataImporter {
def importResult = webhookService.importWebhook(
authContext,
hook,
importOptions[WHK_REGEN_AUTH_TOKENS] == 'true'
importOptions[WHK_REGEN_AUTH_TOKENS] == 'true',
importOptions[WHK_REGEN_UUID] == 'true'
)
if (importResult.msg) {
logger.debug(importResult.msg)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,16 @@ class WebhooksProjectImporterSpec extends Specification {
def authContext = Mock(UserAndRolesAuthContext)
def files=['webhooks.yaml':new File(getClass().getClassLoader().getResource("webhooks.yaml").toURI())]
when:
def errors = importer.doImport(authContext, "webhook", files, [regenAuthTokens: false])
def errors = importer.doImport(authContext, "webhook", files, [:])

then:
errors.isEmpty()
2 * importer.webhookService.importWebhook(_,_,_) >> {
2 * importer.webhookService.importWebhook(_,_,_,_) >> {
[msg:"ok"]
}
}

static interface MockWebhookService {
def importWebhook(UserAndRolesAuthContext authContext, def hookData, boolean regenFlag)
def importWebhook(UserAndRolesAuthContext authContext, def hookData, boolean regenFlag,boolean regenUuid)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ class ProjectController extends ControllerBase{
archiveParams.importComponents = [(WebhooksProjectComponent.COMPONENT_NAME): true]
}
archiveParams.importOpts[WebhooksProjectComponent.COMPONENT_NAME][WebhooksProjectImporter.WHK_REGEN_AUTH_TOKENS] = (String) params.whkRegenAuthTokens
archiveParams.importOpts[WebhooksProjectComponent.COMPONENT_NAME][WebhooksProjectImporter.WHK_REGEN_UUID] = (String) params.whkRegenUuid
}

if (archiveParams.hasErrors()) {
Expand Down Expand Up @@ -2828,6 +2829,8 @@ Requires `export` authorization for the project resource.""",
}
archiveParams.exportOpts[WebhooksProjectComponent.COMPONENT_NAME][
WebhooksProjectExporter.INLUDE_AUTH_TOKENS] = params.whkIncludeAuthTokens
archiveParams.exportOpts[WebhooksProjectComponent.COMPONENT_NAME][
WebhooksProjectExporter.WHK_REGEN_UUID] = params.whkRegenUuid
}

ProjectArchiveExportRequest options
Expand Down Expand Up @@ -3312,6 +3315,8 @@ Note: `other_errors` included since API v35""",
}
archiveParams.importOpts[WebhooksProjectComponent.COMPONENT_NAME][
WebhooksProjectImporter.WHK_REGEN_AUTH_TOKENS] = params.whkRegenAuthTokens
archiveParams.importOpts[WebhooksProjectComponent.COMPONENT_NAME][
WebhooksProjectImporter.WHK_REGEN_UUID] = params.whkRegenUuid
}

//previous version must import nodes together with the project config
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -727,7 +727,8 @@ class ProjectService implements InitializingBean, ExecutionFileProducer, EventPu
exportArchiveParams.exportAcls,
exportArchiveParams.exportScm,
importWebhookOpt,
importWebhookOpt && !Boolean.getBoolean(exportArchiveParams.exportOpts[WebhooksProjectComponent.COMPONENT_NAME]['inludeAuthTokens']),
importWebhookOpt && !Boolean.valueOf(exportArchiveParams.exportOpts[WebhooksProjectComponent.COMPONENT_NAME]['inludeAuthTokens']),
importWebhookOpt && Boolean.valueOf(exportArchiveParams.exportOpts[WebhooksProjectComponent.COMPONENT_NAME]['regenUuid']),
exportArchiveParams.exportConfigs,
prependStringToKeysInMap('importComponents', exportArchiveParams.exportComponents),
RequestBody.create(archiveToExport, RundeckClient.MEDIA_TYPE_ZIP)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -354,18 +354,20 @@ class ProjectControllerSpec extends Specification implements ControllerUnitTest<
true | false | false | false | false | false
}

def "api v34 exportAll include webhooks auth tokens when whkIncludeAuthTokens is set to true"(){
def "api v34 exportAll include webhooks auth tokens when whkIncludeAuthTokens && whkRegenUuid are set to true"(){

given:"a project to be exported"
controller.projectService = Mock(ProjectService)
controller.apiService = Mock(ApiService)
controller.frameworkService = Mock(FrameworkService)
setupGetResource()
params.project = 'aproject'
Map<String, String> exportOpts = [(WebhooksProjectExporter.INLUDE_AUTH_TOKENS):"true",(WebhooksProjectExporter.WHK_REGEN_UUID):"true"]

when:"exporting the project using the API"
params.exportAll = "true"
params.whkIncludeAuthTokens = "true"
params.whkRegenUuid = "true"
request.api_version = ApiVersions.V34
controller.apiProjectExport()

Expand All @@ -374,7 +376,7 @@ class ProjectControllerSpec extends Specification implements ControllerUnitTest<
1 * controller.apiService.requireApi(_, _) >> true
1 * controller.projectService.exportProjectToOutputStream(_, _, _, _, { ArchiveOptions opts ->
opts.all == true &&
opts.exportOpts[WebhooksProjectComponent.COMPONENT_NAME]==[(WebhooksProjectExporter.INLUDE_AUTH_TOKENS):"true"]
opts.exportOpts[WebhooksProjectComponent.COMPONENT_NAME]==exportOpts
},_
)
}
Expand Down Expand Up @@ -429,10 +431,12 @@ class ProjectControllerSpec extends Specification implements ControllerUnitTest<
controller.frameworkService = Mock(FrameworkService)
setupGetResource()

Map<String, String> exportOpts = [(WebhooksProjectExporter.INLUDE_AUTH_TOKENS):whinclude.toString(),(WebhooksProjectExporter.WHK_REGEN_UUID):regenUuid.toString()]
params.project = 'aproject'

params.exportWebhooks=whenable.toString()
params.whkIncludeAuthTokens=whinclude.toString()
params.whkRegenUuid=regenUuid.toString()
request.api_version = 34

when:
Expand All @@ -443,14 +447,14 @@ class ProjectControllerSpec extends Specification implements ControllerUnitTest<

1 * controller.projectService.exportProjectToOutputStream(_, _, _, _, { ArchiveOptions opts ->
opts.exportComponents[WebhooksProjectComponent.COMPONENT_NAME] == whenable &&
opts.exportOpts[WebhooksProjectComponent.COMPONENT_NAME]==[(WebhooksProjectExporter.INLUDE_AUTH_TOKENS):whinclude.toString()]
opts.exportOpts[WebhooksProjectComponent.COMPONENT_NAME]==exportOpts
},_
)

where:
whenable | whinclude
true | true
true | false
whenable | whinclude | regenUuid
true | true | true
true | false | false
}

def "api project delete error"() {
Expand Down Expand Up @@ -2263,6 +2267,9 @@ class ProjectControllerSpec extends Specification implements ControllerUnitTest<
request.content = 'test'.bytes
params.importWebhooks='true'
params.whkRegenAuthTokens='true'
params.whkRegenUuid='true'
Map<String, String> exportOpts = [(WebhooksProjectImporter.WHK_REGEN_AUTH_TOKENS):"true",
(WebhooksProjectImporter.WHK_REGEN_UUID):"true"]
when: "import project via api"
controller.apiProjectImport(aparams)
then: "webhook component import options are set"
Expand All @@ -2274,8 +2281,7 @@ class ProjectControllerSpec extends Specification implements ControllerUnitTest<
// **Deprecated**
// 1 * controller.projectService.importToProject(project,_,_,_,{ ProjectArchiveImportRequest req->
1 * controller.projectService.handleApiImport(_,_,project,_,{ ProjectArchiveImportRequest req->
req.importComponents == [(WebhooksProjectComponent.COMPONENT_NAME): true]
req.importOpts == [(WebhooksProjectComponent.COMPONENT_NAME): [(WebhooksProjectImporter.WHK_REGEN_AUTH_TOKENS): 'true']]
req.importOpts.webhooks == exportOpts
}) >> [success:true]
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1824,7 +1824,7 @@ class ProjectServiceSpec extends Specification implements ServiceUnitTest<Projec
'node-wizard' : true
],
exportOpts : [
'webhooks' : ['inludeAuthTokens' : 'true']
'webhooks' : ['inludeAuthTokens' : 'true', 'regenUuid':'true']
],
])
File archive = new File("testfile")
Expand Down Expand Up @@ -1855,7 +1855,7 @@ class ProjectServiceSpec extends Specification implements ServiceUnitTest<Projec
'node-wizard' : true
],
exportOpts : [
'webhooks' : ['inludeAuthTokens' : 'true']
'webhooks' : ['inludeAuthTokens' : 'true', 'regenUuid':'true']
],
])
File archive = new File("testfile")
Expand Down Expand Up @@ -1883,7 +1883,7 @@ class ProjectServiceSpec extends Specification implements ServiceUnitTest<Projec
'node-wizard' : true
],
exportOpts : [
'webhooks' : ['inludeAuthTokens' : 'true']
'webhooks' : ['inludeAuthTokens' : 'true', 'regenUuid':'true']
],
])
File archive = new File("testfile")
Expand Down Expand Up @@ -3305,6 +3305,7 @@ abstract class MockRundeckApi implements RundeckApi{
Boolean importScm,
Boolean importWebhooks,
Boolean whkRegenAuthTokens,
Boolean whkRegenUuid,
Boolean importNodesSources,
Map<String,String> params,
RequestBody body
Expand All @@ -3322,6 +3323,6 @@ abstract class MockRundeckApi implements RundeckApi{
response = Response.error(responseStatusCode, errorResponseJsonBody)
}

return delegate.returning(Calls.response(response)).importProjectArchive(project,jobUuidOption, importExecutions, importConfig, importACL, importScm, importWebhooks, whkRegenAuthTokens, importNodesSources, params, body)
return delegate.returning(Calls.response(response)).importProjectArchive(project,jobUuidOption, importExecutions, importConfig, importACL, importScm, importWebhooks, whkRegenAuthTokens,whkRegenUuid, importNodesSources, params, body)
}
}

0 comments on commit 9faa6ec

Please sign in to comment.