Skip to content

Commit

Permalink
Support for associating WebHook templates with Projects
Browse files Browse the repository at this point in the history
This allows a WebHook Template to be created and associated with a
project rather then just _Root.
Now group admins can create templates for use with their own webhooks.
This addresses issue #131

Note: WebHook Template Ids must be unique across a teamcity instance,
even if the user creating the template is not aware and is not
permissioned to see an existing template with that ID.

- Uses templateId rather than templateName when referring to ID.
- Added projectId field and getProjectId method to templates.
- Added validators and permissions
- Fixed up permissions for template editing in REST API
- Added Templates to WebHook Project tab
- Added Templates list to Webhook Project Config tab.
- Added Project Name and link to Templates page.
- WebHooks now resolve project specific templates when building payload.
  • Loading branch information
netwolfuk committed Mar 17, 2020
1 parent f412472 commit 909fce5
Show file tree
Hide file tree
Showing 93 changed files with 4,116 additions and 2,860 deletions.
9 changes: 9 additions & 0 deletions tcwebhooks-core/src/main/java/webhook/Constants.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package webhook;

public class Constants {

private Constants() {}

public static final String ROOT_PROJECT_ID = "_Root";

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package webhook.teamcity;

public interface DeferrableService {

/**
* Register with the deferred startup service.
* Later it will invoke the deferredStart method.
*/
public void requestDeferredRegistration();

/**
* Run any initialisation code after TeamCity has started.
*/
public void register();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package webhook.teamcity;

public interface DeferrableServiceManager {

public void registerService(DeferrableService deferrableService);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package webhook.teamcity;

import java.util.ArrayList;
import java.util.List;

import jetbrains.buildServer.serverSide.BuildServerAdapter;
import jetbrains.buildServer.serverSide.SBuildServer;

public class DeferrableServiceManagerImpl implements DeferrableServiceManager {

List<DeferrableService> deferrableServices = new ArrayList<>();

public DeferrableServiceManagerImpl(SBuildServer server) {
server.addListener(new BuildServerAdapter() {
@Override
public void serverStartup() {
deferrableServices.forEach(DeferrableService::register);
}
});
}

@Override
public void registerService(DeferrableService deferrableService) {
deferrableServices.add(deferrableService);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package webhook.teamcity;

public interface ProjectIdResolver {

String getExternalProjectId(String internalProjectId);
String getInternalProjectId(String externalProjectId);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package webhook.teamcity;

import java.util.Objects;

import jetbrains.buildServer.serverSide.ProjectManager;
import webhook.teamcity.exception.NonExistantProjectException;

public class ProjectIdResolverImpl implements ProjectIdResolver {

private ProjectManager myProjectManager;

public ProjectIdResolverImpl(ProjectManager projectManager) {
myProjectManager = projectManager;
}

@Override
public String getExternalProjectId(String internalProjectId) {
try {
if (Objects.isNull(internalProjectId) || internalProjectId.isEmpty()) {
return(myProjectManager.findProjectById("_Root").getExternalId());
}
return myProjectManager.findProjectById(internalProjectId).getExternalId();
} catch (NullPointerException e) {
throw new NonExistantProjectException("No project found with matching internal Id:" + internalProjectId, internalProjectId);
}
}

@Override
public String getInternalProjectId(String externalProjectId) {
try {
if (Objects.isNull(externalProjectId) || externalProjectId.isEmpty()) {
return(myProjectManager.findProjectByExternalId("_Root").getProjectId());
}
return myProjectManager.findProjectByExternalId(externalProjectId).getProjectId();
} catch (NullPointerException npe) {
throw new NonExistantProjectException("No project found with matching external Id:" + externalProjectId, externalProjectId);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package webhook.teamcity.exception;

import lombok.Getter;

public class NonExistantProjectException extends RuntimeException {
private static final long serialVersionUID = 1L;

@Getter
private final String projectID;

public NonExistantProjectException(String message, String projectId) {
super(message);
this.projectID = projectId;
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import webhook.teamcity.settings.entity.WebHookTemplateEntity;

public interface WebHookPayloadTemplate {

/**
* Sets the TemplateManager so that register() can register this template with that webHookTemplateManager.
*
Expand Down Expand Up @@ -56,6 +56,15 @@ public interface WebHookPayloadTemplate {
*/
String getTemplateId();

/**
* Returns the externalProjectId of the project this template is associated to.
* Most templates will be "_Root" but some may be associated with a specific project
* and hence only available to webhooks from that project or sub-projects;
*
* @return The externalProjectId of the associated TeamCity Project.
*/
String getProjectId();

/**
* Asks if this template can provide a set of templates for this format.
*
Expand Down Expand Up @@ -113,8 +122,8 @@ public interface WebHookPayloadTemplate {

/**
* Get the list of BuildStates for which we have {@link WebHookTemplateContent} content.<br>
* This method expected to be called to resolve templates for builds that are running
* from a VCS is that not branch aware.<br>
* This method is expected to be called to resolve templates for builds that are running
* from a VCS that is not branch aware.<br>
* A "branch aware VCS" is one which TeamCity has knowledge about regarding branches. Eg. Git and Mecurial.
*
* @return A set of templates that don't contain branch information.
Expand All @@ -124,11 +133,11 @@ public interface WebHookPayloadTemplate {

/**
* Get the list of BuildStates for which we have {@link WebHookTemplateContent} content.<br>
* This method expected to be called to resolve templates for builds that are running
* from a VCS is that is branch aware.<br>
* This method id expected to be called to resolve templates for builds that are running
* from a VCS that is branch aware.<br>
* A "branch aware VCS" is one which TeamCity has knowledge about regarding branches. Eg. Git and Mecurial.
*
* @return A set of templates that contain branch information.
* @return A set of templates that do contain branch information.
*
*/
Set<BuildStateEnum> getSupportedBranchBuildStates();
Expand All @@ -141,7 +150,19 @@ public interface WebHookPayloadTemplate {
*/
String getPreferredDateTimeFormat();

/**
* Get the template as a {@link WebHookTemplateEntity} so that it can be persisted in the
* <code>webhook-templates.xml</code> file.
*
* @return The template as an XML friendly entity.
*/
WebHookTemplateEntity getAsEntity();

/**
* Get the template as a {@link WebHookTemplateConfig} object. Typically this will be a
* copy of the object, not a reference.
*
* @return The template as a standard config object.
*/
WebHookTemplateConfig getAsConfig();
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,38 @@
import jetbrains.buildServer.configuration.FileWatcher;
import jetbrains.buildServer.log.Loggers;
import jetbrains.buildServer.serverSide.ServerPaths;
import webhook.teamcity.DeferrableService;
import webhook.teamcity.DeferrableServiceManager;
import webhook.teamcity.settings.WebHookConfigChangeHandler;
import webhook.teamcity.settings.entity.WebHookTemplateJaxHelper;
import webhook.teamcity.settings.entity.WebHookTemplates;

public class WebHookTemplateFileChangeHandler implements ChangeListener, WebHookConfigChangeHandler {
public class WebHookTemplateFileChangeHandler implements ChangeListener, WebHookConfigChangeHandler, DeferrableService {

final WebHookTemplateManager webHookTemplateManager;
final WebHookPayloadManager webHookPayloadManager;
final WebHookTemplateJaxHelper webHookTemplateJaxHelper;
final DeferrableServiceManager deferrableServiceManager;
File configFile;
FileWatcher fw;
final ServerPaths serverPaths;

public WebHookTemplateFileChangeHandler(
ServerPaths serverPaths,
WebHookTemplateManager webHookTemplateManager, WebHookPayloadManager webHookPayloadManager, WebHookTemplateJaxHelper webHookTemplateJaxHelper) {
WebHookTemplateManager webHookTemplateManager, WebHookPayloadManager webHookPayloadManager, WebHookTemplateJaxHelper webHookTemplateJaxHelper, DeferrableServiceManager deferrableServiceManager) {
this.webHookTemplateManager = webHookTemplateManager;
this.webHookPayloadManager = webHookPayloadManager;
this.webHookTemplateJaxHelper = webHookTemplateJaxHelper;
this.serverPaths = serverPaths;
this.deferrableServiceManager = deferrableServiceManager;
Loggers.SERVER.debug("WebHookTemplateFileChangeHandler :: Starting");
}

public void requestDeferredRegistration() {
Loggers.SERVER.info("WebHookTemplateFileChangeHandler :: Registering as a deferrable service");
deferrableServiceManager.registerService(this);
}

public void register(){
Loggers.SERVER.debug("WebHookTemplateFileChangeHandler :: Registering");
this.configFile = new File(this.serverPaths.getConfigDir() + File.separator + "webhook-templates.xml");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,20 @@
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import javax.xml.bind.JAXBException;

import jetbrains.buildServer.serverSide.SProject;
import jetbrains.buildServer.serverSide.auth.AccessDeniedException;
import webhook.Constants;
import webhook.teamcity.Loggers;
import webhook.teamcity.ProbableJaxbJarConflictErrorException;
import webhook.teamcity.ProjectIdResolver;
import webhook.teamcity.payload.template.WebHookTemplateFromXml;
import webhook.teamcity.settings.WebHookSettingsManager;
import webhook.teamcity.settings.config.WebHookTemplateConfig;
import webhook.teamcity.settings.config.builder.WebHookTemplateConfigBuilder;
import webhook.teamcity.settings.entity.WebHookTemplateEntity;
Expand All @@ -29,14 +35,17 @@ public class WebHookTemplateManager {
private List<WebHookPayloadTemplate> orderedTemplateCollection = new ArrayList<>();
private final WebHookPayloadManager webHookPayloadManager;
private final WebHookTemplateJaxHelper webHookTemplateJaxHelper;
private final ProjectIdResolver projectIdResolver;
private String configFilePath;

public WebHookTemplateManager(
WebHookPayloadManager webHookPayloadManager,
WebHookTemplateJaxHelper webHookTemplateJaxHelper)
WebHookTemplateJaxHelper webHookTemplateJaxHelper,
ProjectIdResolver projectIdResolver)
{
this.webHookPayloadManager = webHookPayloadManager;
this.webHookTemplateJaxHelper = webHookTemplateJaxHelper;
this.projectIdResolver = projectIdResolver;
Loggers.SERVER.debug("WebHookTemplateManager :: Starting (" + toString() + ")");
}

Expand All @@ -58,6 +67,10 @@ private void registerTemplateFormatFromXmlEntityUnsyncd(WebHookTemplateEntity pa
Loggers.SERVER.info(this.getClass().getSimpleName() + " :: Registering XML template "
+ payloadTemplate.getId()
+ " with rank of " + payloadTemplate.getRank());
// Set template as belonging to _Root if no associated project id found for this template.
if (Objects.isNull(payloadTemplate.getAssociatedProjectId()) || payloadTemplate.getAssociatedProjectId().isEmpty()) {
payloadTemplate.setAssociatedProjectId(projectIdResolver.getInternalProjectId(Constants.ROOT_PROJECT_ID));
}
payloadTemplate.fixTemplateIds();
xmlConfigTemplates.put(payloadTemplate.getId(),WebHookTemplateFromXml.build(payloadTemplate, webHookPayloadManager));
}
Expand Down Expand Up @@ -142,13 +155,13 @@ private void rebuildOrderedListOfTemplates() {
Collections.sort(this.orderedTemplateCollection, rankComparator);
}

public WebHookPayloadTemplate getTemplate(String formatShortname){
public WebHookPayloadTemplate getTemplate(String templateId){
synchronized (orderedTemplateCollection) {
if (xmlConfigTemplates.containsKey(formatShortname)){
return xmlConfigTemplates.get(formatShortname);
if (xmlConfigTemplates.containsKey(templateId)){
return xmlConfigTemplates.get(templateId);
}
if (springTemplates.containsKey(formatShortname)){
return springTemplates.get(formatShortname);
if (springTemplates.containsKey(templateId)){
return springTemplates.get(templateId);
}
return null;
}
Expand Down Expand Up @@ -176,22 +189,61 @@ public WebHookTemplateConfig getTemplateConfig(String templateId, TemplateState
}
}

public Boolean isRegisteredTemplate(String template){
public boolean isRegisteredTemplate(String template){
return xmlConfigTemplates.containsKey(template) || springTemplates.containsKey(template);
}

public List<WebHookPayloadTemplate> getRegisteredTemplates(){
return orderedTemplateCollection;
}

public List<WebHookTemplateConfig> getRegisteredTemplateConfigs(){
List<WebHookTemplateConfig> orderedEntities = new ArrayList<>();
for (WebHookPayloadTemplate xmlConfig : orderedTemplateCollection){
public List<WebHookPayloadTemplate> getRegisteredPermissionedTemplates(){
List<WebHookPayloadTemplate> orderedTemplates = new ArrayList<>();
for (WebHookPayloadTemplate template : orderedTemplateCollection) {
try {
projectIdResolver.getExternalProjectId(template.getProjectId()); // Throws AccessDeniedException if user is not permissioned on Project
orderedTemplates.add(template);
} catch (AccessDeniedException ex) {
// Don't add the template if user is not permissioned for the project.
}
}
return orderedTemplates;
}

public List<WebHookPayloadTemplate> getRegisteredPermissionedTemplatesForProject(SProject project){
List<WebHookPayloadTemplate> orderedTemplates = new ArrayList<>();
for (WebHookPayloadTemplate template : orderedTemplateCollection) {
if (project.getProjectId().equals(template.getProjectId())) {
try {
projectIdResolver.getExternalProjectId(template.getProjectId()); // Throws AccessDeniedException if user is not permissioned on Project
orderedTemplates.add(template);
} catch (AccessDeniedException ex) {
// Don't add the template if user is not permissioned for the project.
}
}
}
return orderedTemplates;
}

public Map<String,List<WebHookPayloadTemplate>> getRegisteredTemplatesForProjects(List<String> projectExternalIds) {
Map<String, List<WebHookPayloadTemplate>> projectTemplates = new LinkedHashMap<>();
getRegisteredTemplates().forEach(template -> {
if (projectExternalIds.contains(template.getProjectId())) {
projectTemplates.putIfAbsent(template.getProjectId(), new ArrayList<WebHookPayloadTemplate>());
projectTemplates.get(template.getProjectId()).add(template);
}
});
return projectTemplates;
}

public List<WebHookTemplateConfig> getRegisteredPermissionedTemplateConfigs(){
List<WebHookTemplateConfig> orderedTemplateConfigs = new ArrayList<>();
for (WebHookPayloadTemplate xmlConfig : getRegisteredPermissionedTemplates()){
if (xmlConfig.getAsEntity()!=null){
orderedEntities.add(WebHookTemplateConfigBuilder.buildConfig(xmlConfig.getAsEntity()));
orderedTemplateConfigs.add(WebHookTemplateConfigBuilder.buildConfig(xmlConfig.getAsEntity()));
}
}
return orderedEntities;
return orderedTemplateConfigs;
}

public TemplateState getTemplateState(String template, TemplateState templateState){
Expand All @@ -205,10 +257,11 @@ public TemplateState getTemplateState(String template, TemplateState templateSta
return TemplateState.UNKNOWN;
}

public static enum TemplateState {
public enum TemplateState {
PROVIDED ("Template bundled with tcWebhooks"),
USER_DEFINED ("User defined template"),
USER_OVERRIDDEN ("Overridden by user defined template"),
PROJECT_DEFINED ("Template associated with project"),
BEST ("Template in its most specific state"), // Only used for finding. Template will never actually be in this state.
UNKNOWN ("Unknown origin");

Expand Down
Loading

0 comments on commit 909fce5

Please sign in to comment.