Cloudogu Logo

Hallo, wir sind Cloudogu!

Experten für Software-Lifecycle-Management und Prozess­auto­mati­sierung, Förderer von Open-Source-Soft­ware und Entwickler des Cloudogu EcoSystem.

featured image SCM-Manager – Wie starte ich mein erstes Plugin?
08.11.2021 in Technology

SCM-Manager – Wie starte ich mein erstes Plugin?


Eduard Heimbuch
Eduard Heimbuch

SCM-Manager Entwickler


Im vorherigen Beitrag haben wir erklärt, warum und wie Plugins verwendet werden können, um die Grundfunktionalität von SCM-Manager zu erweitern, wie ihre grundlegende Architektur aussieht und wie Sie von Ihnen entwickelte Plugins mit der Community teilen können. In diesem Beitrag zeigen wir Ihnen, wie Sie ein Plugin von Grund auf erstellen können, indem Sie das Backend für ein neues Plugin entwickeln.

Unsere Plugin-Idee

Vor einiger Zeit wurde von SCM-Manager-Benutzern der Wunsch geäußert, eigene Links zum SCM-Manager hinzuzufügen, z.B. für ein Impressum oder das zugehörige Issue-Tracker-System. Diese Links sollten konfigurierbar und von jeder Seite im SCM-Manager erreichbar sein. Aus diesem Grund haben wir uns entschieden, ein Custom-Links-Plugin als Beispiel für die Erstellung von Plugins für den SCM-Manager zu schreiben. Es besteht aus einem Backend-Speicher zur Persistenz der Daten, einer REST-API-Ressource, einer Frontend-Konfigurationsseite im Admin-Bereich und einer Erweiterung zur Anzeige unserer Links in der Fußzeile von SCM-Manager.

Bootstrap für unser Plugin

Zunächst müssen wir unser Plugin einrichten. Wir verwenden create-plugin.scm-manager.org und geben die erforderlichen Informationen ein. Plugin Bootstrap

Nach dem Absenden erhalten wir ein Paket mit der Grundstruktur unseres Plugins, mit dem wir beginnen müssen. Plugin-Struktur

Allgemeine Hinweise

  • SCM-Manager ist unter der MIT-Lizenz lizenziert. Daher muss fast jede Datei einen gültigen Lizenz-Header enthalten. Wenn Sie auf Fehler bezüglich Ihrer Lizenz-Header stoßen, versuchen Sie ./gradlew licenseFormat, um diese automatisch zu beheben.
  • Wir empfehlen dringend, Unit-Tests für jede von Ihnen implementierte Logik zu schreiben. Wenn Sie Probleme beim Erstellen von Unit-Tests haben oder auf der Suche nach Best Practices sind, können Sie hier oder in anderen SCM-Manager Plugins nachsehen.

Unsere IDE einrichten

Für die Plugin-Entwicklung nutzen wir IntelliJ IDEA mit folgenden Einstellungen: IDE Konfiguration

Besonders die Punkte Lizenzen und Formatierung können schnell von den eigentlichen Aufgaben ablenken und sollten deshalb möglichst automatisiert werden.

Datenpersistenz

Zeit zum Coden! Wir erstellen ein Datenobjekt, das unsere einzelne benutzerdefinierte Link-Entität darstellt. In unserem Fall sollten zwei Felder für Name und Url ausreichen. Dieses Objekt muss serialisierbar sein, da wir es speichern wollen. Also fügen wir einige Getter, Setter und zwei Konstruktoren hinzu.

Tipp: Sie können auch die Lombok Annotationen wie @Getter oder @NoArgsConstructor verwenden.

public class CustomLink {
  private String name;
  private String url;

  CustomLink() {
  }

  public CustomLink(String name, String url) {
    this.name = name;
    this.url = url;
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public String getUrl() {
    return url;
  }

  public void setUrl(String url) {
    this.url = url;
  }
}

Da wir wissen, wie unsere Daten aussehen werden, können wir einen Speicher erstellen, um diese benutzerdefinierten Links zu speichern. SCM-Manager bietet verschiedene Speichertypen wie Datenspeicher, Konfigurationsspeicher und Blob-Speicher. Für unseren Anwendungsfall haben wir den ConfigurationEntryStore gewählt. Dieser Speicher kann mehrere benutzerdefinierte Links unabhängig voneinander verwalten. Unser Speicher besteht aus den drei öffentlichen Methoden getAllLinks, addLink und deleteLink.

package com.cloudogu.customlinks;

import com.google.common.annotations.VisibleForTesting;
import sonia.scm.store.ConfigurationEntryStore;
import sonia.scm.store.ConfigurationEntryStoreFactory;

import jakarta.inject.Inject;
import java.util.Collection;

public class CustomLinkConfigStore {

  @VisibleForTesting
  public static final String STORE_NAME = "custom-links";

  private final ConfigurationEntryStoreFactory configurationEntryStoreFactory;

  @Inject
  public CustomLinkConfigStore(ConfigurationEntryStoreFactory configurationEntryStoreFactory) {
    this.configurationEntryStoreFactory = configurationEntryStoreFactory;
  }

  public Collection<CustomLink> getAllLinks() {
    return getStore().getAll().values();
  }

  public void addLink(String name, String url) {
    //TODO Manage links permission
    getStore().put(name, new CustomLink(name, url));
  }

  public void removeLink(String name) {
    //TODO Manage links permission
    getStore().remove(name);
  }

  private ConfigurationEntryStore<CustomLink> getStore() {
    return configurationEntryStoreFactory.withType(CustomLink.class).withName(STORE_NAME).build();
  }
}

Jetzt können wir bereits Daten im internen SCM-Manager Datenspeicher persistieren. Die gespeicherten Daten aus unserem Plugin könnten wie folgt aussehen:

<?xml version="1.0" ?>
<configuration type="config-entry">
  <entry>
    <key>Imprint</key>
    <value>
      <name>Imprint</name>
      <url>https://scm-manager.org/imprint</url>
    </value>
  </entry>
</configuration>

Berechtigungen

Bevor wir uns der REST-API zuwenden, sollten wir einen Blick auf die Berechtigungen werfen. SCM-Manager verwendet Apache Shiro zur Handhabung von Berechtigungen. Jeder angemeldete User ist ein Subjekt, dem Berechtigungen erteilt werden. Für unseren Anwendungsfall möchten wir, dass jeder User die benutzerdefinierten Links sehen kann, auch ohne angemeldet zu sein. Aber nur autorisierte User können die benutzerdefinierten Links ändern.

Daher erstellen wir eine Klasse PermissionCheck. Diese Klasse führt eine neue Berechtigung manageCustomLinks ein und bietet zwei Prüfungen dafür. In Shiro sieht unsere neue Berechtigung wie configuration:manageCustomLinks aus. Wir schützen die Schreibaktionen in unserem Store mit diesen Berechtigungsprüfungen. Sobald ein nicht autorisierter Benutzer versucht, benutzerdefinierte Links zu ändern, wird eine AuthorizationException geworfen.

Wie erhalten die Benutzer diese Berechtigung? Sie muss ihnen gewährt werden. Bevor dies jedoch geschehen kann, müssen wir unsere neue Berechtigung in den SCM-Manager einführen. Dazu erstellen wir eine permissions.xml im Ordner resources/META-INF/scm.

<permissions>
  <permission>
    <value>configuration:manageCustomLinks</value>
  </permission>
</permissions>

Damit unsere neue Berechtigung aus dieser Datei angezogen wird, müssen wir den Server einmal neu starten. Bevor wir dies tun, können wir noch eine Übersetzung für die neue Berechtigung eintragen. Der automatisch generierte Übersetzungsschlüssel kann durch Hinzufügen von Übersetzungen in der Datei plugins.json ersetzt werden.

Custom-Links User-Berechtigungen

REST-API

SCM-Manager verwendet eine Level 3 REST-API und stützt sich auf die HAL-Spezifikation für Hypermedia-Typen. Um diesen Anforderungen gerecht zu werden, stellt SCM-Manager eigene Bibliotheken zur Verfügung, die eine Menge von Standardcode reduzieren. Beginnen wir mit der Erstellung eines DTO (Data Transfer Object) aus unserer benutzerdefinierten Links-Entität. Dazu gibt es Annotationen, die auf der Datenklasse platziert werden, um automatisch eine DTO-Objektklasse daraus zu generieren. Mit „@GenerateDto“ wird, wie Sie sich denken können, unsere DTO-Objektklasse erstellt. Wir müssen die Felder, die in unsere CustomLinkDto.java aufgenommen werden sollen, mit @Include (com.cloudogu.conveyor.Include) markieren.

Die zweite SCM-Manager-spezifische Annotation, die wir verwenden wollen, ist @GenerateLinkBuilder. Diese generiert eine neue Klasse, die Link-Builder für unsere gesamte REST-API enthält (basierend auf der Annotation @Path von jax-rs). Wir haben uns für den spezifischen Klassennamen RestAPI entschieden, um unsere kommenden Beispiele leichter verständlich zu machen. Wir benötigen diese Link-Builder, wenn wir Links für unsere JSON-Antworten hinzufügen, da wir die HAL-Spezifikation einhalten müssen.

@GenerateDto
@GenerateLinkBuilder(className = "RestAPI")
public class CustomLink {
  @Include
  @NotEmpty
  private String name;
  @Include
  @NotEmpty
  private String url;

Als nächstes erstellen wir eine REST-Ressource, die aus drei Methoden besteht, die eine Schnittstelle zu unserem Speicher bilden. Neben dem HTTP Verb und einem Medientyp fügen wir auch die OpenAPI-Annotationen zu jeder Methode hinzu. Diese stellen sicher, dass eine OpenAPI-Spezifikation erstellt wird und dass die REST-API im OpenAPI-Plugin ordnungsgemäß dokumentiert ist. In der Implementierung unserer Methode getAllLinks sammeln wir zunächst unsere Links aus dem Speicher, dann bilden wir sie auf DTOs ab und fügen den delete-Link an jede einzelne Entität an, wenn der User die Berechtigung dazu hat. Schließlich verpacken wir sie in ein HalRepresentation-Objekt als _embedded und fügen weitere Links für die gesamte Collection hinzu.

Eine spezielle Annotation, die im Allgemeinen selten verwendet wird, ist AllowAnonymousAccess. Sie erlaubt den öffentlichen Zugriff auf diese REST-Methode, da sie die Authentifizierungsfilter ignoriert. Für unseren Anwendungsfall müssen wir sie auf den Endpunkt @GET setzen.

@OpenAPIDefinition(tags = {
        @Tag(name = "Custom Links", description = "Custom links plugin related endpoints")
})
@Path(CUSTOM_LINKS_CONFIG_PATH)
public class CustomLinksResource {

    public static final String CUSTOM_LINKS_MEDIA_TYPE = VndMediaType.PREFIX + "custom-links" + VndMediaType.SUFFIX;
    public static final String CUSTOM_LINKS_CONFIG_PATH = "v2/custom-links";

    private final CustomLinkConfigStore configStore;

    @Inject //jakarta.inject.Inject
    CustomLinksResource(CustomLinkConfigStore configStore) {
        this.configStore = configStore;
    }

    @GET
    @Path("")
    @Produces(CUSTOM_LINKS_MEDIA_TYPE)
    @Operation(
            summary = "Get all custom links",
            description = "Returns all custom links.",
            tags = "Custom Links",
            operationId = "custom_links_get_all_links"
    )
    @ApiResponse(
            responseCode = "200",
            description = "success",
            content = @Content(
                    mediaType = CUSTOM_LINKS_MEDIA_TYPE,
                    schema = @Schema(implementation = HalRepresentation.class)
            )
    )
    @ApiResponse(
            responseCode = "500",
            description = "internal server error",
            content = @Content(
                    mediaType = VndMediaType.ERROR_TYPE,
                    schema = @Schema(implementation = ErrorDto.class)
            )
    )
    @AllowAnonymousAccess
    public HalRepresentation getAllCustomLinks(@Context UriInfo uriInfo) {
        RestAPI restAPI = new RestAPI(uriInfo);
        Collection<CustomLink> customLinks = configStore.getAllLinks();
        List<CustomLinkDto> linkDtos = mapCustomLinksToDtos(restAPI, customLinks);
        return new HalRepresentation(createCollectionLinks(restAPI), Embedded.embedded("customLinks", linkDtos));
    }

Hinweis: Die Klasse RestAPI wird über Annotationen generiert. Damit diese Klasse zur Verfügung steht, muss ein ./gradlew build ausgeführt werden.

Nachdem wir unsere REST-Ressource implementiert haben, ist unser Backend bereit und wir können es bereits mit curl testen, z.B. curl http://localhost:8081/scm/api/v2/custom-links. Aber wie genau weiß das SCM-Manager-Frontend von unseren neuen REST-Endpunkten und wie sie zu verwenden sind? Wir wollen keine Links oder URLs im Code des Frontends fest codieren.

Enricher

Die Antwort lautet Enricher. In unserem Fall benötigen wir nur einen Enricher, aber es gibt mehrere Objekte, die angereichert werden können. Beim Zugriff auf SCM-Manager holt sich das Frontend immer den Index (http://localhost:8081/scm/api/v2). Je nachdem, woraus der Index besteht, weiß das Frontend, welche Seiten, Navigationspunkte und Aktionen gerendert werden müssen. Das bedeutet, dass alle Berechtigungsprüfungen im Frontend darauf basieren, welche Links vorhanden sind.

Beispiel: Unser User darf keine Repositories anlegen. Er meldet sich in SCM-Manager an und der Index wird geholt. Im Index finden wir den Link zum Abrufen der Repositories. Der Link „Repositories“ ruft also alle Repositories ab, die unser User sehen kann. In der JSON-Antwort der Repository-Collection finden wir die Repositories als _embedded und auch wieder einige _links. Wir haben den self-Link und einige Paginierungs-Links, aber keinen create-Link. Das bedeutet, dass das Frontend den Create Repository-Button nicht darstellen darf.

SCM-Manager Index

Der IndexDtoGenerator erstellt den Index für jeden User auf der Grundlage der jeweiligen Berechtigungen. Wir reichern diesen Index mit unseren „benutzerdefinierten Links“ an, indem wir einen IndexLinkEnricher erstellen. In unserem Anwendungsfall fügen wir dem Index zwei verschiedene Links hinzu. customLinks wird verwendet, um die benutzerdefinierten Links abzurufen, die für alle User verfügbar sein müssen. customLinksConfig wird verwendet, um zu prüfen, ob unser angemeldeter Benutzer die benutzerdefinierten Links auf der Administrationsseite verwalten darf.

@Extension // sonia.scm.plugin.Extension
@Enrich(Index.class) // sonia.scm.api.v2.resources.Enrich
public class IndexLinkEnricher implements HalEnricher {

  private final Provider<ScmPathInfoStore> scmPathInfoStore;

  @Inject
  public IndexLinkEnricher(Provider<ScmPathInfoStore> scmPathInfoStore) {
    this.scmPathInfoStore = scmPathInfoStore;
  }

  @Override
  public void enrich(HalEnricherContext context, HalAppender appender) {
    String link = new RestAPI(scmPathInfoStore.get().get().getApiRestUri()).customLinks().getAllCustomLinks().asString();
    appender.appendLink("customLinks", link);

    if (PermissionCheck.mayManageCustomLinks()) {
      appender.appendLink("customLinksConfig", link);
    }
  }
}

Fazit

Das war’s. Unser Custom-Links-Plugin Backend ist fertig. Mit einem HTTP Request über curl sehen wir, dass unsere Daten wie erwartet zugeliefert werden. Wir können mit der Frontend-Integration beginnen. Wenn Sie es nicht abwarten können, können Sie bereits einen Blick darauf werfen, wie wir das Frontend aufbauen. Die detaillierten Schritt-für-Schritt-Erklärungen folgen im nächsten und letzten Blog-Beitrag dieser Serie.

▶ curl http://localhost:8081/scm/api/v2/custom-links | jq
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   165  100   165    0     0  23571      0 --:--:-- --:--:-- --:--:-- 23571
{
  "_links": {
    "self": {
      "href": "http://localhost:8081/scm/api/v2/custom-links"
    }
  },
  "_embedded": {
    "customLinks": [
      {
        "name": "Imprint",
        "url": "https://scm-manager.org/imprint"
      }
    ]
  }
}

SCM-Manager

Der einfachste Weg Ihre Git-, Mercurial- and Subversion-Repositories zu teilen und zu verwalten.

Jetzt kennenlernen
SCM-Manager Logo

Aktuelle Diskussion

Kommentare zu diesem Thema auf der Cloudogu Platform