diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..0609164d73 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,3 @@ +# ignore everything except scm-server.tar.gz +** +!scm-server/target/*.tar.gz diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties index 9dda3b659b..8b4bf8dee1 100644 --- a/.mvn/wrapper/maven-wrapper.properties +++ b/.mvn/wrapper/maven-wrapper.properties @@ -1 +1,2 @@ +# Keep this version number in sync with Jenkinsfile distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.2/apache-maven-3.5.2-bin.zip diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..781a3e890b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +FROM openjdk:8u171-alpine3.8 + +ENV SCM_HOME=/var/lib/scm + +RUN set -x \ + && apk add --no-cache mercurial bash \ + && addgroup -S -g 1000 scm \ + && adduser -S -s /bin/false -G scm -h /opt/scm-server -D -H -u 1000 scm \ + && mkdir ${SCM_HOME} \ + && chown scm:scm ${SCM_HOME} + +ADD scm-server/target/scm-server-app.tar.gz /opt +RUN chown -R scm:scm /opt/scm-server + +WORKDIR /opt/scm-server +VOLUME [ "${SCM_HOME}", "/opt/scm-server/var/log" ] +EXPOSE 8080 +USER scm + +ENTRYPOINT [ "/opt/scm-server/bin/scm-server" ] diff --git a/Jenkinsfile b/Jenkinsfile index cd02d15487..57cc3b901f 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -4,7 +4,7 @@ @Library('github.com/cloudogu/ces-build-lib@59d3e94') import com.cloudogu.ces.cesbuildlib.* -node() { // No specific label +node('docker') { // Change this as when we go back to default - necessary for proper SonarQube analysis mainBranch = "2.0.0-m3" @@ -12,15 +12,14 @@ node() { // No specific label properties([ // Keep only the last 10 build to preserve space buildDiscarder(logRotator(numToKeepStr: '10')), + disableConcurrentBuilds() ]) - catchError { + timeout(activity: true, time: 20, unit: 'MINUTES') { - Maven mvn = setupMavenBuild() - // Maven build specified it must be 1.8.0-101 or newer - def javaHome = tool 'JDK-1.8.0-101+' + catchError { - withEnv(["JAVA_HOME=${javaHome}", "PATH=${env.JAVA_HOME}/bin:${env.PATH}"]) { + Maven mvn = setupMavenBuild() stage('Checkout') { checkout scm @@ -46,24 +45,45 @@ node() { // No specific label currentBuild.result = 'UNSTABLE' } } + + def commitHash = getCommitHash() + def dockerImageTag = "2.0.0-dev-${commitHash.substring(0,7)}-${BUILD_NUMBER}" + + if (isMainBranch()) { + stage('Docker') { + def image = docker.build('cloudogu/scm-manager') + docker.withRegistry('', 'hub.docker.com-cesmarvin') { + image.push(dockerImageTag) + image.push('latest') + } + } + + stage('Deployment') { + build job: 'scm-manager/next-scm.cloudogu.com', propagate: false, wait: false, parameters: [ + string(name: 'changeset', value: commitHash), + string(name: 'imageTag', value: dockerImageTag) + ] + } + } } + + // Archive Unit and integration test results, if any + junit allowEmptyResults: true, testResults: '**/target/failsafe-reports/TEST-*.xml,**/target/surefire-reports/TEST-*.xml,**/target/jest-reports/TEST-*.xml' + + // Find maven warnings and visualize in job + warnings consoleParsers: [[parserName: 'Maven']], canRunOnFailed: true + + mailIfStatusChanged(commitAuthorEmail) } - - // Archive Unit and integration test results, if any - junit allowEmptyResults: true, testResults: '**/target/failsafe-reports/TEST-*.xml,**/target/surefire-reports/TEST-*.xml,**/target/jest-reports/TEST-*.xml' - - // Find maven warnings and visualize in job - warnings consoleParsers: [[parserName: 'Maven']], canRunOnFailed: true - - mailIfStatusChanged(commitAuthorEmail) } String mainBranch Maven setupMavenBuild() { - Maven mvn = new MavenWrapper(this) + // Keep this version number in sync with .mvn/maven-wrapper.properties + Maven mvn = new MavenInDocker(this, "3.5.2-jdk-8") - if (mainBranch.equals(env.BRANCH_NAME)) { + if (isMainBranch()) { // Release starts javadoc, which takes very long, so do only for certain branches mvn.additionalArgs += ' -DperformRelease' // JDK8 is more strict, we should fix this before the next release. Right now, this is just not the focus, yet. @@ -90,7 +110,7 @@ void analyzeWith(Maven mvn) { "-Dsonar.pullrequest.bitbucketcloud.repository=scm-manager " } else { mvnArgs += " -Dsonar.branch.name=${env.BRANCH_NAME} " - if (!mainBranch.equals(env.BRANCH_NAME)) { + if (!isMainBranch()) { // Avoid exception "The main branch must not have a target" on main branch mvnArgs += " -Dsonar.branch.target=${mainBranch} " } @@ -99,6 +119,10 @@ void analyzeWith(Maven mvn) { } } +boolean isMainBranch() { + return mainBranch.equals(env.BRANCH_NAME) +} + boolean waitForQualityGateWebhookToBeCalled() { boolean isQualityGateSucceeded = true timeout(time: 2, unit: 'MINUTES') { // Needed when there is no webhook for example @@ -115,6 +139,10 @@ String getCommitAuthorComplete() { new Sh(this).returnStdOut 'hg log --branch . --limit 1 --template "{author}"' } +String getCommitHash() { + new Sh(this).returnStdOut 'hg log --branch . --limit 1 --template "{node}"' +} + String getCommitAuthorEmail() { def matcher = getCommitAuthorComplete() =~ "<(.*?)>" matcher ? matcher[0][1] : "" diff --git a/deployments/helm/.helmignore b/deployments/helm/.helmignore new file mode 100644 index 0000000000..f0c1319444 --- /dev/null +++ b/deployments/helm/.helmignore @@ -0,0 +1,21 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj diff --git a/deployments/helm/Chart.yaml b/deployments/helm/Chart.yaml new file mode 100644 index 0000000000..c5b5fff4cc --- /dev/null +++ b/deployments/helm/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +appVersion: "1.0" +description: A Helm chart for SCM-Manager +name: scm-manager +version: 0.1.0 diff --git a/deployments/helm/templates/NOTES.txt b/deployments/helm/templates/NOTES.txt new file mode 100644 index 0000000000..a58c8f124a --- /dev/null +++ b/deployments/helm/templates/NOTES.txt @@ -0,0 +1,19 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range .Values.ingress.hosts }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ . }}{{ $.Values.ingress.path }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "scm-manager.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get svc -w {{ include "scm-manager.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "scm-manager.fullname" . }} -o jsonpath='{.status.loadBalancer.ingress[0].ip}') + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app={{ include "scm-manager.name" . }},release={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl port-forward $POD_NAME 8080:80 +{{- end }} diff --git a/deployments/helm/templates/_helpers.tpl b/deployments/helm/templates/_helpers.tpl new file mode 100644 index 0000000000..23d4e1b03e --- /dev/null +++ b/deployments/helm/templates/_helpers.tpl @@ -0,0 +1,32 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "scm-manager.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "scm-manager.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "scm-manager.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} diff --git a/deployments/helm/templates/configmap.yaml b/deployments/helm/templates/configmap.yaml new file mode 100644 index 0000000000..dd52b6fa8c --- /dev/null +++ b/deployments/helm/templates/configmap.yaml @@ -0,0 +1,160 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "scm-manager.fullname" . }} + labels: + app: {{ include "scm-manager.name" . }} + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + release: "{{ .Release.Name }}" + heritage: "{{ .Release.Service }}" +data: + server-config.xml: | + + + + + + + 16384 + 16384 + + {{- if .Values.ingress.enabled -}} + + + + + {{- end }} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + /scm + + /var/webapp/scm-webapp.war + + + org.eclipse.jetty.servlet.Default.dirAllowed + false + + + /work/scm + + + + + / + + + + + + /var/webapp/docroot + + + + + + /work/docroot + + + + + + + + + + + + + + + + + + + + + logging.xml: | + + + + <-- + in a container environment we only need stdout + --> + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/deployments/helm/templates/deployment.yaml b/deployments/helm/templates/deployment.yaml new file mode 100644 index 0000000000..928daa5f06 --- /dev/null +++ b/deployments/helm/templates/deployment.yaml @@ -0,0 +1,77 @@ +apiVersion: apps/v1beta2 +kind: Deployment +metadata: + name: {{ include "scm-manager.fullname" . }} + labels: + app: {{ include "scm-manager.name" . }} + chart: {{ include "scm-manager.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + replicas: 1 # could not be scaled + strategy: + type: Recreate + selector: + matchLabels: + app: {{ include "scm-manager.name" . }} + release: {{ .Release.Name }} + template: + metadata: + labels: + app: {{ include "scm-manager.name" . }} + release: {{ .Release.Name }} + spec: + initContainers: + - name: volume-permissions + image: alpine:3.8 + imagePullPolicy: IfNotPresent + command: ['sh', '-c', 'chown 1000:1000 /data'] + volumeMounts: + - name: data + mountPath: /data + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: 8080 + protocol: TCP + livenessProbe: + httpGet: + path: /scm + port: http + readinessProbe: + httpGet: + path: /scm + port: http + resources: +{{ toYaml .Values.resources | indent 12 }} + volumeMounts: + - name: data + mountPath: /var/lib/scm + - name: config + mountPath: /opt/scm-server/conf + volumes: + - name: data + {{- if .Values.persistence.enabled }} + persistentVolumeClaim: + claimName: {{ include "scm-manager.fullname" . }} + {{- else }} + emptyDir: {} + {{- end }} + - name: config + configMap: + name: {{ include "scm-manager.fullname" . }} + {{- with .Values.nodeSelector }} + nodeSelector: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: +{{ toYaml . | indent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: +{{ toYaml . | indent 8 }} + {{- end }} diff --git a/deployments/helm/templates/ingress.yaml b/deployments/helm/templates/ingress.yaml new file mode 100644 index 0000000000..66912c9d96 --- /dev/null +++ b/deployments/helm/templates/ingress.yaml @@ -0,0 +1,38 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "scm-manager.fullname" . -}} +{{- $ingressPath := .Values.ingress.path -}} +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + app: {{ include "scm-manager.name" . }} + chart: {{ include "scm-manager.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +{{- with .Values.ingress.annotations }} + annotations: +{{ toYaml . | indent 4 }} +{{- end }} +spec: +{{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} +{{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ . | quote }} + http: + paths: + - path: {{ $ingressPath }} + backend: + serviceName: {{ $fullName }} + servicePort: http + {{- end }} +{{- end }} diff --git a/deployments/helm/templates/pvc.yaml b/deployments/helm/templates/pvc.yaml new file mode 100644 index 0000000000..0e7d0f6db4 --- /dev/null +++ b/deployments/helm/templates/pvc.yaml @@ -0,0 +1,24 @@ +{{- if .Values.persistence.enabled -}} +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: {{ include "scm-manager.fullname" . }} + labels: + app: {{ include "scm-manager.name" . }} + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + release: "{{ .Release.Name }}" + heritage: "{{ .Release.Service }}" +spec: + accessModes: + - {{ .Values.persistence.accessMode | quote }} + resources: + requests: + storage: {{ .Values.persistence.size | quote }} +{{- if .Values.persistence.storageClass }} +{{- if (eq "-" .Values.persistence.storageClass) }} + storageClassName: "" +{{- else }} + storageClassName: "{{ .Values.persistence.storageClass }}" +{{- end }} +{{- end }} +{{- end -}} diff --git a/deployments/helm/templates/service.yaml b/deployments/helm/templates/service.yaml new file mode 100644 index 0000000000..f3a8207908 --- /dev/null +++ b/deployments/helm/templates/service.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "scm-manager.fullname" . }} + labels: + app: {{ include "scm-manager.name" . }} + chart: {{ include "scm-manager.chart" . }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: 8080 + protocol: TCP + name: http + selector: + app: {{ include "scm-manager.name" . }} + release: {{ .Release.Name }} diff --git a/deployments/helm/values.yaml b/deployments/helm/values.yaml new file mode 100644 index 0000000000..d54088aa8b --- /dev/null +++ b/deployments/helm/values.yaml @@ -0,0 +1,65 @@ +# Default values for scm-manager. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +# replicaCount: 1 + +image: + repository: cloudogu/scm-manager + # TODO change after release, to something more stable + tag: latest + pullPolicy: Always + +nameOverride: "" +fullnameOverride: "" + +service: + type: LoadBalancer + port: 80 + +ingress: + enabled: false + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + path: / + hosts: + - scm-manager.local + tls: [] + # - secretName: scm-manager-tls + # hosts: + # - scm-manager.local + +## Enable persistence using Persistent Volume Claims +## ref: http://kubernetes.io/docs/user-guide/persistent-volumes/ +## +persistence: + enabled: true + ## ghost data Persistent Volume Storage Class + ## If defined, storageClassName: + ## If set to "-", storageClassName: "", which disables dynamic provisioning + ## If undefined (the default) or set to null, no storageClassName spec is + ## set, choosing the default provisioner. (gp2 on AWS, standard on + ## GKE, AWS & OpenStack) + ## + # storageClass: "-" + accessMode: ReadWriteOnce + size: 12Gi + +resources: + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + limits: + cpu: 2000m + memory: 2048Mi + requests: + cpu: 50m + memory: 256Mi + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/pom.xml b/pom.xml index 8ca4318230..df49023265 100644 --- a/pom.xml +++ b/pom.xml @@ -188,15 +188,14 @@ test - - com.github.sdorra.shiro-static-permissions + com.github.sdorra ssp-lib ${ssp.version} - com.github.sdorra.shiro-static-permissions + com.github.sdorra ssp-processor ${ssp.version} true @@ -430,7 +429,9 @@ org.codehaus.mojo animal-sniffer-maven-plugin - 1.17 + + 1.16 org.codehaus.mojo.signature @@ -763,7 +764,7 @@ 9.2.10.v20150310 - 967c8fd521 + 1.1.0 1.4.0 diff --git a/scm-core/pom.xml b/scm-core/pom.xml index 17ca03b115..3c90fec779 100644 --- a/scm-core/pom.xml +++ b/scm-core/pom.xml @@ -32,6 +32,13 @@ scm-annotations 2.0.0-SNAPSHOT + + + org.projectlombok + lombok + provided + + @@ -87,6 +94,12 @@ javax.ws.rs-api + + org.jboss.resteasy + resteasy-jaxrs + test + + @@ -153,14 +166,13 @@ provided - - com.github.sdorra.shiro-static-permissions + com.github.sdorra ssp-lib - com.github.sdorra.shiro-static-permissions + com.github.sdorra ssp-processor true diff --git a/scm-core/src/main/java/sonia/scm/ArgumentIsInvalidException.java b/scm-core/src/main/java/sonia/scm/ArgumentIsInvalidException.java deleted file mode 100644 index 727e9a8160..0000000000 --- a/scm-core/src/main/java/sonia/scm/ArgumentIsInvalidException.java +++ /dev/null @@ -1,85 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - -package sonia.scm; - -/** - * - * @author Sebastian Sdorra - * @since 1.17 - */ -public class ArgumentIsInvalidException extends IllegalStateException -{ - - /** - * Constructs ... - * - */ - public ArgumentIsInvalidException() - { - super(); - } - - /** - * Constructs ... - * - * - * @param s - */ - public ArgumentIsInvalidException(String s) - { - super(s); - } - - /** - * Constructs ... - * - * - * @param cause - */ - public ArgumentIsInvalidException(Throwable cause) - { - super(cause); - } - - /** - * Constructs ... - * - * - * @param message - * @param cause - */ - public ArgumentIsInvalidException(String message, Throwable cause) - { - super(message, cause); - } -} diff --git a/scm-core/src/main/java/sonia/scm/NotFoundException.java b/scm-core/src/main/java/sonia/scm/NotFoundException.java index 0d8c14c61b..8a7ae642bd 100644 --- a/scm-core/src/main/java/sonia/scm/NotFoundException.java +++ b/scm-core/src/main/java/sonia/scm/NotFoundException.java @@ -7,9 +7,4 @@ public class NotFoundException extends Exception { public NotFoundException() { } - - - public NotFoundException(String message) { - super(message); - } } diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/BaseMapper.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/BaseMapper.java index e4cf8ecb5d..d7f299d989 100644 --- a/scm-core/src/main/java/sonia/scm/api/v2/resources/BaseMapper.java +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/BaseMapper.java @@ -3,14 +3,8 @@ package sonia.scm.api.v2.resources; import de.otto.edison.hal.HalRepresentation; import org.mapstruct.Mapping; -import java.time.Instant; - -public abstract class BaseMapper { +public abstract class BaseMapper implements InstantAttributeMapper { @Mapping(target = "attributes", ignore = true) // We do not map HAL attributes public abstract D map(T modelObject); - - protected Instant mapTime(Long epochMilli) { - return epochMilli == null? null: Instant.ofEpochMilli(epochMilli); - } } diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/InstantAttributeMapper.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/InstantAttributeMapper.java new file mode 100644 index 0000000000..468bdfc137 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/InstantAttributeMapper.java @@ -0,0 +1,9 @@ +package sonia.scm.api.v2.resources; + +import java.time.Instant; + +public interface InstantAttributeMapper { + default Instant mapTime(Long epochMilli) { + return epochMilli == null? null: Instant.ofEpochMilli(epochMilli); + } +} diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/LinkBuilder.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/LinkBuilder.java index 6f6831b058..0797134c9f 100644 --- a/scm-core/src/main/java/sonia/scm/api/v2/resources/LinkBuilder.java +++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/LinkBuilder.java @@ -3,7 +3,6 @@ package sonia.scm.api.v2.resources; import com.google.common.collect.ImmutableList; import javax.ws.rs.core.UriBuilder; -import javax.ws.rs.core.UriInfo; import java.net.URI; import java.util.Arrays; @@ -14,7 +13,7 @@ import java.util.Arrays; * builder for each method. * *
- * LinkBuilder builder = new LinkBuilder(uriInfo, MainResource.class, SubResource.class);
+ * LinkBuilder builder = new LinkBuilder(pathInfo, MainResource.class, SubResource.class);
  * Link link = builder
  *     .method("sub")
  *     .parameters("param")
@@ -25,16 +24,16 @@ import java.util.Arrays;
  */
 @SuppressWarnings("WeakerAccess") // Non-public will result in IllegalAccessError for plugins
 public class LinkBuilder {
-  private final UriInfo uriInfo;
+  private final ScmPathInfo pathInfo;
   private final Class[] classes;
   private final ImmutableList calls;
 
-  public LinkBuilder(UriInfo uriInfo, Class... classes) {
-    this(uriInfo, classes, ImmutableList.of());
+  public LinkBuilder(ScmPathInfo pathInfo, Class... classes) {
+    this(pathInfo, classes, ImmutableList.of());
   }
 
-  private LinkBuilder(UriInfo uriInfo, Class[] classes, ImmutableList calls) {
-    this.uriInfo = uriInfo;
+  private LinkBuilder(ScmPathInfo pathInfo, Class[] classes, ImmutableList calls) {
+    this.pathInfo = pathInfo;
     this.classes = classes;
     this.calls = calls;
   }
@@ -51,7 +50,7 @@ public class LinkBuilder {
       throw new IllegalStateException("not enough methods for all classes");
     }
 
-    URI baseUri = uriInfo.getBaseUri();
+    URI baseUri = pathInfo.getApiRestUri();
     URI relativeUri = createRelativeUri();
     return baseUri.resolve(relativeUri);
   }
@@ -61,7 +60,7 @@ public class LinkBuilder {
   }
 
   private LinkBuilder add(String method, String[] parameters) {
-    return new LinkBuilder(uriInfo, classes, appendNewCall(method, parameters));
+    return new LinkBuilder(pathInfo, classes, appendNewCall(method, parameters));
   }
 
   private ImmutableList appendNewCall(String method, String[] parameters) {
diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/ScmPathInfo.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/ScmPathInfo.java
new file mode 100644
index 0000000000..fa975520c1
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/ScmPathInfo.java
@@ -0,0 +1,14 @@
+package sonia.scm.api.v2.resources;
+
+import java.net.URI;
+
+public interface ScmPathInfo {
+
+  String REST_API_PATH = "/api/rest";
+
+  URI getApiRestUri();
+
+  default URI getRootUri() {
+    return getApiRestUri().resolve("../..");
+  }
+}
diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/ScmPathInfoStore.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/ScmPathInfoStore.java
new file mode 100644
index 0000000000..c88bd4a2b5
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/api/v2/resources/ScmPathInfoStore.java
@@ -0,0 +1,18 @@
+package sonia.scm.api.v2.resources;
+
+public class ScmPathInfoStore {
+
+  private ScmPathInfo pathInfo;
+
+  public ScmPathInfo get() {
+    return pathInfo;
+  }
+
+  public void set(ScmPathInfo info) {
+    if (this.pathInfo != null) {
+      throw new IllegalStateException("UriInfo already set");
+    }
+    this.pathInfo = info;
+  }
+
+}
diff --git a/scm-core/src/main/java/sonia/scm/api/v2/resources/UriInfoStore.java b/scm-core/src/main/java/sonia/scm/api/v2/resources/UriInfoStore.java
deleted file mode 100644
index 2f61383cfd..0000000000
--- a/scm-core/src/main/java/sonia/scm/api/v2/resources/UriInfoStore.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package sonia.scm.api.v2.resources;
-
-import javax.ws.rs.core.UriInfo;
-
-public class UriInfoStore {
-
-  private UriInfo uriInfo;
-
-  public UriInfo get() {
-    return uriInfo;
-  }
-
-  public void set(UriInfo uriInfo) {
-    if (this.uriInfo != null) {
-      throw new IllegalStateException("UriInfo already set");
-    }
-    this.uriInfo = uriInfo;
-  }
-}
diff --git a/scm-core/src/main/java/sonia/scm/config/Configuration.java b/scm-core/src/main/java/sonia/scm/config/Configuration.java
index e9bf3528d5..823c50b155 100644
--- a/scm-core/src/main/java/sonia/scm/config/Configuration.java
+++ b/scm-core/src/main/java/sonia/scm/config/Configuration.java
@@ -22,7 +22,7 @@ import com.github.sdorra.ssp.StaticPermissions;
 @StaticPermissions(
   value = "configuration",
   permissions = {"read", "write"},
-  globalPermissions = {}
+  globalPermissions = {"list"}
 )
 public interface Configuration extends PermissionObject {
 }
diff --git a/scm-core/src/main/java/sonia/scm/filter/Filters.java b/scm-core/src/main/java/sonia/scm/filter/Filters.java
index b6a45811bc..b1f5ea47cf 100644
--- a/scm-core/src/main/java/sonia/scm/filter/Filters.java
+++ b/scm-core/src/main/java/sonia/scm/filter/Filters.java
@@ -31,6 +31,8 @@
 
 package sonia.scm.filter;
 
+import static sonia.scm.api.v2.resources.ScmPathInfo.REST_API_PATH;
+
 /**
  * Useful constants for filter implementations.
  *
@@ -44,26 +46,26 @@ public final class Filters
   public static final String PATTERN_ALL = "/*";
 
   /** Field description */
-  public static final String PATTERN_CONFIG = "/api/rest/config*";
+  public static final String PATTERN_CONFIG = REST_API_PATH + "/config*";
 
   /** Field description */
   public static final String PATTERN_DEBUG = "/debug.html";
 
   /** Field description */
-  public static final String PATTERN_GROUPS = "/api/rest/groups*";
+  public static final String PATTERN_GROUPS = REST_API_PATH + "/groups*";
 
   /** Field description */
-  public static final String PATTERN_PLUGINS = "/api/rest/plugins*";
+  public static final String PATTERN_PLUGINS = REST_API_PATH + "/plugins*";
 
   /** Field description */
   public static final String PATTERN_RESOURCE_REGEX =
     "^/(?:resources|api|plugins|index)[\\./].*(?:html|\\.css|\\.js|\\.xml|\\.json|\\.txt)";
 
   /** Field description */
-  public static final String PATTERN_RESTAPI = "/api/rest/*";
+  public static final String PATTERN_RESTAPI = REST_API_PATH + "/*";
 
   /** Field description */
-  public static final String PATTERN_USERS = "/api/rest/users*";
+  public static final String PATTERN_USERS = REST_API_PATH + "/users*";
 
   /** authentication priority */
   public static final int PRIORITY_AUTHENTICATION = 5000;
diff --git a/scm-core/src/main/java/sonia/scm/group/Group.java b/scm-core/src/main/java/sonia/scm/group/Group.java
index 98d9dcc7a3..5e7f596c58 100644
--- a/scm-core/src/main/java/sonia/scm/group/Group.java
+++ b/scm-core/src/main/java/sonia/scm/group/Group.java
@@ -60,7 +60,7 @@ import java.util.List;
  *
  * @author Sebastian Sdorra
  */
-@StaticPermissions("group")
+@StaticPermissions(value = "group", globalPermissions = {"create", "list"})
 @XmlRootElement(name = "groups")
 @XmlAccessorType(XmlAccessType.FIELD)
 public class Group extends BasicPropertiesAware
diff --git a/scm-core/src/main/java/sonia/scm/plugin/WebResourceLoader.java b/scm-core/src/main/java/sonia/scm/plugin/WebResourceLoader.java
index 94b31ac844..454003c922 100644
--- a/scm-core/src/main/java/sonia/scm/plugin/WebResourceLoader.java
+++ b/scm-core/src/main/java/sonia/scm/plugin/WebResourceLoader.java
@@ -33,9 +33,8 @@ package sonia.scm.plugin;
 
 //~--- JDK imports ------------------------------------------------------------
 
-import java.net.URL;
-
 import javax.servlet.ServletContext;
+import java.net.URL;
 
 /**
  * The WebResourceLoader is able to load web resources. The resources are loaded
@@ -53,9 +52,11 @@ public interface WebResourceLoader
    * Returns a {@link URL} for the given path. The method will return null if no
    * resources could be found for the given path.
    *
+   * Note: The path is a web path and uses "/" as path separator
+   *
    * @param path resource path
    *
    * @return url object for the given path or null
    */
-  public URL getResource(String path);
+  URL getResource(String path);
 }
diff --git a/scm-core/src/main/java/sonia/scm/repository/Changeset.java b/scm-core/src/main/java/sonia/scm/repository/Changeset.java
index 18ede493f6..7397fecabe 100644
--- a/scm-core/src/main/java/sonia/scm/repository/Changeset.java
+++ b/scm-core/src/main/java/sonia/scm/repository/Changeset.java
@@ -41,7 +41,6 @@ import sonia.scm.util.ValidationUtil;
 
 import javax.xml.bind.annotation.XmlAccessType;
 import javax.xml.bind.annotation.XmlAccessorType;
-import javax.xml.bind.annotation.XmlElement;
 import javax.xml.bind.annotation.XmlRootElement;
 import java.util.ArrayList;
 import java.util.Date;
@@ -84,12 +83,6 @@ public class Changeset extends BasicPropertiesAware implements ModelObject {
    */
   private String id;
 
-  /**
-   * List of files changed by this changeset
-   */
-  @XmlElement(name = "modifications")
-  private Modifications modifications;
-
   /**
    * parent changeset ids
    */
@@ -137,7 +130,6 @@ public class Changeset extends BasicPropertiesAware implements ModelObject {
            && Objects.equal(parents, other.parents)
            && Objects.equal(tags, other.tags)
            && Objects.equal(branches, other.branches)
-           && Objects.equal(modifications, other.modifications)
            && Objects.equal(properties, other.properties);
     //J+
   }
@@ -152,7 +144,7 @@ public class Changeset extends BasicPropertiesAware implements ModelObject {
   public int hashCode()
   {
     return Objects.hashCode(id, date, author, description, parents, tags,
-                            branches, modifications, properties);
+                            branches,  properties);
   }
 
   /**
@@ -184,11 +176,6 @@ public class Changeset extends BasicPropertiesAware implements ModelObject {
     out.append("branches: ").append(Util.toString(branches)).append("\n");
     out.append("tags: ").append(Util.toString(tags)).append("\n");
 
-    if (modifications != null)
-    {
-      out.append("modifications: \n").append(modifications);
-    }
-
     return out.toString();
   }
 
@@ -285,21 +272,6 @@ public class Changeset extends BasicPropertiesAware implements ModelObject {
   }
 
 
-  /**
-   * Returns the file modifications, which was done with this changeset.
-   *
-   *
-   * @return file modifications
-   */
-  public Modifications getModifications()
-  {
-    if (modifications == null)
-    {
-      modifications = new Modifications();
-    }
-
-    return modifications;
-  }
 
   /**
    * Return the ids of the parent changesets.
@@ -402,17 +374,6 @@ public class Changeset extends BasicPropertiesAware implements ModelObject {
     this.id = id;
   }
 
-  /**
-   * Sets the file modification of the changeset.
-   *
-   *
-   * @param modifications file modifications
-   */
-  public void setModifications(Modifications modifications)
-  {
-    this.modifications = modifications;
-  }
-
   /**
    * Sets the parents of the changeset.
    *
diff --git a/scm-core/src/main/java/sonia/scm/repository/EscapeUtil.java b/scm-core/src/main/java/sonia/scm/repository/EscapeUtil.java
deleted file mode 100644
index a06472256d..0000000000
--- a/scm-core/src/main/java/sonia/scm/repository/EscapeUtil.java
+++ /dev/null
@@ -1,203 +0,0 @@
-/**
- * Copyright (c) 2010, Sebastian Sdorra
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are met:
- *
- * 1. Redistributions of source code must retain the above copyright notice,
- *    this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright notice,
- *    this list of conditions and the following disclaimer in the documentation
- *    and/or other materials provided with the distribution.
- * 3. Neither the name of SCM-Manager; nor the names of its
- *    contributors may be used to endorse or promote products derived from this
- *    software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- * DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
- * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
- * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
- * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
- * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *
- * http://bitbucket.org/sdorra/scm-manager
- *
- */
-
-
-
-package sonia.scm.repository;
-
-//~--- non-JDK imports --------------------------------------------------------
-
-import com.google.common.collect.Lists;
-
-import org.apache.commons.lang.StringEscapeUtils;
-
-import sonia.scm.util.Util;
-
-//~--- JDK imports ------------------------------------------------------------
-
-import java.util.List;
-
-/**
- *
- * @author Sebastian Sdorra
- * @since 1.15
- */
-public final class EscapeUtil
-{
-
-  /**
-   * Constructs ...
-   *
-   */
-  private EscapeUtil() {}
-
-  //~--- methods --------------------------------------------------------------
-
-  /**
-   * Method description
-   *
-   *
-   * @param result
-   */
-  public static void escape(BrowserResult result)
-  {
-    result.setBranch(escape(result.getBranch()));
-    result.setTag(escape(result.getTag()));
-
-    for (FileObject fo : result)
-    {
-      escape(fo);
-    }
-  }
-
-  /**
-   * Method description
-   *
-   *
-   * @param result
-   * @since 1.17
-   */
-  public static void escape(BlameResult result)
-  {
-    for (BlameLine line : result.getBlameLines())
-    {
-      escape(line);
-    }
-  }
-
-  /**
-   * Method description
-   *
-   *
-   * @param line
-   * @since 1.17
-   */
-  public static void escape(BlameLine line)
-  {
-    line.setDescription(escape(line.getDescription()));
-    escape(line.getAuthor());
-  }
-
-  /**
-   * Method description
-   *
-   *
-   * @param fo
-   */
-  public static void escape(FileObject fo)
-  {
-    fo.setDescription(escape(fo.getDescription()));
-    fo.setName(fo.getName());
-    fo.setPath(fo.getPath());
-  }
-
-  /**
-   * Method description
-   *
-   *
-   * @param changeset
-   */
-  public static void escape(Changeset changeset)
-  {
-    changeset.setDescription(escape(changeset.getDescription()));
-    escape(changeset.getAuthor());
-    changeset.setBranches(escapeList(changeset.getBranches()));
-    changeset.setTags(escapeList(changeset.getTags()));
-  }
-
-  /**
-   * Method description
-   *
-   *
-   * @param person
-   * @since 1.17
-   */
-  public static void escape(Person person)
-  {
-    if (person != null)
-    {
-      person.setName(escape(person.getName()));
-      person.setMail(escape(person.getMail()));
-    }
-  }
-
-  /**
-   * Method description
-   *
-   *
-   * @param result
-   */
-  public static void escape(ChangesetPagingResult result)
-  {
-    for (Changeset c : result)
-    {
-      escape(c);
-    }
-  }
-
-  /**
-   * Method description
-   *
-   *
-   * @param value
-   *
-   * @return
-   */
-  public static String escape(String value)
-  {
-    return StringEscapeUtils.escapeHtml(value);
-  }
-
-  /**
-   * Method description
-   *
-   *
-   * @param values
-   *
-   * @return
-   */
-  public static List escapeList(List values)
-  {
-    if (Util.isNotEmpty(values))
-    {
-      List newList = Lists.newArrayList();
-
-      for (String v : values)
-      {
-        newList.add(StringEscapeUtils.escapeHtml(v));
-      }
-
-      values = newList;
-    }
-
-    return values;
-  }
-}
diff --git a/scm-core/src/main/java/sonia/scm/repository/Modifications.java b/scm-core/src/main/java/sonia/scm/repository/Modifications.java
index 4079d8f574..a679b0429f 100644
--- a/scm-core/src/main/java/sonia/scm/repository/Modifications.java
+++ b/scm-core/src/main/java/sonia/scm/repository/Modifications.java
@@ -67,7 +67,8 @@ public class Modifications implements Serializable
    * Constructs ...
    *
    */
-  public Modifications() {}
+  public Modifications() {
+  }
 
   /**
    * Constructs ...
@@ -218,6 +219,10 @@ public class Modifications implements Serializable
     return removed;
   }
 
+  public String getRevision() {
+    return revision;
+  }
+
   //~--- set methods ----------------------------------------------------------
 
   /**
@@ -253,8 +258,14 @@ public class Modifications implements Serializable
     this.removed = removed;
   }
 
+  public void setRevision(String revision) {
+    this.revision = revision;
+  }
+
   //~--- fields ---------------------------------------------------------------
 
+  private String revision;
+
   /** list of added files */
   @XmlElement(name = "added")
   @XmlElementWrapper(name = "added")
diff --git a/scm-core/src/main/java/sonia/scm/repository/ModificationsPreProcessor.java b/scm-core/src/main/java/sonia/scm/repository/ModificationsPreProcessor.java
new file mode 100644
index 0000000000..e5870e91aa
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/repository/ModificationsPreProcessor.java
@@ -0,0 +1,23 @@
+package sonia.scm.repository;
+
+import sonia.scm.plugin.ExtensionPoint;
+
+
+/**
+ * A pre processor for {@link Modifications} objects. A pre processor is able to
+ * modify the object before it is delivered to the user interface.
+ *
+ * @author Mohamed Karray
+ * @since 2.0
+ */
+@ExtensionPoint
+public interface ModificationsPreProcessor extends PreProcessor {
+
+  /**
+   * Process the given modifications.
+   *
+   * @param modifications modifications to process
+   */
+  @Override
+  void process(Modifications modifications);
+}
diff --git a/scm-core/src/main/java/sonia/scm/repository/ModificationsPreProcessorFactory.java b/scm-core/src/main/java/sonia/scm/repository/ModificationsPreProcessorFactory.java
new file mode 100644
index 0000000000..71a6a38efa
--- /dev/null
+++ b/scm-core/src/main/java/sonia/scm/repository/ModificationsPreProcessorFactory.java
@@ -0,0 +1,26 @@
+package sonia.scm.repository;
+
+import sonia.scm.plugin.ExtensionPoint;
+
+
+/**
+ * This factory create a {@link ModificationsPreProcessor}
+ *
+ * @author Mohamed Karray
+ * @since 2.0
+ */
+@ExtensionPoint
+public interface ModificationsPreProcessorFactory extends PreProcessorFactory {
+
+  /**
+   * Create a new {@link ModificationsPreProcessor} for the given repository.
+   *
+   *
+   * @param repository repository
+   *
+   * @return {@link ModificationsPreProcessor} for the given repository
+   */
+  @Override
+  ModificationsPreProcessor createPreProcessor(Repository repository);
+
+}
diff --git a/scm-core/src/main/java/sonia/scm/repository/PreProcessorUtil.java b/scm-core/src/main/java/sonia/scm/repository/PreProcessorUtil.java
index b44da13ad1..2a1d9c0340 100644
--- a/scm-core/src/main/java/sonia/scm/repository/PreProcessorUtil.java
+++ b/scm-core/src/main/java/sonia/scm/repository/PreProcessorUtil.java
@@ -36,17 +36,15 @@ package sonia.scm.repository;
 //~--- non-JDK imports --------------------------------------------------------
 
 import com.google.inject.Inject;
-
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
-
 import sonia.scm.util.Util;
 
-//~--- JDK imports ------------------------------------------------------------
-
 import java.util.Collection;
 import java.util.Set;
 
+//~--- JDK imports ------------------------------------------------------------
+
 /**
  *
  * @author Sebastian Sdorra
@@ -73,14 +71,18 @@ public class PreProcessorUtil
    * @param fileObjectPreProcessorFactorySet
    * @param blameLinePreProcessorSet
    * @param blameLinePreProcessorFactorySet
+   * @param modificationsPreProcessorFactorySet
+   * @param modificationsPreProcessorSet
    */
   @Inject
   public PreProcessorUtil(Set changesetPreProcessorSet,
-    Set changesetPreProcessorFactorySet,
-    Set fileObjectPreProcessorSet,
-    Set fileObjectPreProcessorFactorySet,
-    Set blameLinePreProcessorSet,
-    Set blameLinePreProcessorFactorySet)
+                          Set changesetPreProcessorFactorySet,
+                          Set fileObjectPreProcessorSet,
+                          Set fileObjectPreProcessorFactorySet,
+                          Set blameLinePreProcessorSet,
+                          Set blameLinePreProcessorFactorySet,
+                          Set modificationsPreProcessorFactorySet,
+                          Set modificationsPreProcessorSet)
   {
     this.changesetPreProcessorSet = changesetPreProcessorSet;
     this.changesetPreProcessorFactorySet = changesetPreProcessorFactorySet;
@@ -88,6 +90,8 @@ public class PreProcessorUtil
     this.fileObjectPreProcessorFactorySet = fileObjectPreProcessorFactorySet;
     this.blameLinePreProcessorSet = blameLinePreProcessorSet;
     this.blameLinePreProcessorFactorySet = blameLinePreProcessorFactorySet;
+    this.modificationsPreProcessorFactorySet = modificationsPreProcessorFactorySet;
+    this.modificationsPreProcessorSet = modificationsPreProcessorSet;
   }
 
   //~--- methods --------------------------------------------------------------
@@ -107,14 +111,7 @@ public class PreProcessorUtil
         blameLine.getLineNumber(), repository.getName());
     }
 
-    EscapeUtil.escape(blameLine);
-
-    PreProcessorHandler handler =
-      new PreProcessorHandler(blameLinePreProcessorFactorySet,
-        blameLinePreProcessorSet, repository);
-
-    handler.callPreProcessors(blameLine);
-    handler.callPreProcessorFactories(blameLine);
+    handlePreProcess(repository,blameLine,blameLinePreProcessorFactorySet, blameLinePreProcessorSet);
   }
 
   /**
@@ -125,22 +122,6 @@ public class PreProcessorUtil
    * @param blameResult
    */
   public void prepareForReturn(Repository repository, BlameResult blameResult)
-  {
-    prepareForReturn(repository, blameResult, true);
-  }
-
-  /**
-   * Method description
-   *
-   *
-   * @param repository
-   * @param blameResult
-   * @param escape
-   *
-   * @since 1.35
-   */
-  public void prepareForReturn(Repository repository, BlameResult blameResult,
-    boolean escape)
   {
     if (logger.isTraceEnabled())
     {
@@ -148,17 +129,7 @@ public class PreProcessorUtil
         repository.getName());
     }
 
-    if (escape)
-    {
-      EscapeUtil.escape(blameResult);
-    }
-
-    PreProcessorHandler handler =
-      new PreProcessorHandler(blameLinePreProcessorFactorySet,
-        blameLinePreProcessorSet, repository);
-
-    handler.callPreProcessors(blameResult.getBlameLines());
-    handler.callPreProcessorFactories(blameResult.getBlameLines());
+    handlePreProcessForIterable(repository, blameResult.getBlameLines(),blameLinePreProcessorFactorySet, blameLinePreProcessorSet);
   }
 
   /**
@@ -170,39 +141,13 @@ public class PreProcessorUtil
    */
   public void prepareForReturn(Repository repository, Changeset changeset)
   {
-    prepareForReturn(repository, changeset, true);
+    logger.trace("prepare changeset {} of repository {} for return", changeset.getId(), repository.getName());
+    handlePreProcess(repository, changeset, changesetPreProcessorFactorySet, changesetPreProcessorSet);
   }
 
-  /**
-   * Method description
-   *
-   *
-   * @param repository
-   * @param changeset
-   * @param escape
-   *
-   * @since 1.35
-   */
-  public void prepareForReturn(Repository repository, Changeset changeset,
-    boolean escape)
-  {
-    if (logger.isTraceEnabled())
-    {
-      logger.trace("prepare changeset {} of repository {} for return",
-        changeset.getId(), repository.getName());
-    }
-
-    if (escape)
-    {
-      EscapeUtil.escape(changeset);
-    }
-
-    PreProcessorHandler handler =
-      new PreProcessorHandler(changesetPreProcessorFactorySet,
-        changesetPreProcessorSet, repository);
-
-    handler.callPreProcessors(changeset);
-    handler.callPreProcessorFactories(changeset);
+  public void prepareForReturn(Repository repository, Modifications modifications) {
+    logger.trace("prepare modifications {} of repository {} for return", modifications, repository.getName());
+    handlePreProcess(repository, modifications, modificationsPreProcessorFactorySet, modificationsPreProcessorSet);
   }
 
   /**
@@ -213,22 +158,6 @@ public class PreProcessorUtil
    * @param result
    */
   public void prepareForReturn(Repository repository, BrowserResult result)
-  {
-    prepareForReturn(repository, result, true);
-  }
-
-  /**
-   * Method description
-   *
-   *
-   * @param repository
-   * @param result
-   * @param escape
-   *
-   * @since 1.35
-   */
-  public void prepareForReturn(Repository repository, BrowserResult result,
-    boolean escape)
   {
     if (logger.isTraceEnabled())
     {
@@ -236,17 +165,7 @@ public class PreProcessorUtil
         repository.getName());
     }
 
-    if (escape)
-    {
-      EscapeUtil.escape(result);
-    }
-
-    PreProcessorHandler handler =
-      new PreProcessorHandler(fileObjectPreProcessorFactorySet,
-        fileObjectPreProcessorSet, repository);
-
-    handler.callPreProcessors(result);
-    handler.callPreProcessorFactories(result);
+    handlePreProcessForIterable(repository, result,fileObjectPreProcessorFactorySet, fileObjectPreProcessorSet);
   }
 
   /**
@@ -255,12 +174,8 @@ public class PreProcessorUtil
    *
    * @param repository
    * @param result
-   * @param escape
-   *
-   * @since 1.35
    */
-  public void prepareForReturn(Repository repository,
-    ChangesetPagingResult result, boolean escape)
+  public void prepareForReturn(Repository repository, ChangesetPagingResult result)
   {
     if (logger.isTraceEnabled())
     {
@@ -268,30 +183,23 @@ public class PreProcessorUtil
         repository.getName());
     }
 
-    if (escape)
-    {
-      EscapeUtil.escape(result);
-    }
-
-    PreProcessorHandler handler =
-      new PreProcessorHandler(changesetPreProcessorFactorySet,
-        changesetPreProcessorSet, repository);
-
-    handler.callPreProcessors(result);
-    handler.callPreProcessorFactories(result);
+    handlePreProcessForIterable(repository,result,changesetPreProcessorFactorySet, changesetPreProcessorSet);
   }
 
-  /**
-   * Method description
-   *
-   *
-   * @param repository
-   * @param result
-   */
-  public void prepareForReturn(Repository repository,
-    ChangesetPagingResult result)
-  {
-    prepareForReturn(repository, result, true);
+  private , P extends PreProcessor> void handlePreProcess(Repository repository, T processedObject,
+                                                                                                 Collection factories,
+                                                                                                 Collection

preProcessors) { + PreProcessorHandler handler = new PreProcessorHandler(factories, preProcessors, repository); + handler.callPreProcessors(processedObject); + handler.callPreProcessorFactories(processedObject); + } + + private , F extends PreProcessorFactory, P extends PreProcessor> void handlePreProcessForIterable(Repository repository, I processedObjects, + Collection factories, + Collection

preProcessors) { + PreProcessorHandler handler = new PreProcessorHandler(factories, preProcessors, repository); + handler.callPreProcessors(processedObjects); + handler.callPreProcessorFactories(processedObjects); } //~--- inner classes -------------------------------------------------------- @@ -454,6 +362,10 @@ public class PreProcessorUtil /** Field description */ private final Collection changesetPreProcessorSet; + private final Collection modificationsPreProcessorFactorySet; + + private final Collection modificationsPreProcessorSet; + /** Field description */ private final Collection fileObjectPreProcessorFactorySet; diff --git a/scm-core/src/main/java/sonia/scm/repository/Repository.java b/scm-core/src/main/java/sonia/scm/repository/Repository.java index 0d8c6a6af8..cad36f2d88 100644 --- a/scm-core/src/main/java/sonia/scm/repository/Repository.java +++ b/scm-core/src/main/java/sonia/scm/repository/Repository.java @@ -40,7 +40,6 @@ import com.google.common.base.Objects; import com.google.common.collect.Lists; import sonia.scm.BasicPropertiesAware; import sonia.scm.ModelObject; -import sonia.scm.util.HttpUtil; import sonia.scm.util.Util; import sonia.scm.util.ValidationUtil; @@ -349,17 +348,6 @@ public class Repository extends BasicPropertiesAware implements ModelObject, Per // do not copy health check results } - /** - * Creates the url of the repository. - * - * @param baseUrl base url of the server including the context path - * @return url of the repository - * @since 1.17 - */ - public String createUrl(String baseUrl) { - return HttpUtil.concatenate(baseUrl, type, namespace, name); - } - /** * Returns true if the {@link Repository} is the same as the obj argument. * diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java index 2c4d958d8d..1e2fdccf42 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryManager.java @@ -38,7 +38,6 @@ package sonia.scm.repository; import sonia.scm.AlreadyExistsException; import sonia.scm.TypeManager; -import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.util.Collection; @@ -99,29 +98,6 @@ public interface RepositoryManager */ public Collection getConfiguredTypes(); - /** - * Returns the {@link Repository} associated to the request uri. - * - * - * @param request the current http request - * - * @return associated to the request uri - * @since 1.9 - */ - public Repository getFromRequest(HttpServletRequest request); - - /** - * Returns the {@link Repository} associated to the request uri. - * - * - * - * @param uri request uri without context path - * - * @return associated to the request uri - * @since 1.9 - */ - public Repository getFromUri(String uri); - /** * Returns a {@link RepositoryHandler} by the given type (hg, git, svn ...). * diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryManagerDecorator.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryManagerDecorator.java index aa4117af34..87df960da7 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryManagerDecorator.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryManagerDecorator.java @@ -39,7 +39,6 @@ import sonia.scm.AlreadyExistsException; import sonia.scm.ManagerDecorator; import sonia.scm.Type; -import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.util.Collection; @@ -120,34 +119,6 @@ public class RepositoryManagerDecorator return decorated; } - /** - * {@inheritDoc} - * - * - * @param request - * - * @return - */ - @Override - public Repository getFromRequest(HttpServletRequest request) - { - return decorated.getFromRequest(request); - } - - /** - * {@inheritDoc} - * - * - * @param uri - * - * @return - */ - @Override - public Repository getFromUri(String uri) - { - return decorated.getFromUri(uri); - } - /** * {@inheritDoc} * diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryNotFoundException.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryNotFoundException.java index 070221117a..9dd866daa4 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryNotFoundException.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryNotFoundException.java @@ -44,8 +44,8 @@ import sonia.scm.NotFoundException; public class RepositoryNotFoundException extends NotFoundException { - /** Field description */ private static final long serialVersionUID = -6583078808900520166L; + private static final String TYPE_REPOSITORY = "repository"; //~--- constructors --------------------------------------------------------- @@ -55,10 +55,14 @@ public class RepositoryNotFoundException extends NotFoundException * */ public RepositoryNotFoundException(Repository repository) { - super("repository", repository.getName() + "/" + repository.getNamespace()); + super(TYPE_REPOSITORY, repository.getName() + "/" + repository.getNamespace()); } public RepositoryNotFoundException(String repositoryId) { - super("repository", repositoryId); + super(TYPE_REPOSITORY, repositoryId); + } + + public RepositoryNotFoundException(NamespaceAndName namespaceAndName) { + super(TYPE_REPOSITORY, namespaceAndName.toString()); } } diff --git a/scm-core/src/main/java/sonia/scm/repository/RepositoryProvider.java b/scm-core/src/main/java/sonia/scm/repository/RepositoryProvider.java index 1a5300ad21..cea8574d14 100644 --- a/scm-core/src/main/java/sonia/scm/repository/RepositoryProvider.java +++ b/scm-core/src/main/java/sonia/scm/repository/RepositoryProvider.java @@ -6,13 +6,13 @@ * modification, are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. + * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. + * contributors may be used to endorse or promote products derived from this + * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE @@ -26,35 +26,21 @@ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * http://bitbucket.org/sdorra/scm-manager - * */ - package sonia.scm.repository; //~--- non-JDK imports -------------------------------------------------------- import com.google.inject.throwingproviders.CheckedProvider; -import sonia.scm.security.ScmSecurityException; - /** * * @author Sebastian Sdorra * @since 1.10 */ -public interface RepositoryProvider extends CheckedProvider -{ - - /** - * Method description - * - * - * @return - * - * @throws ScmSecurityException - */ +public interface RepositoryProvider extends CheckedProvider { @Override - public Repository get() throws ScmSecurityException; + Repository get(); } diff --git a/scm-core/src/main/java/sonia/scm/repository/api/BlameCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/BlameCommandBuilder.java index 5a34345dce..f55abd4598 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/BlameCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/BlameCommandBuilder.java @@ -185,7 +185,7 @@ public final class BlameCommandBuilder if (!disablePreProcessors && (result != null)) { - preProcessorUtil.prepareForReturn(repository, result, !disableEscaping); + preProcessorUtil.prepareForReturn(repository, result); } return result; @@ -210,24 +210,6 @@ public final class BlameCommandBuilder return this; } - /** - * Disable html escaping for the returned blame lines. By default all - * blame lines are html escaped. - * - * - * @param disableEscaping true to disable the html escaping - * - * @return {@code this} - * - * @since 1.35 - */ - public BlameCommandBuilder setDisableEscaping(boolean disableEscaping) - { - this.disableEscaping = disableEscaping; - - return this; - } - /** * Disable the execution of pre processors. * @@ -362,9 +344,6 @@ public final class BlameCommandBuilder /** the cache */ private final Cache cache; - /** disable escaping */ - private boolean disableEscaping = false; - /** disable change */ private boolean disableCache = false; diff --git a/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java index eeae45b893..fe39aa0a05 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/BrowseCommandBuilder.java @@ -179,7 +179,7 @@ public final class BrowseCommandBuilder if (!disablePreProcessors && (result != null)) { - preProcessorUtil.prepareForReturn(repository, result, !disableEscaping); + preProcessorUtil.prepareForReturn(repository, result); List fileObjects = result.getFiles(); @@ -212,24 +212,6 @@ public final class BrowseCommandBuilder return this; } - /** - * Disable html escaping for the returned file objects. By default all - * file objects are html escaped. - * - * - * @param disableEscaping true to disable the html escaping - * - * @return {@code this} - * - * @since 1.35 - */ - public BrowseCommandBuilder setDisableEscaping(boolean disableEscaping) - { - this.disableEscaping = disableEscaping; - - return this; - } - /** * Disabling the last commit means that every call to * {@link FileObject#getDescription()} and @@ -433,9 +415,6 @@ public final class BrowseCommandBuilder /** cache */ private final Cache cache; - /** disable escaping */ - private boolean disableEscaping = false; - /** disables the cache */ private boolean disableCache = false; diff --git a/scm-core/src/main/java/sonia/scm/repository/api/Command.java b/scm-core/src/main/java/sonia/scm/repository/api/Command.java index ccb0d8c2c0..2f844cfbfb 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/Command.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/Command.java @@ -61,5 +61,11 @@ public enum Command /** * @since 1.43 */ - BUNDLE, UNBUNDLE; + BUNDLE, UNBUNDLE, + + /** + * @since 2.0 + */ + MODIFICATIONS + } diff --git a/scm-core/src/main/java/sonia/scm/repository/api/HookChangesetBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/HookChangesetBuilder.java index c06e227d5f..fe43fee038 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/HookChangesetBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/HookChangesetBuilder.java @@ -132,8 +132,7 @@ public final class HookChangesetBuilder try { copy = DeepCopy.copy(c); - preProcessorUtil.prepareForReturn(repository, copy, - !disableEscaping); + preProcessorUtil.prepareForReturn(repository, copy); } catch (IOException ex) { @@ -156,24 +155,6 @@ public final class HookChangesetBuilder //~--- set methods ---------------------------------------------------------- - /** - * Disable html escaping for the returned changesets. By default all - * changesets are html escaped. - * - * - * @param disableEscaping true to disable the html escaping - * - * @return {@code this} - * - * @since 1.35 - */ - public HookChangesetBuilder setDisableEscaping(boolean disableEscaping) - { - this.disableEscaping = disableEscaping; - - return this; - } - /** * Disable the execution of pre processors. * @@ -192,9 +173,6 @@ public final class HookChangesetBuilder //~--- fields --------------------------------------------------------------- - /** disable escaping */ - private boolean disableEscaping = false; - /** disable pre processors marker */ private boolean disablePreProcessors = false; diff --git a/scm-core/src/main/java/sonia/scm/repository/api/LogCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/LogCommandBuilder.java index 1450cf687a..9c782a781b 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/LogCommandBuilder.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/LogCommandBuilder.java @@ -264,7 +264,7 @@ public final class LogCommandBuilder if (!disablePreProcessors && (cpr != null)) { - preProcessorUtil.prepareForReturn(repository, cpr, !disableEscaping); + preProcessorUtil.prepareForReturn(repository, cpr); } return cpr; @@ -306,24 +306,6 @@ public final class LogCommandBuilder return this; } - /** - * Disable html escaping for the returned changesets. By default all - * changesets are html escaped. - * - * - * @param disableEscaping true to disable the html escaping - * - * @return {@code this} - * - * @since 1.35 - */ - public LogCommandBuilder setDisableEscaping(boolean disableEscaping) - { - this.disableEscaping = disableEscaping; - - return this; - } - /** * Disable the execution of pre processors. * @@ -545,9 +527,6 @@ public final class LogCommandBuilder /** repository to query */ private final Repository repository; - /** disable escaping */ - private boolean disableEscaping = false; - /** disable cache */ private boolean disableCache = false; diff --git a/scm-core/src/main/java/sonia/scm/repository/api/ModificationsCommandBuilder.java b/scm-core/src/main/java/sonia/scm/repository/api/ModificationsCommandBuilder.java new file mode 100644 index 0000000000..6459b47cd5 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/ModificationsCommandBuilder.java @@ -0,0 +1,110 @@ +package sonia.scm.repository.api; + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.Setter; +import lombok.ToString; +import lombok.experimental.Accessors; +import lombok.extern.slf4j.Slf4j; +import sonia.scm.cache.Cache; +import sonia.scm.repository.Modifications; +import sonia.scm.repository.PreProcessorUtil; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryCacheKey; +import sonia.scm.repository.RevisionNotFoundException; +import sonia.scm.repository.spi.ModificationsCommand; +import sonia.scm.repository.spi.ModificationsCommandRequest; + +import java.io.IOException; + +/** + * Get the modifications applied to files in a revision. + *

+ * Modifications are for example: Add, Update and Delete + * + * @author Mohamed Karray + * @since 2.0 + */ +@Slf4j +@RequiredArgsConstructor +@Accessors(fluent = true) +public final class ModificationsCommandBuilder { + static final String CACHE_NAME = "sonia.cache.cmd.modifications"; + + private final ModificationsCommand modificationsCommand; + + private final ModificationsCommandRequest request = new ModificationsCommandRequest(); + + private final Repository repository; + + private final Cache cache; + + private final PreProcessorUtil preProcessorUtil; + + @Setter + private boolean disableCache = false; + + @Setter + private boolean disablePreProcessors = false; + + public ModificationsCommandBuilder revision(String revision){ + request.setRevision(revision); + return this; + } + + /** + * Reset each parameter to its default value. + * + * + * @return {@code this} + */ + public ModificationsCommandBuilder reset() { + request.reset(); + this.disableCache = false; + this.disablePreProcessors = false; + return this; + } + + public Modifications getModifications() throws IOException, RevisionNotFoundException { + Modifications modifications; + if (disableCache) { + log.info("Get modifications for {} with disabled cache", request); + modifications = modificationsCommand.getModifications(request); + } else { + ModificationsCommandBuilder.CacheKey key = new ModificationsCommandBuilder.CacheKey(repository.getId(), request); + if (cache.contains(key)) { + modifications = cache.get(key); + log.debug("Get modifications for {} from the cache", request); + } else { + log.info("Get modifications for {} with enabled cache", request); + modifications = modificationsCommand.getModifications(request); + if (modifications != null) { + cache.put(key, modifications); + log.debug("Modifications for {} added to the cache with key {}", request, key); + } + } + } + if (!disablePreProcessors && (modifications != null)) { + preProcessorUtil.prepareForReturn(repository, modifications); + } + return modifications; + } + + @AllArgsConstructor + @Getter + @Setter + @EqualsAndHashCode + @ToString + class CacheKey implements RepositoryCacheKey { + private final String repositoryId; + private final ModificationsCommandRequest request; + + @Override + public String getRepositoryId() { + return repositoryId; + } + } + +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java index 27eed6becd..bdd6e4b320 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryService.java @@ -31,6 +31,7 @@ package sonia.scm.repository.api; +import lombok.extern.slf4j.Slf4j; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.cache.CacheManager; @@ -42,6 +43,8 @@ import sonia.scm.repository.spi.RepositoryServiceProvider; import java.io.Closeable; import java.io.IOException; +import java.util.Set; +import java.util.stream.Stream; /** * From the {@link RepositoryService} it is possible to access all commands for @@ -78,30 +81,32 @@ import java.io.IOException; * @apiviz.uses sonia.scm.repository.api.UnbundleCommandBuilder * @since 1.17 */ +@Slf4j public final class RepositoryService implements Closeable { - private CacheManager cacheManager; - private PreProcessorUtil preProcessorUtil; - private RepositoryServiceProvider provider; - private Repository repository; - private static final Logger logger = - LoggerFactory.getLogger(RepositoryService.class); + + private static final Logger logger = LoggerFactory.getLogger(RepositoryService.class); + + private final CacheManager cacheManager; + private final PreProcessorUtil preProcessorUtil; + private final RepositoryServiceProvider provider; + private final Repository repository; + private final Set protocolProviders; /** * Constructs a new {@link RepositoryService}. This constructor should only * be called from the {@link RepositoryServiceFactory}. - * - * @param cacheManager cache manager + * @param cacheManager cache manager * @param provider implementation for {@link RepositoryServiceProvider} * @param repository the repository - * @param preProcessorUtil */ RepositoryService(CacheManager cacheManager, - RepositoryServiceProvider provider, Repository repository, - PreProcessorUtil preProcessorUtil) { + RepositoryServiceProvider provider, Repository repository, + PreProcessorUtil preProcessorUtil, Set protocolProviders) { this.cacheManager = cacheManager; this.provider = provider; this.repository = repository; this.preProcessorUtil = preProcessorUtil; + this.protocolProviders = protocolProviders; } /** @@ -125,7 +130,7 @@ public final class RepositoryService implements Closeable { try { provider.close(); } catch (IOException ex) { - logger.error("Could not close repository service provider", ex); + log.error("Could not close repository service provider", ex); } } @@ -138,7 +143,7 @@ public final class RepositoryService implements Closeable { */ public BlameCommandBuilder getBlameCommand() { logger.debug("create blame command for repository {}", - repository.getName()); + repository.getNamespaceAndName()); return new BlameCommandBuilder(cacheManager, provider.getBlameCommand(), repository, preProcessorUtil); @@ -153,7 +158,7 @@ public final class RepositoryService implements Closeable { */ public BranchesCommandBuilder getBranchesCommand() { logger.debug("create branches command for repository {}", - repository.getName()); + repository.getNamespaceAndName()); return new BranchesCommandBuilder(cacheManager, provider.getBranchesCommand(), repository); @@ -168,7 +173,7 @@ public final class RepositoryService implements Closeable { */ public BrowseCommandBuilder getBrowseCommand() { logger.debug("create browse command for repository {}", - repository.getName()); + repository.getNamespaceAndName()); return new BrowseCommandBuilder(cacheManager, provider.getBrowseCommand(), repository, preProcessorUtil); @@ -184,7 +189,7 @@ public final class RepositoryService implements Closeable { */ public BundleCommandBuilder getBundleCommand() { logger.debug("create bundle command for repository {}", - repository.getName()); + repository.getNamespaceAndName()); return new BundleCommandBuilder(provider.getBundleCommand(), repository); } @@ -198,7 +203,7 @@ public final class RepositoryService implements Closeable { */ public CatCommandBuilder getCatCommand() { logger.debug("create cat command for repository {}", - repository.getName()); + repository.getNamespaceAndName()); return new CatCommandBuilder(provider.getCatCommand()); } @@ -213,7 +218,7 @@ public final class RepositoryService implements Closeable { */ public DiffCommandBuilder getDiffCommand() { logger.debug("create diff command for repository {}", - repository.getName()); + repository.getNamespaceAndName()); return new DiffCommandBuilder(provider.getDiffCommand()); } @@ -229,7 +234,7 @@ public final class RepositoryService implements Closeable { */ public IncomingCommandBuilder getIncomingCommand() { logger.debug("create incoming command for repository {}", - repository.getName()); + repository.getNamespaceAndName()); return new IncomingCommandBuilder(cacheManager, provider.getIncomingCommand(), repository, preProcessorUtil); @@ -244,12 +249,24 @@ public final class RepositoryService implements Closeable { */ public LogCommandBuilder getLogCommand() { logger.debug("create log command for repository {}", - repository.getName()); + repository.getNamespaceAndName()); return new LogCommandBuilder(cacheManager, provider.getLogCommand(), repository, preProcessorUtil); } + /** + * The modification command shows file modifications in a revision. + * + * @return instance of {@link ModificationsCommandBuilder} + * @throws CommandNotSupportedException if the command is not supported + * by the implementation of the repository service provider. + */ + public ModificationsCommandBuilder getModificationsCommand() { + logger.debug("create modifications command for repository {}", repository.getNamespaceAndName()); + return new ModificationsCommandBuilder(provider.getModificationsCommand(),repository, cacheManager.getCache(ModificationsCommandBuilder.CACHE_NAME), preProcessorUtil); + } + /** * The outgoing command show {@link Changeset}s not found in a remote repository. * @@ -260,7 +277,7 @@ public final class RepositoryService implements Closeable { */ public OutgoingCommandBuilder getOutgoingCommand() { logger.debug("create outgoing command for repository {}", - repository.getName()); + repository.getNamespaceAndName()); return new OutgoingCommandBuilder(cacheManager, provider.getOutgoingCommand(), repository, preProcessorUtil); @@ -276,7 +293,7 @@ public final class RepositoryService implements Closeable { */ public PullCommandBuilder getPullCommand() { logger.debug("create pull command for repository {}", - repository.getName()); + repository.getNamespaceAndName()); return new PullCommandBuilder(provider.getPullCommand(), repository); } @@ -291,7 +308,7 @@ public final class RepositoryService implements Closeable { */ public PushCommandBuilder getPushCommand() { logger.debug("create push command for repository {}", - repository.getName()); + repository.getNamespaceAndName()); return new PushCommandBuilder(provider.getPushCommand()); } @@ -314,7 +331,7 @@ public final class RepositoryService implements Closeable { */ public TagsCommandBuilder getTagsCommand() { logger.debug("create tags command for repository {}", - repository.getName()); + repository.getNamespaceAndName()); return new TagsCommandBuilder(cacheManager, provider.getTagsCommand(), repository); @@ -330,7 +347,7 @@ public final class RepositoryService implements Closeable { */ public UnbundleCommandBuilder getUnbundleCommand() { logger.debug("create unbundle command for repository {}", - repository.getName()); + repository.getNamespaceAndName()); return new UnbundleCommandBuilder(provider.getUnbundleCommand(), repository); @@ -357,5 +374,20 @@ public final class RepositoryService implements Closeable { return provider.getSupportedFeatures().contains(feature); } + public Stream getSupportedProtocols() { + return protocolProviders.stream() + .filter(protocolProvider -> protocolProvider.getType().equals(getRepository().getType())) + .map(this::createProviderInstanceForRepository); + } + private T createProviderInstanceForRepository(ScmProtocolProvider protocolProvider) { + return protocolProvider.get(repository); + } + + public T getProtocol(Class clazz) { + return this.getSupportedProtocols() + .filter(scmProtocol -> clazz.isAssignableFrom(scmProtocol.getClass())) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException(String.format("no implementation for %s and repository type %s", clazz.getName(),getRepository().getType()))); + } } diff --git a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java index 627e57a797..fbb1ee6b58 100644 --- a/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java +++ b/scm-core/src/main/java/sonia/scm/repository/api/RepositoryServiceFactory.java @@ -137,13 +137,15 @@ public final class RepositoryServiceFactory @Inject public RepositoryServiceFactory(ScmConfiguration configuration, CacheManager cacheManager, RepositoryManager repositoryManager, - Set resolvers, PreProcessorUtil preProcessorUtil) + Set resolvers, PreProcessorUtil preProcessorUtil, + Set protocolProviders) { this.configuration = configuration; this.cacheManager = cacheManager; this.repositoryManager = repositoryManager; this.resolvers = resolvers; this.preProcessorUtil = preProcessorUtil; + this.protocolProviders = protocolProviders; ScmEventBus.getInstance().register(new CacheClearHook(cacheManager)); } @@ -208,9 +210,7 @@ public final class RepositoryServiceFactory if (repository == null) { - String msg = "could not find a repository with namespace/name " + namespaceAndName; - - throw new RepositoryNotFoundException(msg); + throw new RepositoryNotFoundException(namespaceAndName); } return create(repository); @@ -254,7 +254,7 @@ public final class RepositoryServiceFactory } service = new RepositoryService(cacheManager, provider, repository, - preProcessorUtil); + preProcessorUtil, protocolProviders); break; } @@ -369,4 +369,6 @@ public final class RepositoryServiceFactory /** service resolvers */ private final Set resolvers; + + private Set protocolProviders; } diff --git a/scm-core/src/main/java/sonia/scm/repository/api/ScmProtocol.java b/scm-core/src/main/java/sonia/scm/repository/api/ScmProtocol.java new file mode 100644 index 0000000000..c987510491 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/ScmProtocol.java @@ -0,0 +1,19 @@ +package sonia.scm.repository.api; + +/** + * An ScmProtocol represents a concrete protocol provided by the SCM-Manager instance + * to interact with a repository depending on its type. There may be multiple protocols + * available for a repository type (eg. http and ssh). + */ +public interface ScmProtocol { + + /** + * The type of the concrete protocol, eg. "http" or "ssh". + */ + String getType(); + + /** + * The URL to access the repository providing this protocol. + */ + String getUrl(); +} diff --git a/scm-core/src/main/java/sonia/scm/repository/api/ScmProtocolProvider.java b/scm-core/src/main/java/sonia/scm/repository/api/ScmProtocolProvider.java new file mode 100644 index 0000000000..597826676d --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/api/ScmProtocolProvider.java @@ -0,0 +1,12 @@ +package sonia.scm.repository.api; + +import sonia.scm.plugin.ExtensionPoint; +import sonia.scm.repository.Repository; + +@ExtensionPoint(multi = true) +public interface ScmProtocolProvider { + + String getType(); + + T get(Repository repository); +} diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/HttpScmProtocol.java b/scm-core/src/main/java/sonia/scm/repository/spi/HttpScmProtocol.java new file mode 100644 index 0000000000..b933abf559 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/spi/HttpScmProtocol.java @@ -0,0 +1,38 @@ +package sonia.scm.repository.spi; + +import sonia.scm.repository.Repository; +import sonia.scm.repository.api.ScmProtocol; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.URI; + +public abstract class HttpScmProtocol implements ScmProtocol { + + private final Repository repository; + private final String basePath; + + public HttpScmProtocol(Repository repository, String basePath) { + this.repository = repository; + this.basePath = basePath; + } + + @Override + public String getType() { + return "http"; + } + + @Override + public String getUrl() { + return URI.create(basePath + "/").resolve(String.format("repo/%s/%s", repository.getNamespace(), repository.getName())).toASCIIString(); + } + + public final void serve(HttpServletRequest request, HttpServletResponse response, ServletConfig config) throws ServletException, IOException { + serve(request, response, repository, config); + } + + protected abstract void serve(HttpServletRequest request, HttpServletResponse response, Repository repository, ServletConfig config) throws ServletException, IOException; +} diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/InitializingHttpScmProtocolWrapper.java b/scm-core/src/main/java/sonia/scm/repository/spi/InitializingHttpScmProtocolWrapper.java new file mode 100644 index 0000000000..c1b7229036 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/spi/InitializingHttpScmProtocolWrapper.java @@ -0,0 +1,91 @@ +package sonia.scm.repository.spi; + +import lombok.extern.slf4j.Slf4j; +import sonia.scm.api.v2.resources.ScmPathInfoStore; +import sonia.scm.config.ScmConfiguration; +import sonia.scm.repository.Repository; +import sonia.scm.repository.api.ScmProtocolProvider; + +import javax.inject.Provider; +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Optional; + +import static java.util.Optional.empty; +import static java.util.Optional.of; + +@Slf4j +public abstract class InitializingHttpScmProtocolWrapper implements ScmProtocolProvider { + + private final Provider delegateProvider; + private final Provider pathInfoStore; + private final ScmConfiguration scmConfiguration; + + private volatile boolean isInitialized = false; + + + protected InitializingHttpScmProtocolWrapper(Provider delegateProvider, Provider pathInfoStore, ScmConfiguration scmConfiguration) { + this.delegateProvider = delegateProvider; + this.pathInfoStore = pathInfoStore; + this.scmConfiguration = scmConfiguration; + } + + protected void initializeServlet(ServletConfig config, ScmProviderHttpServlet httpServlet) throws ServletException { + httpServlet.init(config); + } + + @Override + public HttpScmProtocol get(Repository repository) { + if (!repository.getType().equals(getType())) { + throw new IllegalArgumentException(String.format("cannot handle repository with type %s with protocol for type %s", repository.getType(), getType())); + } + return new ProtocolWrapper(repository, computeBasePath()); + } + + private String computeBasePath() { + return getPathFromScmPathInfoIfAvailable().orElse(getPathFromConfiguration()); + } + + private Optional getPathFromScmPathInfoIfAvailable() { + try { + ScmPathInfoStore scmPathInfoStore = pathInfoStore.get(); + if (scmPathInfoStore != null && scmPathInfoStore.get() != null) { + return of(scmPathInfoStore.get().getRootUri().toASCIIString()); + } + } catch (Exception e) { + log.debug("could not get ScmPathInfoStore from context", e); + } + return empty(); + } + + private String getPathFromConfiguration() { + log.debug("using base path from configuration: {}", scmConfiguration.getBaseUrl()); + return scmConfiguration.getBaseUrl(); + } + + private class ProtocolWrapper extends HttpScmProtocol { + + public ProtocolWrapper(Repository repository, String basePath) { + super(repository, basePath); + } + + @Override + protected void serve(HttpServletRequest request, HttpServletResponse response, Repository repository, ServletConfig config) throws ServletException, IOException { + if (!isInitialized) { + synchronized (InitializingHttpScmProtocolWrapper.this) { + if (!isInitialized) { + ScmProviderHttpServlet httpServlet = delegateProvider.get(); + initializeServlet(config, httpServlet); + isInitialized = true; + } + } + } + + delegateProvider.get().service(request, response, repository); + } + + } +} diff --git a/scm-core/src/test/java/sonia/scm/repository/RepositoryTest.java b/scm-core/src/main/java/sonia/scm/repository/spi/ModificationsCommand.java similarity index 73% rename from scm-core/src/test/java/sonia/scm/repository/RepositoryTest.java rename to scm-core/src/main/java/sonia/scm/repository/spi/ModificationsCommand.java index f13f4cbc67..e9b40e8a17 100644 --- a/scm-core/src/test/java/sonia/scm/repository/RepositoryTest.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/ModificationsCommand.java @@ -1,4 +1,4 @@ -/** +/* * Copyright (c) 2010, Sebastian Sdorra * All rights reserved. * @@ -29,32 +29,25 @@ * */ +package sonia.scm.repository.spi; -package sonia.scm.repository; +import sonia.scm.repository.Modifications; +import sonia.scm.repository.RevisionNotFoundException; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; +import java.io.IOException; /** + * Command to get the modifications applied to files in a revision. * - * @author Sebastian Sdorra + * Modifications are for example: Add, Update, Delete + * + * @author Mohamed Karray + * @since 2.0 */ -public class RepositoryTest -{ +public interface ModificationsCommand { - /** - * Method description - * - */ - @Test - public void testCreateUrl() - { - Repository repository = new Repository("123", "hg", "test", "repo"); + Modifications getModifications(String revision) throws IOException, RevisionNotFoundException; + + Modifications getModifications(ModificationsCommandRequest request) throws IOException, RevisionNotFoundException; - assertEquals("http://localhost:8080/scm/hg/test/repo", - repository.createUrl("http://localhost:8080/scm")); - assertEquals("http://localhost:8080/scm/hg/test/repo", - repository.createUrl("http://localhost:8080/scm/")); - } } diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/ModificationsCommandRequest.java b/scm-core/src/main/java/sonia/scm/repository/spi/ModificationsCommandRequest.java new file mode 100644 index 0000000000..8a910a3396 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/spi/ModificationsCommandRequest.java @@ -0,0 +1,24 @@ +package sonia.scm.repository.spi; + + +import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@ToString +@EqualsAndHashCode +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class ModificationsCommandRequest implements Resetable { + private String revision; + + @Override + public void reset() { + revision = null; + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java b/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java index 976f38fffb..c66c56c0f1 100644 --- a/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java +++ b/scm-core/src/main/java/sonia/scm/repository/spi/RepositoryServiceProvider.java @@ -33,20 +33,17 @@ package sonia.scm.repository.spi; -//~--- non-JDK imports -------------------------------------------------------- - import sonia.scm.repository.Feature; import sonia.scm.repository.api.Command; import sonia.scm.repository.api.CommandNotSupportedException; -//~--- JDK imports ------------------------------------------------------------ - import java.io.Closeable; import java.io.IOException; - import java.util.Collections; import java.util.Set; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -173,6 +170,16 @@ public abstract class RepositoryServiceProvider implements Closeable throw new CommandNotSupportedException(Command.LOG); } + /** + * Get the corresponding {@link ModificationsCommand} implemented from the Plugins + * + * @return the corresponding {@link ModificationsCommand} implemented from the Plugins + * @throws CommandNotSupportedException if there is no Implementation + */ + public ModificationsCommand getModificationsCommand() { + throw new CommandNotSupportedException(Command.MODIFICATIONS); + } + /** * Method description * diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/ScmProviderHttpServlet.java b/scm-core/src/main/java/sonia/scm/repository/spi/ScmProviderHttpServlet.java new file mode 100644 index 0000000000..3a9dad52d6 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/spi/ScmProviderHttpServlet.java @@ -0,0 +1,16 @@ +package sonia.scm.repository.spi; + +import sonia.scm.repository.Repository; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public interface ScmProviderHttpServlet { + + void service(HttpServletRequest request, HttpServletResponse response, Repository repository) throws ServletException, IOException; + + void init(ServletConfig config) throws ServletException; +} diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/ScmProviderHttpServletDecorator.java b/scm-core/src/main/java/sonia/scm/repository/spi/ScmProviderHttpServletDecorator.java new file mode 100644 index 0000000000..c5dd55d277 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/spi/ScmProviderHttpServletDecorator.java @@ -0,0 +1,28 @@ +package sonia.scm.repository.spi; + +import sonia.scm.repository.Repository; + +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class ScmProviderHttpServletDecorator implements ScmProviderHttpServlet { + + private final ScmProviderHttpServlet object; + + public ScmProviderHttpServletDecorator(ScmProviderHttpServlet object) { + this.object = object; + } + + @Override + public void service(HttpServletRequest request, HttpServletResponse response, Repository repository) throws ServletException, IOException { + object.service(request, response, repository); + } + + @Override + public void init(ServletConfig config) throws ServletException { + object.init(config); + } +} diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/ScmProviderHttpServletDecoratorFactory.java b/scm-core/src/main/java/sonia/scm/repository/spi/ScmProviderHttpServletDecoratorFactory.java new file mode 100644 index 0000000000..531a25e91d --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/spi/ScmProviderHttpServletDecoratorFactory.java @@ -0,0 +1,15 @@ +package sonia.scm.repository.spi; + +import sonia.scm.DecoratorFactory; +import sonia.scm.plugin.ExtensionPoint; + +@ExtensionPoint +public interface ScmProviderHttpServletDecoratorFactory extends DecoratorFactory { + /** + * Has to return true if this factory provides a decorator for the given scm type (eg. "git", "hg" or + * "svn"). + * @param type The current scm type this factory can provide a decorator for. + * @return true when the provided decorator should be used for the given scm type. + */ + boolean handlesScmType(String type); +} diff --git a/scm-core/src/main/java/sonia/scm/repository/spi/ScmProviderHttpServletProvider.java b/scm-core/src/main/java/sonia/scm/repository/spi/ScmProviderHttpServletProvider.java new file mode 100644 index 0000000000..3793d4b935 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/repository/spi/ScmProviderHttpServletProvider.java @@ -0,0 +1,33 @@ +package sonia.scm.repository.spi; + +import com.google.inject.Inject; +import sonia.scm.util.Decorators; + +import javax.inject.Provider; +import java.util.List; +import java.util.Set; + +import static java.util.stream.Collectors.toList; + +public abstract class ScmProviderHttpServletProvider implements Provider { + + @Inject(optional = true) + private Set decoratorFactories; + + private final String type; + + protected ScmProviderHttpServletProvider(String type) { + this.type = type; + } + + @Override + public ScmProviderHttpServlet get() { + return Decorators.decorate(getRootServlet(), getDecoratorsForType()); + } + + private List getDecoratorsForType() { + return decoratorFactories.stream().filter(d -> d.handlesScmType(type)).collect(toList()); + } + + protected abstract ScmProviderHttpServlet getRootServlet(); +} diff --git a/scm-core/src/main/java/sonia/scm/user/ChangePasswordNotAllowedException.java b/scm-core/src/main/java/sonia/scm/user/ChangePasswordNotAllowedException.java new file mode 100644 index 0000000000..19c609ba30 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/user/ChangePasswordNotAllowedException.java @@ -0,0 +1,11 @@ +package sonia.scm.user; + +public class ChangePasswordNotAllowedException extends RuntimeException { + + public static final String WRONG_USER_TYPE = "User of type {0} are not allowed to change password"; + + public ChangePasswordNotAllowedException(String message) { + super(message); + } + +} diff --git a/scm-core/src/main/java/sonia/scm/user/InvalidPasswordException.java b/scm-core/src/main/java/sonia/scm/user/InvalidPasswordException.java new file mode 100644 index 0000000000..e06191a8f2 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/user/InvalidPasswordException.java @@ -0,0 +1,10 @@ +package sonia.scm.user; + +public class InvalidPasswordException extends RuntimeException { + + public static final String INVALID_MATCHING = "The given Password does not match with the stored one."; + + public InvalidPasswordException(String message) { + super(message); + } +} diff --git a/scm-core/src/main/java/sonia/scm/user/User.java b/scm-core/src/main/java/sonia/scm/user/User.java index 778e573c14..0d909bec8d 100644 --- a/scm-core/src/main/java/sonia/scm/user/User.java +++ b/scm-core/src/main/java/sonia/scm/user/User.java @@ -55,11 +55,10 @@ import java.security.Principal; * * @author Sebastian Sdorra */ -@StaticPermissions("user") +@StaticPermissions(value = "user", globalPermissions = {"create", "list"}) @XmlRootElement(name = "users") @XmlAccessorType(XmlAccessType.FIELD) -public class -User extends BasicPropertiesAware implements Principal, ModelObject, PermissionObject +public class User extends BasicPropertiesAware implements Principal, ModelObject, PermissionObject { /** Field description */ @@ -274,6 +273,10 @@ User extends BasicPropertiesAware implements Principal, ModelObject, PermissionO //J+ } + public User changePassword(String password){ + setPassword(password); + return this; + } //~--- get methods ---------------------------------------------------------- /** diff --git a/scm-core/src/main/java/sonia/scm/user/UserManager.java b/scm-core/src/main/java/sonia/scm/user/UserManager.java index 0a705c8d60..1f5aee1f19 100644 --- a/scm-core/src/main/java/sonia/scm/user/UserManager.java +++ b/scm-core/src/main/java/sonia/scm/user/UserManager.java @@ -38,6 +38,11 @@ package sonia.scm.user; import sonia.scm.Manager; import sonia.scm.search.Searchable; +import java.text.MessageFormat; +import java.util.function.Consumer; + +import static sonia.scm.user.ChangePasswordNotAllowedException.WRONG_USER_TYPE; + /** * The central class for managing {@link User} objects. * This class is a singleton and is available via injection. @@ -68,4 +73,22 @@ public interface UserManager * @since 1.14 */ public String getDefaultType(); + + + /** + * Only account of the default type "xml" can change their password + */ + default Consumer getUserTypeChecker() { + return user -> { + if (!isTypeDefault(user)) { + throw new ChangePasswordNotAllowedException(MessageFormat.format(WRONG_USER_TYPE, user.getType())); + } + }; + } + + default boolean isTypeDefault(User user) { + return getDefaultType().equals(user.getType()); + } + + } diff --git a/scm-webapp/src/main/java/sonia/scm/util/Decorators.java b/scm-core/src/main/java/sonia/scm/util/Decorators.java similarity index 99% rename from scm-webapp/src/main/java/sonia/scm/util/Decorators.java rename to scm-core/src/main/java/sonia/scm/util/Decorators.java index 41c75660a9..6465631d03 100644 --- a/scm-webapp/src/main/java/sonia/scm/util/Decorators.java +++ b/scm-core/src/main/java/sonia/scm/util/Decorators.java @@ -37,7 +37,6 @@ package sonia.scm.util; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.DecoratorFactory; /** diff --git a/scm-core/src/main/java/sonia/scm/web/JsonEnricherBase.java b/scm-core/src/main/java/sonia/scm/web/JsonEnricherBase.java new file mode 100644 index 0000000000..1baecb62af --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/web/JsonEnricherBase.java @@ -0,0 +1,40 @@ +package sonia.scm.web; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import java.util.Map; + +public abstract class JsonEnricherBase implements JsonEnricher { + + private final ObjectMapper objectMapper; + + protected JsonEnricherBase(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + protected boolean resultHasMediaType(String mediaType, JsonEnricherContext context) { + return mediaType.equals(context.getResponseMediaType().toString()); + } + + protected JsonNode value(Object object) { + return objectMapper.convertValue(object, JsonNode.class); + } + + protected ObjectNode createObject() { + return objectMapper.createObjectNode(); + } + + protected ObjectNode createObject(Map values) { + ObjectNode object = createObject(); + + values.forEach((key, value) -> object.set(key, value(value))); + + return object; + } + + protected void addPropertyNode(JsonNode parent, String newKey, JsonNode child) { + ((ObjectNode) parent).set(newKey, child); + } +} diff --git a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java index ddcce9f4e8..f0711cd1e4 100644 --- a/scm-core/src/main/java/sonia/scm/web/VndMediaType.java +++ b/scm-core/src/main/java/sonia/scm/web/VndMediaType.java @@ -15,12 +15,14 @@ public class VndMediaType { public static final String PLAIN_TEXT_PREFIX = "text/" + SUBTYPE_PREFIX; public static final String PLAIN_TEXT_SUFFIX = "+plain;v=" + VERSION; + public static final String INDEX = PREFIX + "index" + SUFFIX; public static final String USER = PREFIX + "user" + SUFFIX; public static final String GROUP = PREFIX + "group" + SUFFIX; public static final String REPOSITORY = PREFIX + "repository" + SUFFIX; public static final String PERMISSION = PREFIX + "permission" + SUFFIX; public static final String CHANGESET = PREFIX + "changeset" + SUFFIX; public static final String CHANGESET_COLLECTION = PREFIX + "changesetCollection" + SUFFIX; + public static final String MODIFICATIONS = PREFIX + "modifications" + SUFFIX; public static final String TAG = PREFIX + "tag" + SUFFIX; public static final String TAG_COLLECTION = PREFIX + "tagCollection" + SUFFIX; public static final String BRANCH = PREFIX + "branch" + SUFFIX; @@ -34,6 +36,8 @@ public class VndMediaType { public static final String REPOSITORY_TYPE = PREFIX + "repositoryType" + SUFFIX; public static final String UI_PLUGIN = PREFIX + "uiPlugin" + SUFFIX; public static final String UI_PLUGIN_COLLECTION = PREFIX + "uiPluginCollection" + SUFFIX; + @SuppressWarnings("squid:S2068") + public static final String PASSWORD_CHANGE = PREFIX + "passwordChange" + SUFFIX; public static final String ME = PREFIX + "me" + SUFFIX; public static final String SOURCE = PREFIX + "source" + SUFFIX; diff --git a/scm-core/src/main/java/sonia/scm/web/filter/PermissionFilter.java b/scm-core/src/main/java/sonia/scm/web/filter/PermissionFilter.java index dd3c82e800..328494a626 100644 --- a/scm-core/src/main/java/sonia/scm/web/filter/PermissionFilter.java +++ b/scm-core/src/main/java/sonia/scm/web/filter/PermissionFilter.java @@ -33,39 +33,32 @@ package sonia.scm.web.filter; -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.common.base.Splitter; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authz.AuthorizationException; import org.apache.shiro.subject.Subject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import sonia.scm.ArgumentIsInvalidException; import sonia.scm.SCMContext; import sonia.scm.config.ScmConfiguration; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryPermissions; +import sonia.scm.repository.spi.ScmProviderHttpServlet; +import sonia.scm.repository.spi.ScmProviderHttpServletDecorator; import sonia.scm.security.Role; import sonia.scm.security.ScmSecurityException; import sonia.scm.util.HttpUtil; -import sonia.scm.util.Util; -import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; -import java.util.Iterator; - -//~--- JDK imports ------------------------------------------------------------ /** * Abstract http filter to check repository permissions. * * @author Sebastian Sdorra */ -public abstract class PermissionFilter extends HttpFilter +public abstract class PermissionFilter extends ScmProviderHttpServletDecorator { /** the logger for PermissionFilter */ @@ -81,23 +74,14 @@ public abstract class PermissionFilter extends HttpFilter * * @since 1.21 */ - public PermissionFilter(ScmConfiguration configuration) + protected PermissionFilter(ScmConfiguration configuration, ScmProviderHttpServlet delegate) { + super(delegate); this.configuration = configuration; } //~--- get methods ---------------------------------------------------------- - /** - * Returns the requested repository. - * - * - * @param request current http request - * - * @return requested repository - */ - protected abstract Repository getRepository(HttpServletRequest request); - /** * Returns true if the current request is a write request. * @@ -117,66 +101,38 @@ public abstract class PermissionFilter extends HttpFilter * * @param request http request * @param response http response - * @param chain filter chain * * @throws IOException * @throws ServletException */ @Override - protected void doFilter(HttpServletRequest request, - HttpServletResponse response, FilterChain chain) + public void service(HttpServletRequest request, + HttpServletResponse response, Repository repository) throws IOException, ServletException { Subject subject = SecurityUtils.getSubject(); try { - Repository repository = getRepository(request); + boolean writeRequest = isWriteRequest(request); - if (repository != null) + if (hasPermission(repository, writeRequest)) { - boolean writeRequest = isWriteRequest(request); + logger.trace("{} access to repository {} for user {} granted", + getActionAsString(writeRequest), repository.getName(), + getUserName(subject)); - if (hasPermission(repository, writeRequest)) - { - logger.trace("{} access to repository {} for user {} granted", - getActionAsString(writeRequest), repository.getName(), - getUserName(subject)); - - chain.doFilter(request, response); - } - else - { - logger.info("{} access to repository {} for user {} denied", - getActionAsString(writeRequest), repository.getName(), - getUserName(subject)); - - sendAccessDenied(request, response, subject); - } + super.service(request, response, repository); } else { - logger.debug("repository not found"); + logger.info("{} access to repository {} for user {} denied", + getActionAsString(writeRequest), repository.getName(), + getUserName(subject)); - response.sendError(HttpServletResponse.SC_NOT_FOUND); + sendAccessDenied(request, response, subject); } } - catch (ArgumentIsInvalidException ex) - { - if (logger.isTraceEnabled()) - { - logger.trace( - "wrong request at ".concat(request.getRequestURI()).concat( - " send redirect"), ex); - } - else if (logger.isWarnEnabled()) - { - logger.warn("wrong request at {} send redirect", - request.getRequestURI()); - } - - response.sendRedirect(getRepositoryRootHelpUrl(request)); - } catch (ScmSecurityException | AuthorizationException ex) { logger.warn("user " + subject.getPrincipal() + " has not enough permissions", ex); @@ -217,29 +173,6 @@ public abstract class PermissionFilter extends HttpFilter HttpUtil.sendUnauthorized(response, configuration.getRealmDescription()); } - /** - * Extracts the type of the repositroy from url. - * - * - * @param request http request - * - * @return type of repository - */ - private String extractType(HttpServletRequest request) - { - Iterator it = Splitter.on( - HttpUtil.SEPARATOR_PATH).omitEmptyStrings().split( - request.getRequestURI()).iterator(); - String type = it.next(); - - if (Util.isNotEmpty(request.getContextPath())) - { - type = it.next(); - } - - return type; - } - /** * Send access denied to the servlet response. * @@ -280,25 +213,6 @@ public abstract class PermissionFilter extends HttpFilter : "read"; } - /** - * Returns the repository root help url. - * - * - * @param request current http request - * - * @return repository root help url - */ - private String getRepositoryRootHelpUrl(HttpServletRequest request) - { - String type = extractType(request); - String helpUrl = HttpUtil.getCompleteUrl(request, - "/api/rest/help/repository-root/"); - - helpUrl = helpUrl.concat(type).concat(".html"); - - return helpUrl; - } - /** * Returns the username from the given subject or anonymous. * diff --git a/scm-core/src/main/java/sonia/scm/web/filter/ProviderPermissionFilter.java b/scm-core/src/main/java/sonia/scm/web/filter/ProviderPermissionFilter.java deleted file mode 100644 index ea0d90c915..0000000000 --- a/scm-core/src/main/java/sonia/scm/web/filter/ProviderPermissionFilter.java +++ /dev/null @@ -1,118 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * 3. Neither the name of SCM-Manager; nor the names of its - * contributors may be used to endorse or promote products derived from this - * software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE - * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY - * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES - * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; - * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON - * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS - * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - - -package sonia.scm.web.filter; - -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.common.base.Throwables; -import com.google.inject.ProvisionException; - -import org.apache.shiro.authz.AuthorizationException; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import sonia.scm.config.ScmConfiguration; -import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryProvider; - -//~--- JDK imports ------------------------------------------------------------ - -import javax.servlet.http.HttpServletRequest; - -/** - * - * @author Sebastian Sdorra - * @since 1.9 - */ -public abstract class ProviderPermissionFilter extends PermissionFilter -{ - - /** - * the logger for ProviderPermissionFilter - */ - private static final Logger logger = - LoggerFactory.getLogger(ProviderPermissionFilter.class); - - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * - * @param configuration - * @param repositoryProvider - * @since 1.21 - */ - public ProviderPermissionFilter(ScmConfiguration configuration, - RepositoryProvider repositoryProvider) - { - super(configuration); - this.repositoryProvider = repositoryProvider; - } - - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param request - * - * @return - */ - @Override - protected Repository getRepository(HttpServletRequest request) - { - Repository repository = null; - - try - { - repository = repositoryProvider.get(); - } - catch (ProvisionException ex) - { - Throwables.propagateIfPossible(ex.getCause(), - IllegalStateException.class, AuthorizationException.class); - logger.error("could not get repository from request", ex); - } - - return repository; - } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private final RepositoryProvider repositoryProvider; -} diff --git a/scm-core/src/test/java/sonia/scm/repository/api/RepositoryServiceTest.java b/scm-core/src/test/java/sonia/scm/repository/api/RepositoryServiceTest.java new file mode 100644 index 0000000000..2ceafc19bb --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/repository/api/RepositoryServiceTest.java @@ -0,0 +1,74 @@ +package sonia.scm.repository.api; + +import org.junit.Test; +import sonia.scm.repository.Repository; +import sonia.scm.repository.spi.HttpScmProtocol; +import sonia.scm.repository.spi.RepositoryServiceProvider; + +import javax.servlet.ServletConfig; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Collections; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.util.IterableUtil.sizeOf; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; + +public class RepositoryServiceTest { + + private final RepositoryServiceProvider provider = mock(RepositoryServiceProvider.class); + private final Repository repository = new Repository("", "git", "space", "repo"); + + @Test + public void shouldReturnMatchingProtocolsFromProvider() { + RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider())); + Stream supportedProtocols = repositoryService.getSupportedProtocols(); + + assertThat(sizeOf(supportedProtocols.collect(Collectors.toList()))).isEqualTo(1); + } + + @Test + public void shouldFindKnownProtocol() { + RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider())); + + HttpScmProtocol protocol = repositoryService.getProtocol(HttpScmProtocol.class); + + assertThat(protocol).isNotNull(); + } + + @Test + public void shouldFailForUnknownProtocol() { + RepositoryService repositoryService = new RepositoryService(null, provider, repository, null, Collections.singleton(new DummyScmProtocolProvider())); + + assertThrows(IllegalArgumentException.class, () -> { + repositoryService.getProtocol(UnknownScmProtocol.class); + }); + } + + private static class DummyHttpProtocol extends HttpScmProtocol { + public DummyHttpProtocol(Repository repository) { + super(repository, ""); + } + + @Override + public void serve(HttpServletRequest request, HttpServletResponse response, Repository repository, ServletConfig config) { + } + } + + private static class DummyScmProtocolProvider implements ScmProtocolProvider { + @Override + public String getType() { + return "git"; + } + + @Override + public ScmProtocol get(Repository repository) { + return new DummyHttpProtocol(repository); + } + } + + private interface UnknownScmProtocol extends ScmProtocol {} +} diff --git a/scm-core/src/test/java/sonia/scm/repository/spi/HttpScmProtocolTest.java b/scm-core/src/test/java/sonia/scm/repository/spi/HttpScmProtocolTest.java new file mode 100644 index 0000000000..1fd772fee3 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/repository/spi/HttpScmProtocolTest.java @@ -0,0 +1,40 @@ +package sonia.scm.repository.spi; + +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; +import sonia.scm.repository.Repository; + +import javax.servlet.ServletConfig; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +class HttpScmProtocolTest { + + @TestFactory + Stream shouldCreateCorrectUrlsWithContextPath() { + return Stream.of("http://localhost/scm", "http://localhost/scm/") + .map(url -> assertResultingUrl(url, "http://localhost/scm/repo/space/name")); + } + + @TestFactory + Stream shouldCreateCorrectUrlsWithPort() { + return Stream.of("http://localhost:8080", "http://localhost:8080/") + .map(url -> assertResultingUrl(url, "http://localhost:8080/repo/space/name")); + } + + DynamicTest assertResultingUrl(String baseUrl, String expectedUrl) { + String actualUrl = createInstanceOfHttpScmProtocol(baseUrl).getUrl(); + return DynamicTest.dynamicTest(baseUrl + " -> " + expectedUrl, () -> assertThat(actualUrl).isEqualTo(expectedUrl)); + } + + private HttpScmProtocol createInstanceOfHttpScmProtocol(String baseUrl) { + return new HttpScmProtocol(new Repository("", "", "space", "name"), baseUrl) { + @Override + protected void serve(HttpServletRequest request, HttpServletResponse response, Repository repository, ServletConfig config) { + } + }; + } +} diff --git a/scm-core/src/test/java/sonia/scm/repository/spi/InitializingHttpScmProtocolWrapperTest.java b/scm-core/src/test/java/sonia/scm/repository/spi/InitializingHttpScmProtocolWrapperTest.java new file mode 100644 index 0000000000..8c910f92a9 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/repository/spi/InitializingHttpScmProtocolWrapperTest.java @@ -0,0 +1,120 @@ +package sonia.scm.repository.spi; + +import com.google.inject.ProvisionException; +import com.google.inject.util.Providers; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.stubbing.OngoingStubbing; +import sonia.scm.api.v2.resources.ScmPathInfo; +import sonia.scm.api.v2.resources.ScmPathInfoStore; +import sonia.scm.config.ScmConfiguration; +import sonia.scm.repository.Repository; + +import javax.inject.Provider; +import javax.servlet.ServletConfig; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.URI; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; + +public class InitializingHttpScmProtocolWrapperTest { + + private static final Repository REPOSITORY = new Repository("", "git", "space", "name"); + + @Mock + private ScmProviderHttpServlet delegateServlet; + @Mock + private ScmPathInfoStore pathInfoStore; + @Mock + private ScmConfiguration scmConfiguration; + private Provider pathInfoStoreProvider; + + @Mock + private HttpServletRequest request; + @Mock + private HttpServletResponse response; + @Mock + private ServletConfig servletConfig; + + private InitializingHttpScmProtocolWrapper wrapper; + + @Before + public void init() { + initMocks(this); + pathInfoStoreProvider = mock(Provider.class); + when(pathInfoStoreProvider.get()).thenReturn(pathInfoStore); + + wrapper = new InitializingHttpScmProtocolWrapper(Providers.of(this.delegateServlet), pathInfoStoreProvider, scmConfiguration) { + @Override + public String getType() { + return "git"; + } + }; + when(scmConfiguration.getBaseUrl()).thenReturn("http://example.com/scm"); + } + + @Test + public void shouldUsePathFromPathInfo() { + mockSetPathInfo(); + + HttpScmProtocol httpScmProtocol = wrapper.get(REPOSITORY); + + assertEquals("http://example.com/scm/repo/space/name", httpScmProtocol.getUrl()); + } + + @Test + public void shouldUseConfigurationWhenPathInfoNotSet() { + HttpScmProtocol httpScmProtocol = wrapper.get(REPOSITORY); + + assertEquals("http://example.com/scm/repo/space/name", httpScmProtocol.getUrl()); + } + + @Test + public void shouldUseConfigurationWhenNotInRequestScope() { + when(pathInfoStoreProvider.get()).thenThrow(new ProvisionException("test")); + + HttpScmProtocol httpScmProtocol = wrapper.get(REPOSITORY); + + assertEquals("http://example.com/scm/repo/space/name", httpScmProtocol.getUrl()); + } + + @Test + public void shouldInitializeAndDelegateRequestThroughFilter() throws ServletException, IOException { + HttpScmProtocol httpScmProtocol = wrapper.get(REPOSITORY); + + httpScmProtocol.serve(request, response, servletConfig); + + verify(delegateServlet).init(servletConfig); + verify(delegateServlet).service(request, response, REPOSITORY); + } + + @Test + public void shouldInitializeOnlyOnce() throws ServletException, IOException { + HttpScmProtocol httpScmProtocol = wrapper.get(REPOSITORY); + + httpScmProtocol.serve(request, response, servletConfig); + httpScmProtocol.serve(request, response, servletConfig); + + verify(delegateServlet, times(1)).init(servletConfig); + verify(delegateServlet, times(2)).service(request, response, REPOSITORY); + } + + @Test(expected = IllegalArgumentException.class) + public void shouldFailForIllegalScmType() { + HttpScmProtocol httpScmProtocol = wrapper.get(new Repository("", "other", "space", "name")); + } + + private OngoingStubbing mockSetPathInfo() { + return when(pathInfoStore.get()).thenReturn(() -> URI.create("http://example.com/scm/api/rest/")); + } + +} diff --git a/scm-core/src/test/java/sonia/scm/web/JsonEnricherBaseTest.java b/scm-core/src/test/java/sonia/scm/web/JsonEnricherBaseTest.java new file mode 100644 index 0000000000..43ed4940fa --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/web/JsonEnricherBaseTest.java @@ -0,0 +1,51 @@ +package sonia.scm.web; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.Test; + +import javax.ws.rs.core.MediaType; + +import static java.util.Collections.singletonMap; +import static org.assertj.core.api.Assertions.assertThat; + +public class JsonEnricherBaseTest { + + private ObjectMapper objectMapper = new ObjectMapper(); + private TestJsonEnricher enricher = new TestJsonEnricher(objectMapper); + + @Test + public void testResultHasMediaType() { + JsonEnricherContext context = new JsonEnricherContext(null, MediaType.APPLICATION_JSON_TYPE, null); + + assertThat(enricher.resultHasMediaType(MediaType.APPLICATION_JSON, context)).isTrue(); + assertThat(enricher.resultHasMediaType(MediaType.APPLICATION_XML, context)).isFalse(); + } + + @Test + public void testAppendLink() { + ObjectNode root = objectMapper.createObjectNode(); + ObjectNode links = objectMapper.createObjectNode(); + root.set("_links", links); + JsonEnricherContext context = new JsonEnricherContext(null, MediaType.APPLICATION_JSON_TYPE, root); + enricher.enrich(context); + + assertThat(links.get("awesome").get("href").asText()).isEqualTo("/my/awesome/link"); + } + + private static class TestJsonEnricher extends JsonEnricherBase { + + public TestJsonEnricher(ObjectMapper objectMapper) { + super(objectMapper); + } + + @Override + public void enrich(JsonEnricherContext context) { + JsonNode gitConfigRefNode = createObject(singletonMap("href", value("/my/awesome/link"))); + + addPropertyNode(context.getResponseEntity().get("_links"), "awesome", gitConfigRefNode); + } + } + +} diff --git a/scm-core/src/test/java/sonia/scm/web/filter/PermissionFilterTest.java b/scm-core/src/test/java/sonia/scm/web/filter/PermissionFilterTest.java new file mode 100644 index 0000000000..9fa65d51b8 --- /dev/null +++ b/scm-core/src/test/java/sonia/scm/web/filter/PermissionFilterTest.java @@ -0,0 +1,74 @@ +package sonia.scm.web.filter; + +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; +import org.junit.Rule; +import org.junit.Test; +import sonia.scm.config.ScmConfiguration; +import sonia.scm.repository.Repository; +import sonia.scm.repository.spi.ScmProviderHttpServlet; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +@SubjectAware(configuration = "classpath:sonia/scm/shiro.ini") +public class PermissionFilterTest { + + public static final Repository REPOSITORY = new Repository("1", "git", "space", "name"); + + @Rule + public final ShiroRule shiroRule = new ShiroRule(); + + private final ScmProviderHttpServlet delegateServlet = mock(ScmProviderHttpServlet.class); + + private final PermissionFilter permissionFilter = new PermissionFilter(new ScmConfiguration(), delegateServlet) { + @Override + protected boolean isWriteRequest(HttpServletRequest request) { + return writeRequest; + } + }; + + private final HttpServletRequest request = mock(HttpServletRequest.class); + private final HttpServletResponse response = mock(HttpServletResponse.class); + + private boolean writeRequest = false; + + @Test + @SubjectAware(username = "reader", password = "secret") + public void shouldPassForReaderOnReadRequest() throws IOException, ServletException { + writeRequest = false; + + permissionFilter.service(request, response, REPOSITORY); + + verify(delegateServlet).service(request, response, REPOSITORY); + } + + @Test + @SubjectAware(username = "reader", password = "secret") + public void shouldBlockForReaderOnWriteRequest() throws IOException, ServletException { + writeRequest = true; + + permissionFilter.service(request, response, REPOSITORY); + + verify(response).sendError(eq(401), anyString()); + verify(delegateServlet, never()).service(request, response, REPOSITORY); + } + + @Test + @SubjectAware(username = "writer", password = "secret") + public void shouldPassForWriterOnWriteRequest() throws IOException, ServletException { + writeRequest = true; + + permissionFilter.service(request, response, REPOSITORY); + + verify(delegateServlet).service(request, response, REPOSITORY); + } +} diff --git a/scm-core/src/test/resources/sonia/scm/shiro.ini b/scm-core/src/test/resources/sonia/scm/shiro.ini index e87c81b097..fbdd35ba50 100644 --- a/scm-core/src/test/resources/sonia/scm/shiro.ini +++ b/scm-core/src/test/resources/sonia/scm/shiro.ini @@ -1,6 +1,12 @@ [users] trillian = secret, user +admin = secret, admin +writer = secret, repo_write +reader = secret, repo_read +unpriv = secret [roles] admin = * -user = something:* \ No newline at end of file +user = something:* +repo_read = "repository:read:1" +repo_write = "repository:push:1" diff --git a/scm-it/src/test/java/sonia/scm/it/IndexITCase.java b/scm-it/src/test/java/sonia/scm/it/IndexITCase.java new file mode 100644 index 0000000000..4a621d962f --- /dev/null +++ b/scm-it/src/test/java/sonia/scm/it/IndexITCase.java @@ -0,0 +1,48 @@ +package sonia.scm.it; + +import io.restassured.RestAssured; +import org.apache.http.HttpStatus; +import org.junit.Test; +import sonia.scm.it.utils.RestUtil; +import sonia.scm.web.VndMediaType; + +import static sonia.scm.it.utils.RegExMatcher.matchesPattern; +import static sonia.scm.it.utils.RestUtil.given; + +public class IndexITCase { + + @Test + public void shouldLinkEverythingForAdmin() { + given(VndMediaType.INDEX) + + .when() + .get(RestUtil.createResourceUrl("")) + + .then() + .statusCode(HttpStatus.SC_OK) + .body( + "_links.repositories.href", matchesPattern(".+/repositories/"), + "_links.users.href", matchesPattern(".+/users/"), + "_links.groups.href", matchesPattern(".+/groups/"), + "_links.config.href", matchesPattern(".+/config"), + "_links.gitConfig.href", matchesPattern(".+/config/git"), + "_links.hgConfig.href", matchesPattern(".+/config/hg"), + "_links.svnConfig.href", matchesPattern(".+/config/svn") + ); + } + + @Test + public void shouldCreateLoginLinksForAnonymousAccess() { + RestAssured.given() // do not specify user credentials + + .when() + .get(RestUtil.createResourceUrl("")) + + .then() + .statusCode(HttpStatus.SC_OK) + .body( + "_links.login.href", matchesPattern(".+/auth/.+") + ); + } + +} diff --git a/scm-it/src/test/java/sonia/scm/it/MeITCase.java b/scm-it/src/test/java/sonia/scm/it/MeITCase.java new file mode 100644 index 0000000000..64f06765ea --- /dev/null +++ b/scm-it/src/test/java/sonia/scm/it/MeITCase.java @@ -0,0 +1,65 @@ +package sonia.scm.it; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import sonia.scm.it.utils.ScmRequests; +import sonia.scm.it.utils.TestData; + +import static org.assertj.core.api.Assertions.assertThat; + +public class MeITCase { + + @Before + public void init() { + TestData.cleanup(); + } + + @Test + public void adminShouldChangeOwnPassword() { + String newPassword = TestData.USER_SCM_ADMIN + "1"; + // admin change the own password + ScmRequests.start() + .given() + .url(TestData.getMeUrl()) + .usernameAndPassword(TestData.USER_SCM_ADMIN, TestData.USER_SCM_ADMIN) + .getMeResource() + .assertStatusCode(200) + .usingMeResponse() + .assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE)) + .assertPassword(Assert::assertNull) + .assertType(s -> assertThat(s).isEqualTo("xml")) + .requestChangePassword(TestData.USER_SCM_ADMIN, newPassword) + .assertStatusCode(204); + // assert password is changed -> login with the new Password than undo changes + ScmRequests.start() + .given() + .url(TestData.getUserUrl(TestData.USER_SCM_ADMIN)) + .usernameAndPassword(TestData.USER_SCM_ADMIN, newPassword) + .getMeResource() + .assertStatusCode(200) + .usingMeResponse() + .assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE))// still admin + .requestChangePassword(newPassword, TestData.USER_SCM_ADMIN) + .assertStatusCode(204); + } + + @Test + public void shouldHidePasswordLinkIfUserTypeIsNotXML() { + String newUser = "user"; + String password = "pass"; + String type = "not XML Type"; + TestData.createUser(newUser, password, true, type); + ScmRequests.start() + .given() + .url(TestData.getMeUrl()) + .usernameAndPassword(newUser, password) + .getMeResource() + .assertStatusCode(200) + .usingMeResponse() + .assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE)) + .assertPassword(Assert::assertNull) + .assertType(s -> assertThat(s).isEqualTo(type)) + .assertPasswordLinkDoesNotExists(); + } +} diff --git a/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java b/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java index 63d8b46d47..f288d4891c 100644 --- a/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java +++ b/scm-it/src/test/java/sonia/scm/it/PermissionsITCase.java @@ -39,6 +39,8 @@ import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; +import sonia.scm.it.utils.RepositoryUtil; +import sonia.scm.it.utils.TestData; import sonia.scm.repository.PermissionType; import sonia.scm.repository.client.api.RepositoryClient; import sonia.scm.repository.client.api.RepositoryClientException; @@ -51,11 +53,11 @@ import java.util.Objects; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; -import static sonia.scm.it.RepositoryUtil.addAndCommitRandomFile; -import static sonia.scm.it.RestUtil.given; -import static sonia.scm.it.ScmTypes.availableScmTypes; -import static sonia.scm.it.TestData.USER_SCM_ADMIN; -import static sonia.scm.it.TestData.callRepository; +import static sonia.scm.it.utils.RepositoryUtil.addAndCommitRandomFile; +import static sonia.scm.it.utils.RestUtil.given; +import static sonia.scm.it.utils.ScmTypes.availableScmTypes; +import static sonia.scm.it.utils.TestData.USER_SCM_ADMIN; +import static sonia.scm.it.utils.TestData.callRepository; @RunWith(Parameterized.class) public class PermissionsITCase { diff --git a/scm-it/src/test/java/sonia/scm/it/RepositoriesITCase.java b/scm-it/src/test/java/sonia/scm/it/RepositoriesITCase.java index 21d4d97b1b..c49a65bea2 100644 --- a/scm-it/src/test/java/sonia/scm/it/RepositoriesITCase.java +++ b/scm-it/src/test/java/sonia/scm/it/RepositoriesITCase.java @@ -42,6 +42,8 @@ import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; +import sonia.scm.it.utils.RepositoryUtil; +import sonia.scm.it.utils.TestData; import sonia.scm.repository.client.api.RepositoryClient; import sonia.scm.web.VndMediaType; @@ -53,11 +55,11 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; import static org.junit.Assert.assertEquals; -import static sonia.scm.it.RegExMatcher.matchesPattern; -import static sonia.scm.it.RestUtil.createResourceUrl; -import static sonia.scm.it.RestUtil.given; -import static sonia.scm.it.ScmTypes.availableScmTypes; -import static sonia.scm.it.TestData.repositoryJson; +import static sonia.scm.it.utils.RegExMatcher.matchesPattern; +import static sonia.scm.it.utils.RestUtil.createResourceUrl; +import static sonia.scm.it.utils.RestUtil.given; +import static sonia.scm.it.utils.ScmTypes.availableScmTypes; +import static sonia.scm.it.utils.TestData.repositoryJson; @RunWith(Parameterized.class) public class RepositoriesITCase { diff --git a/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java b/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java index 399dd26035..3f8832a3f5 100644 --- a/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java +++ b/scm-it/src/test/java/sonia/scm/it/RepositoryAccessITCase.java @@ -3,6 +3,7 @@ package sonia.scm.it; import io.restassured.response.ExtractableResponse; import io.restassured.response.Response; import org.apache.http.HttpStatus; +import org.assertj.core.util.Lists; import org.junit.Assume; import org.junit.Before; import org.junit.Rule; @@ -10,6 +11,9 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; +import sonia.scm.it.utils.RepositoryUtil; +import sonia.scm.it.utils.ScmRequests; +import sonia.scm.it.utils.TestData; import sonia.scm.repository.Changeset; import sonia.scm.repository.client.api.ClientCommand; import sonia.scm.repository.client.api.RepositoryClient; @@ -17,17 +21,20 @@ import sonia.scm.web.VndMediaType; import java.io.File; import java.io.IOException; +import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; import java.util.List; +import java.util.Map; import static java.lang.Thread.sleep; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertNotNull; -import static sonia.scm.it.RestUtil.ADMIN_PASSWORD; -import static sonia.scm.it.RestUtil.ADMIN_USERNAME; -import static sonia.scm.it.RestUtil.given; -import static sonia.scm.it.ScmTypes.availableScmTypes; +import static sonia.scm.it.utils.RestUtil.ADMIN_PASSWORD; +import static sonia.scm.it.utils.RestUtil.ADMIN_USERNAME; +import static sonia.scm.it.utils.RestUtil.given; +import static sonia.scm.it.utils.ScmTypes.availableScmTypes; @RunWith(Parameterized.class) public class RepositoryAccessITCase { @@ -37,6 +44,7 @@ public class RepositoryAccessITCase { private final String repositoryType; private File folder; + private ScmRequests.AppliedRepositoryRequest repositoryGetRequest; public RepositoryAccessITCase(String repositoryType) { this.repositoryType = repositoryType; @@ -48,9 +56,20 @@ public class RepositoryAccessITCase { } @Before - public void initClient() { + public void init() { TestData.createDefault(); folder = tempFolder.getRoot(); + repositoryGetRequest = ScmRequests.start() + .given() + .url(TestData.getDefaultRepositoryUrl(repositoryType)) + .usernameAndPassword(ADMIN_USERNAME, ADMIN_PASSWORD) + .getRepositoryResource() + .assertStatusCode(HttpStatus.SC_OK); + ScmRequests.AppliedMeRequest meGetRequest = ScmRequests.start() + .given() + .url(TestData.getMeUrl()) + .usernameAndPassword(ADMIN_USERNAME, ADMIN_PASSWORD) + .getMeResource(); } @Test @@ -153,7 +172,7 @@ public class RepositoryAccessITCase { .isNotNull() .contains(String.format("%s/sources/%s", repositoryUrl, changeset.getId())); - assertThat(response.body().jsonPath().getString("_embedded.tags.find{it.name=='" + tagName + "'}._links.changesets.href")) + assertThat(response.body().jsonPath().getString("_embedded.tags.find{it.name=='" + tagName + "'}._links.changeset.href")) .as("assert single tag changesets link") .isNotNull() .contains(String.format("%s/changesets/%s", repositoryUrl, changeset.getId())); @@ -281,5 +300,187 @@ public class RepositoryAccessITCase { .contains("diff"); } + + @Test + @SuppressWarnings("unchecked") + public void shouldFindFileHistory() throws IOException { + RepositoryClient repositoryClient = RepositoryUtil.createRepositoryClient(repositoryType, folder); + Changeset changeset = RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, "folder/subfolder/a.txt", "a"); + repositoryGetRequest + .usingRepositoryResponse() + .requestSources() + .usingSourcesResponse() + .requestSelf("folder") + .usingSourcesResponse() + .requestSelf("subfolder") + .usingSourcesResponse() + .requestFileHistory("a.txt") + .assertStatusCode(HttpStatus.SC_OK) + .usingChangesetsResponse() + .assertChangesets(changesets -> { + assertThat(changesets).hasSize(1); + assertThat(changesets.get(0)).containsEntry("id", changeset.getId()); + assertThat(changesets.get(0)).containsEntry("description", changeset.getDescription()); + } + ); + } + + @Test + @SuppressWarnings("unchecked") + public void shouldFindAddedModifications() throws IOException { + RepositoryClient repositoryClient = RepositoryUtil.createRepositoryClient(repositoryType, folder); + String fileName = "a.txt"; + Changeset changeset = RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, fileName, "a"); + String revision = changeset.getId(); + repositoryGetRequest + .usingRepositoryResponse() + .requestChangesets() + .assertStatusCode(HttpStatus.SC_OK) + .usingChangesetsResponse() + .requestModifications(revision) + .assertStatusCode(HttpStatus.SC_OK) + .usingModificationsResponse() + .assertRevision(actualRevision -> assertThat(actualRevision).isEqualTo(revision)) + .assertAdded(addedFiles -> assertThat(addedFiles) + .hasSize(1) + .containsExactly(fileName)) + .assertRemoved(removedFiles -> assertThat(removedFiles) + .hasSize(0)) + .assertModified(files -> assertThat(files) + .hasSize(0)); + } + + @Test + @SuppressWarnings("unchecked") + public void shouldFindRemovedModifications() throws IOException { + RepositoryClient repositoryClient = RepositoryUtil.createRepositoryClient(repositoryType, folder); + String fileName = "a.txt"; + RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, fileName, "a"); + Changeset changeset = RepositoryUtil.removeAndCommitFile(repositoryClient, ADMIN_USERNAME, fileName); + + String revision = changeset.getId(); + repositoryGetRequest + .usingRepositoryResponse() + .requestChangesets() + .assertStatusCode(HttpStatus.SC_OK) + .usingChangesetsResponse() + .requestModifications(revision) + .assertStatusCode(HttpStatus.SC_OK) + .usingModificationsResponse() + .assertRevision(actualRevision -> assertThat(actualRevision).isEqualTo(revision)) + .assertRemoved(removedFiles -> assertThat(removedFiles) + .hasSize(1) + .containsExactly(fileName)) + .assertAdded(addedFiles -> assertThat(addedFiles) + .hasSize(0)) + .assertModified(files -> assertThat(files) + .hasSize(0)); + } + + @Test + @SuppressWarnings("unchecked") + public void shouldFindUpdateModifications() throws IOException { + RepositoryClient repositoryClient = RepositoryUtil.createRepositoryClient(repositoryType, folder); + String fileName = "a.txt"; + RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, fileName, "a"); + Changeset changeset = RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, fileName, "new Content"); + + String revision = changeset.getId(); + repositoryGetRequest + .usingRepositoryResponse() + .requestChangesets() + .assertStatusCode(HttpStatus.SC_OK) + .usingChangesetsResponse() + .requestModifications(revision) + .assertStatusCode(HttpStatus.SC_OK) + .usingModificationsResponse() + .assertRevision(actualRevision -> assertThat(actualRevision).isEqualTo(revision)) + .assertModified(modifiedFiles -> assertThat(modifiedFiles) + .hasSize(1) + .containsExactly(fileName)) + .assertRemoved(removedFiles -> assertThat(removedFiles) + .hasSize(0)) + .assertAdded(addedFiles -> assertThat(addedFiles) + .hasSize(0)); + } + + @Test + @SuppressWarnings("unchecked") + public void shouldFindMultipleModifications() throws IOException { + RepositoryClient repositoryClient = RepositoryUtil.createRepositoryClient(repositoryType, folder); + RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, "b.txt", "b"); + RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, "c.txt", "c"); + RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, "d.txt", "d"); + Map addedFiles = new HashMap() + {{ + put("a.txt", "bla bla"); + }}; + Map modifiedFiles = new HashMap() + {{ + put("b.txt", "new content"); + }}; + ArrayList removedFiles = Lists.newArrayList("c.txt", "d.txt"); + Changeset changeset = RepositoryUtil.commitMultipleFileModifications(repositoryClient, ADMIN_USERNAME, addedFiles, modifiedFiles, removedFiles); + + String revision = changeset.getId(); + repositoryGetRequest + .usingRepositoryResponse() + .requestChangesets() + .assertStatusCode(HttpStatus.SC_OK) + .usingChangesetsResponse() + .requestModifications(revision) + .assertStatusCode(HttpStatus.SC_OK) + .usingModificationsResponse() + .assertRevision(actualRevision -> assertThat(actualRevision).isEqualTo(revision)) + .assertAdded(a -> assertThat(a) + .hasSize(1) + .containsExactly("a.txt")) + .assertModified(m-> assertThat(m) + .hasSize(1) + .containsExactly("b.txt")) + .assertRemoved(r -> assertThat(r) + .hasSize(2) + .containsExactly("c.txt", "d.txt")); + } + + @Test + @SuppressWarnings("unchecked") + public void svnShouldCreateOneModificationPerFolder() throws IOException { + Assume.assumeThat(repositoryType, equalTo("svn")); + RepositoryClient repositoryClient = RepositoryUtil.createRepositoryClient(repositoryType, folder); + RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, "bbb/bb/b.txt", "b"); + RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, "ccc/cc/c.txt", "c"); + RepositoryUtil.createAndCommitFile(repositoryClient, ADMIN_USERNAME, "ddd/dd/d.txt", "d"); + Map addedFiles = new HashMap() + {{ + put("aaa/aa/a.txt", "bla bla"); + }}; + Map modifiedFiles = new HashMap() + {{ + put("bbb/bb/b.txt", "new content"); + }}; + ArrayList removedFiles = Lists.newArrayList("ccc/cc/c.txt", "ddd/dd/d.txt"); + Changeset changeset = RepositoryUtil.commitMultipleFileModifications(repositoryClient, ADMIN_USERNAME, addedFiles, modifiedFiles, removedFiles); + + String revision = changeset.getId(); + repositoryGetRequest + .usingRepositoryResponse() + .requestChangesets() + .assertStatusCode(HttpStatus.SC_OK) + .usingChangesetsResponse() + .requestModifications(revision) + .assertStatusCode(HttpStatus.SC_OK) + .usingModificationsResponse() + .assertRevision(actualRevision -> assertThat(actualRevision).isEqualTo(revision)) + .assertAdded(a -> assertThat(a) + .hasSize(3) + .containsExactly("aaa/aa/a.txt", "aaa", "aaa/aa")) + .assertModified(m-> assertThat(m) + .hasSize(1) + .containsExactly("bbb/bb/b.txt")) + .assertRemoved(r -> assertThat(r) + .hasSize(2) + .containsExactly("ccc/cc/c.txt", "ddd/dd/d.txt")); + } } diff --git a/scm-it/src/test/java/sonia/scm/it/UserITCase.java b/scm-it/src/test/java/sonia/scm/it/UserITCase.java new file mode 100644 index 0000000000..33fbe0cc5d --- /dev/null +++ b/scm-it/src/test/java/sonia/scm/it/UserITCase.java @@ -0,0 +1,96 @@ +package sonia.scm.it; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import sonia.scm.it.utils.ScmRequests; +import sonia.scm.it.utils.TestData; + +import static org.assertj.core.api.Assertions.assertThat; + +public class UserITCase { + + @Before + public void init(){ + TestData.cleanup(); + } + + @Test + public void adminShouldChangeOwnPassword() { + String newUser = "user"; + String password = "pass"; + TestData.createUser(newUser, password, true, "xml"); + String newPassword = "new_password"; + // admin change the own password + ScmRequests.start() + .given() + .url(TestData.getUserUrl(newUser)) + .usernameAndPassword(newUser, password) + .getUserResource() + .assertStatusCode(200) + .usingUserResponse() + .assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE)) + .assertPassword(Assert::assertNull) + .requestChangePassword(newPassword) // the oldPassword is not needed in the user resource + .assertStatusCode(204); + // assert password is changed -> login with the new Password + ScmRequests.start() + .given() + .url(TestData.getUserUrl(newUser)) + .usernameAndPassword(newUser, newPassword) + .getUserResource() + .assertStatusCode(200) + .usingUserResponse() + .assertAdmin(isAdmin -> assertThat(isAdmin).isEqualTo(Boolean.TRUE)) + .assertPassword(Assert::assertNull); + + } + + @Test + public void adminShouldChangePasswordOfOtherUser() { + String newUser = "user"; + String password = "pass"; + TestData.createUser(newUser, password, true, "xml"); + String newPassword = "new_password"; + // admin change the password of the user + ScmRequests.start() + .given() + .url(TestData.getUserUrl(newUser))// the admin get the user object + .usernameAndPassword(TestData.USER_SCM_ADMIN, TestData.USER_SCM_ADMIN) + .getUserResource() + .assertStatusCode(200) + .usingUserResponse() + .assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE)) // the user anonymous is not an admin + .assertPassword(Assert::assertNull) + .requestChangePassword(newPassword) // the oldPassword is not needed in the user resource + .assertStatusCode(204); + // assert password is changed + ScmRequests.start() + .given() + .url(TestData.getUserUrl(newUser)) + .usernameAndPassword(newUser, newPassword) + .getUserResource() + .assertStatusCode(200); + + } + + + @Test + public void shouldHidePasswordLinkIfUserTypeIsNotXML() { + String newUser = "user"; + String password = "pass"; + String type = "not XML Type"; + TestData.createUser(newUser, password, true, type); + ScmRequests.start() + .given() + .url(TestData.getMeUrl()) + .usernameAndPassword(newUser, password) + .getUserResource() + .assertStatusCode(200) + .usingUserResponse() + .assertAdmin(aBoolean -> assertThat(aBoolean).isEqualTo(Boolean.TRUE)) + .assertPassword(Assert::assertNull) + .assertType(s -> assertThat(s).isEqualTo(type)) + .assertPasswordLinkDoesNotExists(); + } +} diff --git a/scm-it/src/test/java/sonia/scm/it/utils/NullAwareJsonObjectBuilder.java b/scm-it/src/test/java/sonia/scm/it/utils/NullAwareJsonObjectBuilder.java new file mode 100644 index 0000000000..31a12f1969 --- /dev/null +++ b/scm-it/src/test/java/sonia/scm/it/utils/NullAwareJsonObjectBuilder.java @@ -0,0 +1,95 @@ +package sonia.scm.it.utils; + +import javax.json.JsonArrayBuilder; +import javax.json.JsonObject; +import javax.json.JsonObjectBuilder; +import javax.json.JsonValue; +import java.math.BigDecimal; +import java.math.BigInteger; + +public class NullAwareJsonObjectBuilder implements JsonObjectBuilder { + public static JsonObjectBuilder wrap(JsonObjectBuilder builder) { + if (builder == null) { + throw new IllegalArgumentException("Can't wrap nothing."); + } + return new NullAwareJsonObjectBuilder(builder); + } + + private final JsonObjectBuilder builder; + + private NullAwareJsonObjectBuilder(JsonObjectBuilder builder) { + this.builder = builder; + } + + public JsonObjectBuilder add(String name, JsonValue value) { + return builder.add(name, (value == null) ? JsonValue.NULL : value); + } + + @Override + public JsonObjectBuilder add(String name, String value) { + if (value != null){ + return builder.add(name, value ); + }else{ + return builder.addNull(name); + } + } + + @Override + public JsonObjectBuilder add(String name, BigInteger value) { + if (value != null){ + return builder.add(name, value ); + }else{ + return builder.addNull(name); + } + } + + @Override + public JsonObjectBuilder add(String name, BigDecimal value) { + if (value != null){ + return builder.add(name, value ); + }else{ + return builder.addNull(name); + } + } + + @Override + public JsonObjectBuilder add(String s, int i) { + return builder.add(s, i); + } + + @Override + public JsonObjectBuilder add(String s, long l) { + return builder.add(s, l); + } + + @Override + public JsonObjectBuilder add(String s, double v) { + return builder.add(s, v); + } + + @Override + public JsonObjectBuilder add(String s, boolean b) { + return builder.add(s, b); + } + + @Override + public JsonObjectBuilder addNull(String s) { + return builder.addNull(s); + } + + @Override + public JsonObjectBuilder add(String s, JsonObjectBuilder jsonObjectBuilder) { + return builder.add(s, jsonObjectBuilder); + } + + @Override + public JsonObjectBuilder add(String s, JsonArrayBuilder jsonArrayBuilder) { + return builder.add(s, jsonArrayBuilder); + } + + @Override + public JsonObject build() { + return builder.build(); + } + +} diff --git a/scm-it/src/test/java/sonia/scm/it/RegExMatcher.java b/scm-it/src/test/java/sonia/scm/it/utils/RegExMatcher.java similarity index 76% rename from scm-it/src/test/java/sonia/scm/it/RegExMatcher.java rename to scm-it/src/test/java/sonia/scm/it/utils/RegExMatcher.java index e5dc7931d3..8fb9fdf798 100644 --- a/scm-it/src/test/java/sonia/scm/it/RegExMatcher.java +++ b/scm-it/src/test/java/sonia/scm/it/utils/RegExMatcher.java @@ -1,4 +1,4 @@ -package sonia.scm.it; +package sonia.scm.it.utils; import org.hamcrest.BaseMatcher; import org.hamcrest.Description; @@ -6,7 +6,7 @@ import org.hamcrest.Matcher; import java.util.regex.Pattern; -class RegExMatcher extends BaseMatcher { +public class RegExMatcher extends BaseMatcher { public static Matcher matchesPattern(String pattern) { return new RegExMatcher(pattern); } @@ -24,6 +24,6 @@ class RegExMatcher extends BaseMatcher { @Override public boolean matches(Object o) { - return Pattern.compile(pattern).matcher(o.toString()).matches(); + return o != null && Pattern.compile(pattern).matcher(o.toString()).matches(); } } diff --git a/scm-it/src/test/java/sonia/scm/it/RepositoryUtil.java b/scm-it/src/test/java/sonia/scm/it/utils/RepositoryUtil.java similarity index 50% rename from scm-it/src/test/java/sonia/scm/it/RepositoryUtil.java rename to scm-it/src/test/java/sonia/scm/it/utils/RepositoryUtil.java index 442856eefa..11db0200f1 100644 --- a/scm-it/src/test/java/sonia/scm/it/RepositoryUtil.java +++ b/scm-it/src/test/java/sonia/scm/it/utils/RepositoryUtil.java @@ -1,4 +1,4 @@ -package sonia.scm.it; +package sonia.scm.it.utils; import com.google.common.base.Charsets; import com.google.common.io.Files; @@ -14,6 +14,8 @@ import sonia.scm.repository.client.api.RepositoryClientFactory; import java.io.File; import java.io.IOException; +import java.util.List; +import java.util.Map; import java.util.UUID; public class RepositoryUtil { @@ -22,30 +24,73 @@ public class RepositoryUtil { private static final RepositoryClientFactory REPOSITORY_CLIENT_FACTORY = new RepositoryClientFactory(); - static RepositoryClient createRepositoryClient(String repositoryType, File folder) throws IOException { + public static RepositoryClient createRepositoryClient(String repositoryType, File folder) throws IOException { return createRepositoryClient(repositoryType, folder, "scmadmin", "scmadmin"); } - static RepositoryClient createRepositoryClient(String repositoryType, File folder, String username, String password) throws IOException { + public static RepositoryClient createRepositoryClient(String repositoryType, File folder, String username, String password) throws IOException { String httpProtocolUrl = TestData.callRepository(username, password, repositoryType, HttpStatus.SC_OK) .extract() - .path("_links.httpProtocol.href"); + .path("_links.protocol.find{it.name=='http'}.href"); return REPOSITORY_CLIENT_FACTORY.create(repositoryType, httpProtocolUrl, username, password, folder); } - static String addAndCommitRandomFile(RepositoryClient client, String username) throws IOException { + public static String addAndCommitRandomFile(RepositoryClient client, String username) throws IOException { String uuid = UUID.randomUUID().toString(); String name = "file-" + uuid + ".uuid"; createAndCommitFile(client, username, name, uuid); return name; } - static Changeset createAndCommitFile(RepositoryClient repositoryClient, String username, String fileName, String content) throws IOException { + public static Changeset createAndCommitFile(RepositoryClient repositoryClient, String username, String fileName, String content) throws IOException { + writeAndAddFile(repositoryClient, fileName, content); + return commit(repositoryClient, username, "added " + fileName); + } + + /** + * Bundle multiple File modification in one changeset + * + * @param repositoryClient + * @param username + * @param addedFiles map.key: path of the file, value: the file content + * @param modifiedFiles map.key: path of the file, value: the file content + * @param removedFiles list of file paths to be removed + * @return the changeset with all modifications + * @throws IOException + */ + public static Changeset commitMultipleFileModifications(RepositoryClient repositoryClient, String username, Map addedFiles, Map modifiedFiles, List removedFiles) throws IOException { + for (String fileName : addedFiles.keySet()) { + writeAndAddFile(repositoryClient, fileName, addedFiles.get(fileName)); + } + for (String fileName : modifiedFiles.keySet()) { + writeAndAddFile(repositoryClient, fileName, modifiedFiles.get(fileName)); + } + for (String fileName : removedFiles) { + deleteFileAndApplyRemoveCommand(repositoryClient, fileName); + } + return commit(repositoryClient, username, "multiple file modifications" ); + } + + private static File writeAndAddFile(RepositoryClient repositoryClient, String fileName, String content) throws IOException { File file = new File(repositoryClient.getWorkingCopy(), fileName); + Files.createParentDirs(file); Files.write(content, file, Charsets.UTF_8); addWithParentDirectories(repositoryClient, file); - return commit(repositoryClient, username, "added " + fileName); + return file; + } + + public static Changeset removeAndCommitFile(RepositoryClient repositoryClient, String username, String fileName) throws IOException { + deleteFileAndApplyRemoveCommand(repositoryClient, fileName); + return commit(repositoryClient, username, "removed " + fileName); + } + + private static void deleteFileAndApplyRemoveCommand(RepositoryClient repositoryClient, String fileName) throws IOException { + File file = new File(repositoryClient.getWorkingCopy(), fileName); + if (repositoryClient.isCommandSupported(ClientCommand.REMOVE)) { + repositoryClient.getRemoveCommand().remove(fileName); + } + file.delete(); } private static String addWithParentDirectories(RepositoryClient repositoryClient, File file) throws IOException { @@ -53,7 +98,6 @@ public class RepositoryUtil { String thisName = file.getName(); String path; if (!repositoryClient.getWorkingCopy().equals(parent)) { - addWithParentDirectories(repositoryClient, parent); path = addWithParentDirectories(repositoryClient, parent) + File.separator + thisName; } else { path = thisName; @@ -71,7 +115,7 @@ public class RepositoryUtil { return changeset; } - static Tag addTag(RepositoryClient repositoryClient, String revision, String tagName) throws IOException { + public static Tag addTag(RepositoryClient repositoryClient, String revision, String tagName) throws IOException { if (repositoryClient.isCommandSupported(ClientCommand.TAG)) { Tag tag = repositoryClient.getTagCommand().setRevision(revision).tag(tagName, TestData.USER_SCM_ADMIN); if (repositoryClient.isCommandSupported(ClientCommand.PUSH)) { diff --git a/scm-it/src/test/java/sonia/scm/it/RestUtil.java b/scm-it/src/test/java/sonia/scm/it/utils/RestUtil.java similarity index 97% rename from scm-it/src/test/java/sonia/scm/it/RestUtil.java rename to scm-it/src/test/java/sonia/scm/it/utils/RestUtil.java index a7409e1995..c8b01a6d72 100644 --- a/scm-it/src/test/java/sonia/scm/it/RestUtil.java +++ b/scm-it/src/test/java/sonia/scm/it/utils/RestUtil.java @@ -1,4 +1,4 @@ -package sonia.scm.it; +package sonia.scm.it.utils; import io.restassured.RestAssured; import io.restassured.specification.RequestSpecification; diff --git a/scm-it/src/test/java/sonia/scm/it/utils/ScmRequests.java b/scm-it/src/test/java/sonia/scm/it/utils/ScmRequests.java new file mode 100644 index 0000000000..41fd9a1290 --- /dev/null +++ b/scm-it/src/test/java/sonia/scm/it/utils/ScmRequests.java @@ -0,0 +1,465 @@ +package sonia.scm.it.utils; + +import io.restassured.RestAssured; +import io.restassured.response.Response; +import sonia.scm.web.VndMediaType; + +import java.net.URI; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +import static org.hamcrest.Matchers.is; +import static sonia.scm.it.utils.TestData.createPasswordChangeJson; + + +/** + * Encapsulate rest requests of a repository in builder pattern + *

+ * A Get Request can be applied with the methods request*() + * These methods return a AppliedGet*Request object + * This object can be used to apply general assertions over the rest Assured response + * In the AppliedGet*Request classes there is a using*Response() method + * that return the *Response class containing specific operations related to the specific response + * the *Response class contains also the request*() method to apply the next GET request from a link in the response. + */ +public class ScmRequests { + + private String url; + private String username; + private String password; + + public static ScmRequests start() { + return new ScmRequests(); + } + + public Given given() { + return new Given(); + } + + + /** + * Apply a GET Request to the extracted url from the given link + * + * @param linkPropertyName the property name of link + * @param response the response containing the link + * @return the response of the GET request using the given link + */ + private Response applyGETRequestFromLink(Response response, String linkPropertyName) { + return applyGETRequest(response + .then() + .extract() + .path(linkPropertyName)); + } + + + /** + * Apply a GET Request to the given url and return the response. + * + * @param url the url of the GET request + * @return the response of the GET request using the given url + */ + private Response applyGETRequest(String url) { + return RestAssured.given() + .auth().preemptive().basic(username, password) + .when() + .get(url); + } + + + /** + * Apply a PUT Request to the extracted url from the given link + * + * @param response the response containing the link + * @param linkPropertyName the property name of link + * @param body + * @return the response of the PUT request using the given link + */ + private Response applyPUTRequestFromLink(Response response, String linkPropertyName, String content, String body) { + return applyPUTRequest(response + .then() + .extract() + .path(linkPropertyName), content, body); + } + + + /** + * Apply a PUT Request to the given url and return the response. + * + * @param url the url of the PUT request + * @param mediaType + * @param body + * @return the response of the PUT request using the given url + */ + private Response applyPUTRequest(String url, String mediaType, String body) { + return RestAssured.given() + .auth().preemptive().basic(username, password) + .when() + .contentType(mediaType) + .accept(mediaType) + .body(body) + .put(url); + } + + + private void setUrl(String url) { + this.url = url; + } + + private void setUsername(String username) { + this.username = username; + } + + private void setPassword(String password) { + this.password = password; + } + + private String getUrl() { + return url; + } + + private String getUsername() { + return username; + } + + private String getPassword() { + return password; + } + + public class Given { + + public GivenUrl url(String url) { + setUrl(url); + return new GivenUrl(); + } + + public GivenUrl url(URI url) { + setUrl(url.toString()); + return new GivenUrl(); + } + + } + + public class GivenWithUrlAndAuth { + public AppliedMeRequest getMeResource() { + return new AppliedMeRequest(applyGETRequest(url)); + } + + public AppliedUserRequest getUserResource() { + return new AppliedUserRequest(applyGETRequest(url)); + } + + public AppliedRepositoryRequest getRepositoryResource() { + return new AppliedRepositoryRequest( + applyGETRequest(url) + ); + } + } + + public class AppliedRequest { + private Response response; + + public AppliedRequest(Response response) { + this.response = response; + } + + /** + * apply custom assertions to the actual response + * + * @param consumer consume the response in order to assert the content. the header, the payload etc.. + * @return the self object + */ + public SELF assertResponse(Consumer consumer) { + consumer.accept(response); + return (SELF) this; + } + + /** + * special assertion of the status code + * + * @param expectedStatusCode the expected status code + * @return the self object + */ + public SELF assertStatusCode(int expectedStatusCode) { + this.response.then().assertThat().statusCode(expectedStatusCode); + return (SELF) this; + } + + } + + public class AppliedRepositoryRequest extends AppliedRequest { + + public AppliedRepositoryRequest(Response response) { + super(response); + } + + public RepositoryResponse usingRepositoryResponse() { + return new RepositoryResponse(super.response); + } + } + + public class RepositoryResponse { + + private Response repositoryResponse; + + public RepositoryResponse(Response repositoryResponse) { + this.repositoryResponse = repositoryResponse; + } + + public AppliedSourcesRequest requestSources() { + return new AppliedSourcesRequest(applyGETRequestFromLink(repositoryResponse, "_links.sources.href")); + } + + public AppliedChangesetsRequest requestChangesets() { + return new AppliedChangesetsRequest(applyGETRequestFromLink(repositoryResponse, "_links.changesets.href")); + } + } + + public class AppliedChangesetsRequest extends AppliedRequest { + + public AppliedChangesetsRequest(Response response) { + super(response); + } + + public ChangesetsResponse usingChangesetsResponse() { + return new ChangesetsResponse(super.response); + } + } + + public class ChangesetsResponse { + private Response changesetsResponse; + + public ChangesetsResponse(Response changesetsResponse) { + this.changesetsResponse = changesetsResponse; + } + + public ChangesetsResponse assertChangesets(Consumer> changesetsConsumer) { + List changesets = changesetsResponse.then().extract().path("_embedded.changesets"); + changesetsConsumer.accept(changesets); + return this; + } + + public AppliedDiffRequest requestDiff(String revision) { + return new AppliedDiffRequest(applyGETRequestFromLink(changesetsResponse, "_embedded.changesets.find{it.id=='" + revision + "'}._links.diff.href")); + } + + public AppliedModificationsRequest requestModifications(String revision) { + return new AppliedModificationsRequest(applyGETRequestFromLink(changesetsResponse, "_embedded.changesets.find{it.id=='" + revision + "'}._links.modifications.href")); + } + } + + public class AppliedSourcesRequest extends AppliedRequest { + + public AppliedSourcesRequest(Response sourcesResponse) { + super(sourcesResponse); + } + + public SourcesResponse usingSourcesResponse() { + return new SourcesResponse(super.response); + } + } + + public class SourcesResponse { + + private Response sourcesResponse; + + public SourcesResponse(Response sourcesResponse) { + this.sourcesResponse = sourcesResponse; + } + + public SourcesResponse assertRevision(Consumer assertRevision) { + String revision = sourcesResponse.then().extract().path("revision"); + assertRevision.accept(revision); + return this; + } + + public SourcesResponse assertFiles(Consumer assertFiles) { + List files = sourcesResponse.then().extract().path("files"); + assertFiles.accept(files); + return this; + } + + public AppliedChangesetsRequest requestFileHistory(String fileName) { + return new AppliedChangesetsRequest(applyGETRequestFromLink(sourcesResponse, "_embedded.files.find{it.name=='" + fileName + "'}._links.history.href")); + } + + public AppliedSourcesRequest requestSelf(String fileName) { + return new AppliedSourcesRequest(applyGETRequestFromLink(sourcesResponse, "_embedded.files.find{it.name=='" + fileName + "'}._links.self.href")); + } + } + + public class AppliedDiffRequest extends AppliedRequest { + + public AppliedDiffRequest(Response response) { + super(response); + } + } + + public class GivenUrl { + + public GivenWithUrlAndAuth usernameAndPassword(String username, String password) { + setUsername(username); + setPassword(password); + return new GivenWithUrlAndAuth(); + } + } + + public class AppliedModificationsRequest extends AppliedRequest { + public AppliedModificationsRequest(Response response) { + super(response); + } + + public ModificationsResponse usingModificationsResponse() { + return new ModificationsResponse(super.response); + } + + } + + public class ModificationsResponse { + private Response resource; + + public ModificationsResponse(Response resource) { + this.resource = resource; + } + + public ModificationsResponse assertRevision(Consumer assertRevision) { + String revision = resource.then().extract().path("revision"); + assertRevision.accept(revision); + return this; + } + + public ModificationsResponse assertAdded(Consumer> assertAdded) { + List added = resource.then().extract().path("added"); + assertAdded.accept(added); + return this; + } + + public ModificationsResponse assertRemoved(Consumer> assertRemoved) { + List removed = resource.then().extract().path("removed"); + assertRemoved.accept(removed); + return this; + } + + public ModificationsResponse assertModified(Consumer> assertModified) { + List modified = resource.then().extract().path("modified"); + assertModified.accept(modified); + return this; + } + + } + + public class AppliedMeRequest extends AppliedRequest { + + public AppliedMeRequest(Response response) { + super(response); + } + + public MeResponse usingMeResponse() { + return new MeResponse(super.response); + } + + } + + public class MeResponse extends UserResponse { + + + public MeResponse(Response response) { + super(response); + } + + public AppliedChangePasswordRequest requestChangePassword(String oldPassword, String newPassword) { + return new AppliedChangePasswordRequest(applyPUTRequestFromLink(super.response, "_links.password.href", VndMediaType.PASSWORD_CHANGE, createPasswordChangeJson(oldPassword, newPassword))); + } + + + } + + public class UserResponse extends ModelResponse { + + public static final String LINKS_PASSWORD_HREF = "_links.password.href"; + + public UserResponse(Response response) { + super(response); + } + + public SELF assertPassword(Consumer assertPassword) { + return super.assertSingleProperty(assertPassword, "password"); + } + + public SELF assertType(Consumer assertType) { + return assertSingleProperty(assertType, "type"); + } + + public SELF assertAdmin(Consumer assertAdmin) { + return assertSingleProperty(assertAdmin, "admin"); + } + + public SELF assertPasswordLinkDoesNotExists() { + return assertPropertyPathDoesNotExists(LINKS_PASSWORD_HREF); + } + + public SELF assertPasswordLinkExists() { + return assertPropertyPathExists(LINKS_PASSWORD_HREF); + } + + public AppliedChangePasswordRequest requestChangePassword(String newPassword) { + return new AppliedChangePasswordRequest(applyPUTRequestFromLink(super.response, LINKS_PASSWORD_HREF, VndMediaType.PASSWORD_CHANGE, createPasswordChangeJson(null, newPassword))); + } + + } + + + /** + * encapsulate standard assertions over model properties + */ + public class ModelResponse { + + protected Response response; + + public ModelResponse(Response response) { + this.response = response; + } + + public SELF assertSingleProperty(Consumer assertSingleProperty, String propertyJsonPath) { + T propertyValue = response.then().extract().path(propertyJsonPath); + assertSingleProperty.accept(propertyValue); + return (SELF) this; + } + + public SELF assertPropertyPathExists(String propertyJsonPath) { + response.then().assertThat().body("any { it.containsKey('" + propertyJsonPath + "')}", is(true)); + return (SELF) this; + } + + public SELF assertPropertyPathDoesNotExists(String propertyJsonPath) { + response.then().assertThat().body("this.any { it.containsKey('" + propertyJsonPath + "')}", is(false)); + return (SELF) this; + } + + public SELF assertArrayProperty(Consumer assertProperties, String propertyJsonPath) { + List properties = response.then().extract().path(propertyJsonPath); + assertProperties.accept(properties); + return (SELF) this; + } + } + + public class AppliedChangePasswordRequest extends AppliedRequest { + + public AppliedChangePasswordRequest(Response response) { + super(response); + } + + } + + public class AppliedUserRequest extends AppliedRequest { + + public AppliedUserRequest(Response response) { + super(response); + } + + public UserResponse usingUserResponse() { + return new UserResponse(super.response); + } + + } +} diff --git a/scm-it/src/test/java/sonia/scm/it/ScmTypes.java b/scm-it/src/test/java/sonia/scm/it/utils/ScmTypes.java similarity index 72% rename from scm-it/src/test/java/sonia/scm/it/ScmTypes.java rename to scm-it/src/test/java/sonia/scm/it/utils/ScmTypes.java index e8ba67e561..4c9ac0ea44 100644 --- a/scm-it/src/test/java/sonia/scm/it/ScmTypes.java +++ b/scm-it/src/test/java/sonia/scm/it/utils/ScmTypes.java @@ -1,12 +1,12 @@ -package sonia.scm.it; +package sonia.scm.it.utils; import sonia.scm.util.IOUtil; import java.util.ArrayList; import java.util.Collection; -class ScmTypes { - static Collection availableScmTypes() { +public class ScmTypes { + public static Collection availableScmTypes() { Collection params = new ArrayList<>(); params.add("git"); diff --git a/scm-it/src/test/java/sonia/scm/it/TestData.java b/scm-it/src/test/java/sonia/scm/it/utils/TestData.java similarity index 77% rename from scm-it/src/test/java/sonia/scm/it/TestData.java rename to scm-it/src/test/java/sonia/scm/it/utils/TestData.java index ae0e35004d..03da80ea3b 100644 --- a/scm-it/src/test/java/sonia/scm/it/TestData.java +++ b/scm-it/src/test/java/sonia/scm/it/utils/TestData.java @@ -1,4 +1,4 @@ -package sonia.scm.it; +package sonia.scm.it.utils; import io.restassured.response.ValidatableResponse; import org.apache.http.HttpStatus; @@ -8,14 +8,16 @@ import sonia.scm.repository.PermissionType; import sonia.scm.web.VndMediaType; import javax.json.Json; +import javax.json.JsonObjectBuilder; +import java.net.URI; import java.util.HashMap; import java.util.List; import java.util.Map; import static java.util.Arrays.asList; -import static sonia.scm.it.RestUtil.createResourceUrl; -import static sonia.scm.it.RestUtil.given; -import static sonia.scm.it.ScmTypes.availableScmTypes; +import static sonia.scm.it.utils.RestUtil.createResourceUrl; +import static sonia.scm.it.utils.RestUtil.given; +import static sonia.scm.it.utils.ScmTypes.availableScmTypes; public class TestData { @@ -26,6 +28,7 @@ public class TestData { private static final List PROTECTED_USERS = asList(USER_SCM_ADMIN, USER_ANONYMOUS); private static Map DEFAULT_REPOSITORIES = new HashMap<>(); + public static final JsonObjectBuilder JSON_BUILDER = NullAwareJsonObjectBuilder.wrap(Json.createObjectBuilder()); public static void createDefault() { cleanup(); @@ -44,27 +47,31 @@ public class TestData { } public static void createUser(String username, String password) { + createUser(username, password, false, "xml"); + } + + public static void createUser(String username, String password, boolean isAdmin, String type) { LOG.info("create user with username: {}", username); + String admin = isAdmin ? "true" : "false"; given(VndMediaType.USER) .when() - .content(" {\n" + - " \"active\": true,\n" + - " \"admin\": false,\n" + - " \"creationDate\": \"2018-08-21T12:26:46.084Z\",\n" + - " \"displayName\": \"" + username + "\",\n" + - " \"mail\": \"user1@scm-manager.org\",\n" + - " \"name\": \"" + username + "\",\n" + - " \"password\": \"" + password + "\",\n" + - " \"type\": \"xml\"\n" + - " \n" + - " }") - .post(createResourceUrl("users")) + .content(new StringBuilder() + .append(" {\n") + .append(" \"active\": true,\n") + .append(" \"admin\": ").append(admin).append(",\n") + .append(" \"creationDate\": \"2018-08-21T12:26:46.084Z\",\n") + .append(" \"displayName\": \"").append(username).append("\",\n") + .append(" \"mail\": \"user1@scm-manager.org\",\n") + .append(" \"name\": \"").append(username).append("\",\n") + .append(" \"password\": \"").append(password).append("\",\n") + .append(" \"type\": \"").append(type).append("\"\n") + .append(" }").toString()) + .post(getUsersUrl()) .then() .statusCode(HttpStatus.SC_CREATED) ; } - public static void createUserPermission(String name, PermissionType permissionType, String repositoryType) { String defaultPermissionUrl = TestData.getDefaultPermissionUrl(USER_SCM_ADMIN, USER_SCM_ADMIN, repositoryType); LOG.info("create permission with name {} and type: {} using the endpoint: {}", name, permissionType, defaultPermissionUrl); @@ -183,7 +190,7 @@ public class TestData { } public static String repositoryJson(String repositoryType) { - return Json.createObjectBuilder() + return JSON_BUILDER .add("contact", "zaphod.beeblebrox@hitchhiker.com") .add("description", "Heart of Gold") .add("name", "HeartOfGold-" + repositoryType) @@ -192,6 +199,29 @@ public class TestData { .build().toString(); } + public static URI getMeUrl() { + return RestUtil.createResourceUrl("me/"); + + } + + public static URI getUsersUrl() { + return RestUtil.createResourceUrl("users/"); + + } + + public static URI getUserUrl(String username) { + return getUsersUrl().resolve(username); + + } + + + public static String createPasswordChangeJson(String oldPassword, String newPassword) { + return JSON_BUILDER + .add("oldPassword", oldPassword) + .add("newPassword", newPassword) + .build().toString(); + } + public static void main(String[] args) { cleanup(); } diff --git a/scm-plugins/scm-git-plugin/package.json b/scm-plugins/scm-git-plugin/package.json index 7fc8484d75..93ec098597 100644 --- a/scm-plugins/scm-git-plugin/package.json +++ b/scm-plugins/scm-git-plugin/package.json @@ -9,6 +9,6 @@ "@scm-manager/ui-extensions": "^0.0.7" }, "devDependencies": { - "@scm-manager/ui-bundler": "^0.0.13" + "@scm-manager/ui-bundler": "^0.0.15" } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigInIndexResource.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigInIndexResource.java new file mode 100644 index 0000000000..a1120adda4 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigInIndexResource.java @@ -0,0 +1,40 @@ +package sonia.scm.api.v2.resources; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import sonia.scm.config.ConfigurationPermissions; +import sonia.scm.plugin.Extension; +import sonia.scm.web.JsonEnricherBase; +import sonia.scm.web.JsonEnricherContext; + +import javax.inject.Inject; +import javax.inject.Provider; + +import static java.util.Collections.singletonMap; +import static sonia.scm.web.VndMediaType.INDEX; + +@Extension +public class GitConfigInIndexResource extends JsonEnricherBase { + + private final Provider scmPathInfoStore; + + @Inject + public GitConfigInIndexResource(Provider scmPathInfoStore, ObjectMapper objectMapper) { + super(objectMapper); + this.scmPathInfoStore = scmPathInfoStore; + } + + @Override + public void enrich(JsonEnricherContext context) { + if (resultHasMediaType(INDEX, context) && ConfigurationPermissions.list().isPermitted()) { + String gitConfigUrl = new LinkBuilder(scmPathInfoStore.get().get(), GitConfigResource.class) + .method("get") + .parameters() + .href(); + + JsonNode gitConfigRefNode = createObject(singletonMap("href", value(gitConfigUrl))); + + addPropertyNode(context.getResponseEntity().get("_links"), "gitConfig", gitConfigRefNode); + } + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigToGitConfigDtoMapper.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigToGitConfigDtoMapper.java index 7163497487..7607b31faf 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigToGitConfigDtoMapper.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/api/v2/resources/GitConfigToGitConfigDtoMapper.java @@ -18,7 +18,7 @@ import static de.otto.edison.hal.Links.linkingTo; public abstract class GitConfigToGitConfigDtoMapper extends BaseMapper { @Inject - private UriInfoStore uriInfoStore; + private ScmPathInfoStore scmPathInfoStore; @AfterMapping void appendLinks(GitConfig config, @MappingTarget GitConfigDto target) { @@ -30,12 +30,12 @@ public abstract class GitConfigToGitConfigDtoMapper extends BaseMapper tagCollection = tags.get(commit.getId()); if (Util.isNotEmpty(tagCollection)) @@ -245,108 +231,7 @@ public class GitChangesetConverter implements Closeable return changeset; } - /** - * TODO: copy and rename - * - * - * @param modifications - * @param entry - */ - private void appendModification(Modifications modifications, DiffEntry entry) - { - switch (entry.getChangeType()) - { - case ADD : - modifications.getAdded().add(entry.getNewPath()); - break; - - case MODIFY : - modifications.getModified().add(entry.getNewPath()); - - break; - - case DELETE : - modifications.getRemoved().add(entry.getOldPath()); - - break; - } - } - - /** - * Method description - * - * - * @param treeWalk - * @param commit - * - * @return - * - * @throws IOException - */ - private Modifications createModifications(TreeWalk treeWalk, RevCommit commit) - throws IOException - { - Modifications modifications = null; - - treeWalk.reset(); - treeWalk.setRecursive(true); - - if (commit.getParentCount() > 0) - { - RevCommit parent = commit.getParent(0); - RevTree tree = parent.getTree(); - - if ((tree == null) && (revWalk != null)) - { - revWalk.parseHeaders(parent); - tree = parent.getTree(); - } - - if (tree != null) - { - treeWalk.addTree(tree); - } - else - { - if (logger.isTraceEnabled()) - { - logger.trace("no parent tree at position 0 for commit {}", - commit.getName()); - } - - treeWalk.addTree(new EmptyTreeIterator()); - } - } - else - { - if (logger.isTraceEnabled()) - { - logger.trace("no parent available for commit {}", commit.getName()); - } - - treeWalk.addTree(new EmptyTreeIterator()); - } - - treeWalk.addTree(commit.getTree()); - - List entries = DiffEntry.scan(treeWalk); - - for (DiffEntry e : entries) - { - if (!e.getOldId().equals(e.getNewId())) - { - if (modifications == null) - { - modifications = new Modifications(); - } - - appendModification(modifications, e); - } - } - - return modifications; - } //~--- fields --------------------------------------------------------------- diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModificationsCommand.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModificationsCommand.java new file mode 100644 index 0000000000..2b35ba74f6 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitModificationsCommand.java @@ -0,0 +1,106 @@ +package sonia.scm.repository.spi; + +import lombok.extern.slf4j.Slf4j; +import org.eclipse.jgit.diff.DiffEntry; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevTree; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.treewalk.EmptyTreeIterator; +import org.eclipse.jgit.treewalk.TreeWalk; +import sonia.scm.repository.GitUtil; +import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.repository.Modifications; +import sonia.scm.repository.Repository; + +import java.io.IOException; +import java.text.MessageFormat; +import java.util.List; + + +@Slf4j +public class GitModificationsCommand extends AbstractGitCommand implements ModificationsCommand { + + protected GitModificationsCommand(GitContext context, Repository repository) { + super(context, repository); + } + + private Modifications createModifications(TreeWalk treeWalk, RevCommit commit, RevWalk revWalk, String revision) + throws IOException, UnsupportedModificationTypeException { + treeWalk.reset(); + treeWalk.setRecursive(true); + if (commit.getParentCount() > 0) { + RevCommit parent = commit.getParent(0); + RevTree tree = parent.getTree(); + if ((tree == null) && (revWalk != null)) { + revWalk.parseHeaders(parent); + tree = parent.getTree(); + } + if (tree != null) { + treeWalk.addTree(tree); + } else { + log.trace("no parent tree at position 0 for commit {}", commit.getName()); + treeWalk.addTree(new EmptyTreeIterator()); + } + } else { + log.trace("no parent available for commit {}", commit.getName()); + treeWalk.addTree(new EmptyTreeIterator()); + } + treeWalk.addTree(commit.getTree()); + List entries = DiffEntry.scan(treeWalk); + Modifications modifications = new Modifications(); + for (DiffEntry e : entries) { + if (!e.getOldId().equals(e.getNewId())) { + appendModification(modifications, e); + } + } + modifications.setRevision(revision); + return modifications; + } + + @Override + public Modifications getModifications(String revision) { + org.eclipse.jgit.lib.Repository gitRepository = null; + RevWalk revWalk = null; + try { + gitRepository = open(); + if (!gitRepository.getAllRefs().isEmpty()) { + revWalk = new RevWalk(gitRepository); + ObjectId id = GitUtil.getRevisionId(gitRepository, revision); + RevCommit commit = revWalk.parseCommit(id); + TreeWalk treeWalk = new TreeWalk(gitRepository); + return createModifications(treeWalk, commit, revWalk, revision); + } + } catch (IOException ex) { + log.error("could not open repository", ex); + throw new InternalRepositoryException(ex); + + } catch (UnsupportedModificationTypeException ex) { + log.error("Unsupported modification type", ex); + throw new InternalRepositoryException(ex); + + } finally { + GitUtil.release(revWalk); + GitUtil.close(gitRepository); + } + return null; + } + + @Override + public Modifications getModifications(ModificationsCommandRequest request) { + return getModifications(request.getRevision()); + } + + private void appendModification(Modifications modifications, DiffEntry entry) throws UnsupportedModificationTypeException { + DiffEntry.ChangeType type = entry.getChangeType(); + if (type == DiffEntry.ChangeType.ADD) { + modifications.getAdded().add(entry.getNewPath()); + } else if (type == DiffEntry.ChangeType.MODIFY) { + modifications.getModified().add(entry.getNewPath()); + } else if (type == DiffEntry.ChangeType.DELETE) { + modifications.getRemoved().add(entry.getOldPath()); + } else { + throw new UnsupportedModificationTypeException(MessageFormat.format("The modification type: {0} is not supported.", type)); + } + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java index 7ea26de83f..d60abd424d 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceProvider.java @@ -33,20 +33,16 @@ package sonia.scm.repository.spi; -//~--- non-JDK imports -------------------------------------------------------- - import com.google.common.collect.ImmutableSet; - import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.Repository; import sonia.scm.repository.api.Command; -//~--- JDK imports ------------------------------------------------------------ - import java.io.IOException; - import java.util.Set; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -73,19 +69,10 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider //~--- constructors --------------------------------------------------------- - /** - * Constructs ... - * - * - * @param handler - * @param repository - */ - public GitRepositoryServiceProvider(GitRepositoryHandler handler, - Repository repository) - { + public GitRepositoryServiceProvider(GitRepositoryHandler handler, Repository repository) { this.handler = handler; this.repository = repository; - context = new GitContext(handler.getDirectory(repository)); + this.context = new GitContext(handler.getDirectory(repository)); } //~--- methods -------------------------------------------------------------- @@ -188,6 +175,11 @@ public class GitRepositoryServiceProvider extends RepositoryServiceProvider return new GitLogCommand(context, repository); } + @Override + public ModificationsCommand getModificationsCommand() { + return new GitModificationsCommand(context,repository); + } + /** * Method description * diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceResolver.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceResolver.java index 52c5171627..deca141556 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceResolver.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/GitRepositoryServiceResolver.java @@ -35,7 +35,6 @@ package sonia.scm.repository.spi; //~--- non-JDK imports -------------------------------------------------------- import com.google.inject.Inject; - import sonia.scm.plugin.Extension; import sonia.scm.repository.GitRepositoryHandler; import sonia.scm.repository.Repository; @@ -45,51 +44,23 @@ import sonia.scm.repository.Repository; * @author Sebastian Sdorra */ @Extension -public class GitRepositoryServiceResolver implements RepositoryServiceResolver -{ +public class GitRepositoryServiceResolver implements RepositoryServiceResolver { - /** Field description */ - public static final String TYPE = "git"; + private final GitRepositoryHandler handler; - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * - * @param handler - */ @Inject - public GitRepositoryServiceResolver(GitRepositoryHandler handler) - { + public GitRepositoryServiceResolver(GitRepositoryHandler handler) { this.handler = handler; } - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param repository - * - * @return - */ @Override - public GitRepositoryServiceProvider resolve(Repository repository) - { + public GitRepositoryServiceProvider resolve(Repository repository) { GitRepositoryServiceProvider provider = null; - if (TYPE.equalsIgnoreCase(repository.getType())) - { + if (GitRepositoryHandler.TYPE_NAME.equalsIgnoreCase(repository.getType())) { provider = new GitRepositoryServiceProvider(handler, repository); } return provider; } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private GitRepositoryHandler handler; } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/UnsupportedModificationTypeException.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/UnsupportedModificationTypeException.java new file mode 100644 index 0000000000..5081a29d21 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/repository/spi/UnsupportedModificationTypeException.java @@ -0,0 +1,9 @@ +package sonia.scm.repository.spi; + +import sonia.scm.repository.InternalRepositoryException; + +public class UnsupportedModificationTypeException extends InternalRepositoryException { + public UnsupportedModificationTypeException(String message) { + super(message); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitBasicAuthenticationFilter.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitBasicAuthenticationFilter.java deleted file mode 100644 index f06fcfcd39..0000000000 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitBasicAuthenticationFilter.java +++ /dev/null @@ -1,69 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. 2. Redistributions in - * binary form must reproduce the above copyright notice, this list of - * conditions and the following disclaimer in the documentation and/or other - * materials provided with the distribution. 3. Neither the name of SCM-Manager; - * nor the names of its contributors may be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - - -package sonia.scm.web; - -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.inject.Inject; - -import sonia.scm.Priority; -import sonia.scm.config.ScmConfiguration; -import sonia.scm.filter.Filters; -import sonia.scm.filter.WebElement; -import sonia.scm.web.filter.AuthenticationFilter; - -import java.util.Set; - - -/** - * Handles git specific basic authentication. - * - * @author Sebastian Sdorra - */ -@Priority(Filters.PRIORITY_AUTHENTICATION) -@WebElement(value = GitServletModule.PATTERN_GIT) -public class GitBasicAuthenticationFilter extends AuthenticationFilter -{ - - /** - * Constructs a new instance. - * - * @param configuration scm-manager main configuration - * @param webTokenGenerators web token generators - */ - @Inject - public GitBasicAuthenticationFilter(ScmConfiguration configuration, - Set webTokenGenerators) - { - super(configuration, webTokenGenerators); - } -} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitPermissionFilter.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitPermissionFilter.java index 1f07753f1e..e38f26a309 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitPermissionFilter.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitPermissionFilter.java @@ -33,38 +33,24 @@ package sonia.scm.web; -//~--- non-JDK imports -------------------------------------------------------- - import com.google.common.annotations.VisibleForTesting; -import com.google.inject.Inject; -import com.google.inject.Singleton; - import org.eclipse.jgit.http.server.GitSmartHttpTools; - import sonia.scm.ClientMessages; import sonia.scm.config.ScmConfiguration; import sonia.scm.repository.GitUtil; -import sonia.scm.repository.RepositoryProvider; -import sonia.scm.web.filter.ProviderPermissionFilter; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; +import sonia.scm.repository.spi.ScmProviderHttpServlet; +import sonia.scm.web.filter.PermissionFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import sonia.scm.Priority; -import sonia.scm.filter.Filters; -import sonia.scm.filter.WebElement; +import java.io.IOException; /** * GitPermissionFilter decides if a git request requires write or read privileges. * * @author Sebastian Sdorra */ -@Priority(Filters.PRIORITY_AUTHORIZATION) -@WebElement(value = GitServletModule.PATTERN_GIT) -public class GitPermissionFilter extends ProviderPermissionFilter +public class GitPermissionFilter extends PermissionFilter { private static final String PARAMETER_SERVICE = "service"; @@ -83,11 +69,9 @@ public class GitPermissionFilter extends ProviderPermissionFilter * Constructs a new instance of the GitPermissionFilter. * * @param configuration scm main configuration - * @param repositoryProvider repository provider */ - @Inject - public GitPermissionFilter(ScmConfiguration configuration, RepositoryProvider repositoryProvider) { - super(configuration, repositoryProvider); + public GitPermissionFilter(ScmConfiguration configuration, ScmProviderHttpServlet delegate) { + super(configuration, delegate); } @Override @@ -103,7 +87,7 @@ public class GitPermissionFilter extends ProviderPermissionFilter } @Override - protected boolean isWriteRequest(HttpServletRequest request) { + public boolean isWriteRequest(HttpServletRequest request) { return isReceivePackRequest(request) || isReceiveServiceRequest(request) || isLfsFileUpload(request); diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitPermissionFilterFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitPermissionFilterFactory.java new file mode 100644 index 0000000000..c358da5fb1 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitPermissionFilterFactory.java @@ -0,0 +1,30 @@ +package sonia.scm.web; + +import sonia.scm.config.ScmConfiguration; +import sonia.scm.plugin.Extension; +import sonia.scm.repository.GitRepositoryHandler; +import sonia.scm.repository.spi.ScmProviderHttpServlet; +import sonia.scm.repository.spi.ScmProviderHttpServletDecoratorFactory; + +import javax.inject.Inject; + +@Extension +public class GitPermissionFilterFactory implements ScmProviderHttpServletDecoratorFactory { + + private final ScmConfiguration configuration; + + @Inject + public GitPermissionFilterFactory(ScmConfiguration configuration) { + this.configuration = configuration; + } + + @Override + public boolean handlesScmType(String type) { + return GitRepositoryHandler.TYPE_NAME.equals(type); + } + + @Override + public ScmProviderHttpServlet createDecorator(ScmProviderHttpServlet delegate) { + return new GitPermissionFilter(configuration, delegate); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitRepositoryResolver.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitRepositoryResolver.java index 76e742a71a..7f04bb3a54 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitRepositoryResolver.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitRepositoryResolver.java @@ -125,8 +125,9 @@ public class GitRepositoryResolver implements RepositoryResolver uriInfoStore, ScmConfiguration scmConfiguration) { + super(servletProvider, uriInfoStore, scmConfiguration); + } + + @Override + public String getType() { + return GitRepositoryHandler.TYPE_NAME; + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitServletModule.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitServletModule.java index bdad103c15..e731e01a62 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitServletModule.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/GitServletModule.java @@ -51,18 +51,6 @@ import sonia.scm.web.lfs.LfsBlobStoreFactory; public class GitServletModule extends ServletModule { - public static final String GIT_PATH = "/git"; - - /** Field description */ - public static final String PATTERN_GIT = GIT_PATH + "/*"; - - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - */ @Override protected void configureServlets() { @@ -75,8 +63,5 @@ public class GitServletModule extends ServletModule bind(GitConfigDtoToGitConfigMapper.class).to(Mappers.getMapper(GitConfigDtoToGitConfigMapper.class).getClass()); bind(GitConfigToGitConfigDtoMapper.class).to(Mappers.getMapper(GitConfigToGitConfigDtoMapper.class).getClass()); - - // serlvelts and filters - serve(PATTERN_GIT).with(ScmGitServlet.class); } } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/ScmGitServlet.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/ScmGitServlet.java index 5612a64652..2701764607 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/ScmGitServlet.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/ScmGitServlet.java @@ -33,8 +33,6 @@ package sonia.scm.web; -//~--- non-JDK imports -------------------------------------------------------- - import com.google.common.annotations.VisibleForTesting; import com.google.inject.Inject; import com.google.inject.Singleton; @@ -42,8 +40,8 @@ import org.eclipse.jgit.http.server.GitServlet; import org.eclipse.jgit.lfs.lib.Constants; import org.slf4j.Logger; import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryProvider; import sonia.scm.repository.RepositoryRequestListenerUtil; +import sonia.scm.repository.spi.ScmProviderHttpServlet; import sonia.scm.util.HttpUtil; import sonia.scm.web.lfs.servlet.LfsServletFactory; @@ -57,19 +55,18 @@ import java.util.regex.Pattern; import static org.eclipse.jgit.lfs.lib.Constants.CONTENT_TYPE_GIT_LFS_JSON; import static org.slf4j.LoggerFactory.getLogger; -//~--- JDK imports ------------------------------------------------------------ - /** * * @author Sebastian Sdorra */ @Singleton -public class ScmGitServlet extends GitServlet +public class ScmGitServlet extends GitServlet implements ScmProviderHttpServlet { - /** Field description */ + public static final String REPO_PATH = "/repo"; + public static final Pattern REGEX_GITHTTPBACKEND = Pattern.compile( - "(?x)^/git/(.*/(HEAD|info/refs|objects/(info/[^/]+|[0-9a-f]{2}/[0-9a-f]{38}|pack/pack-[0-9a-f]{40}\\.(pack|idx))|git-(upload|receive)-pack))$" + "(?x)^/repo/(.*/(HEAD|info/refs|objects/(info/[^/]+|[0-9a-f]{2}/[0-9a-f]{38}|pack/pack-[0-9a-f]{40}\\.(pack|idx))|git-(upload|receive)-pack))$" ); /** Field description */ @@ -88,7 +85,6 @@ public class ScmGitServlet extends GitServlet * @param repositoryResolver * @param receivePackFactory * @param repositoryViewer - * @param repositoryProvider * @param repositoryRequestListenerUtil * @param lfsServletFactory */ @@ -96,11 +92,9 @@ public class ScmGitServlet extends GitServlet public ScmGitServlet(GitRepositoryResolver repositoryResolver, GitReceivePackFactory receivePackFactory, GitRepositoryViewer repositoryViewer, - RepositoryProvider repositoryProvider, RepositoryRequestListenerUtil repositoryRequestListenerUtil, LfsServletFactory lfsServletFactory) { - this.repositoryProvider = repositoryProvider; this.repositoryViewer = repositoryViewer; this.repositoryRequestListenerUtil = repositoryRequestListenerUtil; this.lfsServletFactory = lfsServletFactory; @@ -122,44 +116,9 @@ public class ScmGitServlet extends GitServlet * @throws ServletException */ @Override - protected void service(HttpServletRequest request, - HttpServletResponse response) + public void service(HttpServletRequest request, HttpServletResponse response, Repository repository) throws ServletException, IOException { - Repository repository = repositoryProvider.get(); - if (repository != null) { - handleRequest(request, response, repository); - } else { - // logger - response.sendError(HttpServletResponse.SC_NOT_FOUND); - } - } - - /** - * Decides the type request being currently made and delegates it accordingly. - *

    - *
  • Batch API:
  • - *
      - *
    • used to provide the client with information on how handle the large files of a repository.
    • - *
    • response contains the information where to perform the actual upload and download of the large objects.
    • - *
    - *
  • Transfer API:
  • - *
      - *
    • receives and provides the actual large objects (resolves the pointer placed in the file of the working copy).
    • - *
    • invoked only after the Batch API has been questioned about what to do with the large files
    • - *
    - *
  • Regular Git Http API:
  • - *
      - *
    • regular git http wire protocol, use by normal git clients.
    • - *
    - *
  • Browser Overview:
  • - *
      - *
    • short repository overview for browser clients.
    • - *
    - *
  • - *
- */ - private void handleRequest(HttpServletRequest request, HttpServletResponse response, Repository repository) throws ServletException, IOException { String repoPath = repository.getNamespace() + "/" + repository.getName(); logger.trace("handle git repository at {}", repoPath); if (isLfsBatchApiRequest(request, repoPath)) { @@ -210,7 +169,7 @@ public class ScmGitServlet extends GitServlet * @throws IOException * @throws ServletException */ - private void handleBrowserRequest(HttpServletRequest request, HttpServletResponse response, Repository repository) throws ServletException, IOException { + private void handleBrowserRequest(HttpServletRequest request, HttpServletResponse response, Repository repository) throws ServletException { try { repositoryViewer.handleRequest(request, response, repository); } catch (IOException ex) { @@ -229,7 +188,7 @@ public class ScmGitServlet extends GitServlet */ private static boolean isLfsFileTransferRequest(HttpServletRequest request, String repository) { - String regex = String.format("^%s%s/%s(\\.git)?/info/lfs/objects/[a-z0-9]{64}$", request.getContextPath(), GitServletModule.GIT_PATH, repository); + String regex = String.format("^%s%s/%s(\\.git)?/info/lfs/objects/[a-z0-9]{64}$", request.getContextPath(), REPO_PATH, repository); boolean pathMatches = request.getRequestURI().matches(regex); boolean methodMatches = request.getMethod().equals("PUT") || request.getMethod().equals("GET"); @@ -248,7 +207,7 @@ public class ScmGitServlet extends GitServlet */ private static boolean isLfsBatchApiRequest(HttpServletRequest request, String repository) { - String regex = String.format("^%s%s/%s(\\.git)?/info/lfs/objects/batch$", request.getContextPath(), GitServletModule.GIT_PATH, repository); + String regex = String.format("^%s%s/%s(\\.git)?/info/lfs/objects/batch$", request.getContextPath(), REPO_PATH, repository); boolean pathMatches = request.getRequestURI().matches(regex); boolean methodMatches = "POST".equals(request.getMethod()); @@ -284,12 +243,8 @@ public class ScmGitServlet extends GitServlet return false; } - //~--- fields --------------------------------------------------------------- - /** Field description */ - private final RepositoryProvider repositoryProvider; - /** Field description */ private final RepositoryRequestListenerUtil repositoryRequestListenerUtil; @@ -299,5 +254,4 @@ public class ScmGitServlet extends GitServlet private final GitRepositoryViewer repositoryViewer; private final LfsServletFactory lfsServletFactory; - } diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/ScmGitServletProvider.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/ScmGitServletProvider.java new file mode 100644 index 0000000000..56a9e358be --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/ScmGitServletProvider.java @@ -0,0 +1,23 @@ +package sonia.scm.web; + +import com.google.inject.Inject; +import sonia.scm.repository.GitRepositoryHandler; +import sonia.scm.repository.spi.ScmProviderHttpServlet; +import sonia.scm.repository.spi.ScmProviderHttpServletProvider; + +import javax.inject.Provider; + +public class ScmGitServletProvider extends ScmProviderHttpServletProvider { + + @Inject + private Provider servletProvider; + + public ScmGitServletProvider() { + super(GitRepositoryHandler.TYPE_NAME); + } + + @Override + protected ScmProviderHttpServlet getRootServlet() { + return servletProvider.get(); + } +} diff --git a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/servlet/LfsServletFactory.java b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/servlet/LfsServletFactory.java index 58bdb2fcf1..f4eed34678 100644 --- a/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/servlet/LfsServletFactory.java +++ b/scm-plugins/scm-git-plugin/src/main/java/sonia/scm/web/lfs/servlet/LfsServletFactory.java @@ -70,7 +70,7 @@ public class LfsServletFactory { */ @VisibleForTesting static String buildBaseUri(Repository repository, HttpServletRequest request) { - return String.format("%s/git/%s/%s.git/info/lfs/objects/", HttpUtil.getCompleteUrl(request), repository.getNamespace(), repository.getName()); + return String.format("%s/repo/%s/%s.git/info/lfs/objects/", HttpUtil.getCompleteUrl(request), repository.getNamespace(), repository.getName()); } } diff --git a/scm-plugins/scm-git-plugin/src/main/js/ProtocolInformation.js b/scm-plugins/scm-git-plugin/src/main/js/ProtocolInformation.js index 3c1362ba80..d8eb4ae0e0 100644 --- a/scm-plugins/scm-git-plugin/src/main/js/ProtocolInformation.js +++ b/scm-plugins/scm-git-plugin/src/main/js/ProtocolInformation.js @@ -1,5 +1,6 @@ //@flow import React from "react"; +import { repositories } from "@scm-manager/ui-components"; import type { Repository } from "@scm-manager/ui-types"; type Props = { @@ -10,14 +11,16 @@ class ProtocolInformation extends React.Component { render() { const { repository } = this.props; - if (!repository._links.httpProtocol) { + const href = repositories.getProtocolLinkByType(repository, "http"); + if (!href) { return null; } + return (

Clone the repository

-          git clone {repository._links.httpProtocol.href}
+          git clone {href}
         

Create a new repository

@@ -30,7 +33,7 @@ class ProtocolInformation extends React.Component {
             
git commit -m "added readme"
- git remote add origin {repository._links.httpProtocol.href} + git remote add origin {href}
git push -u origin master
@@ -39,7 +42,7 @@ class ProtocolInformation extends React.Component {

Push an existing repository

           
-            git remote add origin {repository._links.httpProtocol.href}
+            git remote add origin {href}
             
git push -u origin master
diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigInIndexResourceTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigInIndexResourceTest.java new file mode 100644 index 0000000000..665be19788 --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigInIndexResourceTest.java @@ -0,0 +1,64 @@ +package sonia.scm.api.v2.resources; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; +import com.google.inject.util.Providers; +import org.junit.Rule; +import org.junit.Test; +import sonia.scm.web.JsonEnricherContext; +import sonia.scm.web.VndMediaType; + +import javax.ws.rs.core.MediaType; +import java.net.URI; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +@SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini") +public class GitConfigInIndexResourceTest { + + @Rule + public final ShiroRule shiroRule = new ShiroRule(); + + private final ObjectMapper objectMapper = new ObjectMapper(); + private final ObjectNode root = objectMapper.createObjectNode(); + private final GitConfigInIndexResource gitConfigInIndexResource; + + public GitConfigInIndexResourceTest() { + root.put("_links", objectMapper.createObjectNode()); + ScmPathInfoStore pathInfoStore = new ScmPathInfoStore(); + pathInfoStore.set(() -> URI.create("/")); + gitConfigInIndexResource = new GitConfigInIndexResource(Providers.of(pathInfoStore), objectMapper); + } + + @Test + @SubjectAware(username = "admin", password = "secret") + public void admin() { + JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root); + + gitConfigInIndexResource.enrich(context); + + assertEquals("/v2/config/git", root.get("_links").get("gitConfig").get("href").asText()); + } + + @Test + @SubjectAware(username = "readOnly", password = "secret") + public void user() { + JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root); + + gitConfigInIndexResource.enrich(context); + + assertFalse(root.get("_links").iterator().hasNext()); + } + + @Test + public void anonymous() { + JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root); + + gitConfigInIndexResource.enrich(context); + + assertFalse(root.get("_links").iterator().hasNext()); + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigResourceTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigResourceTest.java index 42790ea7a4..1ebe7fc98b 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigResourceTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigResourceTest.java @@ -53,7 +53,7 @@ public class GitConfigResourceTest { private GitConfigDtoToGitConfigMapperImpl dtoToConfigMapper; @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private UriInfoStore uriInfoStore; + private ScmPathInfoStore scmPathInfoStore; @InjectMocks private GitConfigToGitConfigDtoMapperImpl configToDtoMapper; @@ -67,7 +67,7 @@ public class GitConfigResourceTest { when(repositoryHandler.getConfig()).thenReturn(gitConfig); GitConfigResource gitConfigResource = new GitConfigResource(dtoToConfigMapper, configToDtoMapper, repositoryHandler); dispatcher.getRegistry().addSingletonResource(gitConfigResource); - when(uriInfoStore.get().getBaseUri()).thenReturn(baseUri); + when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri); } @Test diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigToGitConfigDtoMapperTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigToGitConfigDtoMapperTest.java index 51fded4839..82c85029a3 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigToGitConfigDtoMapperTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/api/v2/resources/GitConfigToGitConfigDtoMapperTest.java @@ -28,7 +28,7 @@ public class GitConfigToGitConfigDtoMapperTest { private URI baseUri = URI.create("http://example.com/base/"); @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private UriInfoStore uriInfoStore; + private ScmPathInfoStore scmPathInfoStore; @InjectMocks private GitConfigToGitConfigDtoMapperImpl mapper; @@ -40,7 +40,7 @@ public class GitConfigToGitConfigDtoMapperTest { @Before public void init() { - when(uriInfoStore.get().getBaseUri()).thenReturn(baseUri); + when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri); expectedBaseUri = baseUri.resolve(GitConfigResource.GIT_CONFIG_PATH_V2); subjectThreadState.bind(); ThreadContext.bind(subject); diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractRemoteCommandTestBase.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractRemoteCommandTestBase.java index e2a401bf7d..97e09c0708 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractRemoteCommandTestBase.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/AbstractRemoteCommandTestBase.java @@ -85,14 +85,14 @@ public class AbstractRemoteCommandTestBase outgoingDirectory = tempFolder.newFile("outgoing"); outgoingDirectory.delete(); - incomgingRepository = new Repository("1", "git", "space", "incoming"); + incomingRepository = new Repository("1", "git", "space", "incoming"); outgoingRepository = new Repository("2", "git", "space", "outgoing"); incoming = Git.init().setDirectory(incomingDirectory).setBare(false).call(); outgoing = Git.init().setDirectory(outgoingDirectory).setBare(false).call(); handler = mock(GitRepositoryHandler.class); - when(handler.getDirectory(incomgingRepository)).thenReturn( + when(handler.getDirectory(incomingRepository)).thenReturn( incomingDirectory); when(handler.getDirectory(outgoingRepository)).thenReturn( outgoingDirectory); @@ -211,7 +211,7 @@ public class AbstractRemoteCommandTestBase protected GitRepositoryHandler handler; /** Field description */ - protected Repository incomgingRepository; + protected Repository incomingRepository; /** Field description */ protected Git incoming; diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitIncomingCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitIncomingCommandTest.java index 6aa852b12c..e3d36601e7 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitIncomingCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitIncomingCommandTest.java @@ -105,7 +105,7 @@ public class GitIncomingCommandTest commit(outgoing, "added a"); - GitPullCommand pull = new GitPullCommand(handler, new GitContext(incomingDirectory), incomgingRepository); + GitPullCommand pull = new GitPullCommand(handler, new GitContext(incomingDirectory), incomingRepository); PullCommandRequest req = new PullCommandRequest(); req.setRemoteRepository(outgoingRepository); pull.pull(req); @@ -192,6 +192,6 @@ public class GitIncomingCommandTest private GitIncomingCommand createCommand() { return new GitIncomingCommand(handler, new GitContext(incomingDirectory), - incomgingRepository); + incomingRepository); } } diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLogCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLogCommandTest.java index 51df1298e5..d6e6ac98d8 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLogCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitLogCommandTest.java @@ -168,21 +168,23 @@ public class GitLogCommandTest extends AbstractGitCommandTestBase Changeset c = command.getChangeset("435df2f061add3589cb3"); assertNotNull(c); - assertEquals("435df2f061add3589cb326cc64be9b9c3897ceca", c.getId()); + String revision = "435df2f061add3589cb326cc64be9b9c3897ceca"; + assertEquals(revision, c.getId()); assertEquals("added a and b files", c.getDescription()); checkDate(c.getDate()); assertEquals("Douglas Adams", c.getAuthor().getName()); assertEquals("douglas.adams@hitchhiker.com", c.getAuthor().getMail()); assertEquals("added a and b files", c.getDescription()); - Modifications mods = c.getModifications(); + GitModificationsCommand gitModificationsCommand = new GitModificationsCommand(createContext(), repository); + Modifications modifications = gitModificationsCommand.getModifications(revision); - assertNotNull(mods); - assertTrue("modified list should be empty", mods.getModified().isEmpty()); - assertTrue("removed list should be empty", mods.getRemoved().isEmpty()); - assertFalse("added list should not be empty", mods.getAdded().isEmpty()); - assertEquals(2, mods.getAdded().size()); - assertThat(mods.getAdded(), contains("a.txt", "b.txt")); + assertNotNull(modifications); + assertTrue("modified list should be empty", modifications.getModified().isEmpty()); + assertTrue("removed list should be empty", modifications.getRemoved().isEmpty()); + assertFalse("added list should not be empty", modifications.getAdded().isEmpty()); + assertEquals(2, modifications.getAdded().size()); + assertThat(modifications.getAdded(), contains("a.txt", "b.txt")); } @Test diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModificationsCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModificationsCommandTest.java new file mode 100644 index 0000000000..fb982f6f0c --- /dev/null +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitModificationsCommandTest.java @@ -0,0 +1,126 @@ +package sonia.scm.repository.spi; + +import org.eclipse.jgit.revwalk.RevCommit; +import org.junit.Before; +import org.junit.Test; +import sonia.scm.repository.Modifications; + +import java.io.File; +import java.io.IOException; +import java.util.function.Consumer; + +import static org.assertj.core.api.Java6Assertions.assertThat; + +public class GitModificationsCommandTest extends AbstractRemoteCommandTestBase { + + private GitModificationsCommand incomingModificationsCommand; + private GitModificationsCommand outgoingModificationsCommand; + + @Before + public void init() { + incomingModificationsCommand = new GitModificationsCommand(new GitContext(incomingDirectory), incomingRepository); + outgoingModificationsCommand = new GitModificationsCommand(new GitContext(outgoingDirectory), outgoingRepository); + } + + @Test + public void shouldReadAddedFiles() throws Exception { + write(outgoing, outgoingDirectory, "a.txt", "bal bla"); + RevCommit addedFileCommit = commit(outgoing, "add file"); + String revision = addedFileCommit.getName(); + Consumer assertModifications = assertAddedFiles("a.txt"); + assertModifications.accept(outgoingModificationsCommand.getModifications(revision)); + pushOutgoingAndPullIncoming(); + assertModifications.accept(incomingModificationsCommand.getModifications(revision)); + } + + @Test + public void shouldReadModifiedFiles() throws Exception { + write(outgoing, outgoingDirectory, "a.txt", "bal bla"); + commit(outgoing, "add file"); + write(outgoing, outgoingDirectory, "a.txt", "modified content"); + RevCommit modifiedFileCommit = commit(outgoing, "modify file"); + String revision = modifiedFileCommit.getName(); + Consumer assertModifications = assertModifiedFiles("a.txt"); + assertModifications.accept(outgoingModificationsCommand.getModifications(revision)); + pushOutgoingAndPullIncoming(); + assertModifications.accept(incomingModificationsCommand.getModifications(revision)); + } + + @Test + public void shouldReadRemovedFiles() throws Exception { + String fileName = "a.txt"; + write(outgoing, outgoingDirectory, fileName, "bal bla"); + commit(outgoing, "add file"); + File file = new File(outgoingDirectory, fileName); + file.delete(); + outgoing.rm().addFilepattern(fileName).call(); + RevCommit removedFileCommit = commit(outgoing, "remove file"); + String revision = removedFileCommit.getName(); + Consumer assertModifications = assertRemovedFiles(fileName); + pushOutgoingAndPullIncoming(); + assertModifications.accept(incomingModificationsCommand.getModifications(revision)); + assertModifications.accept(outgoingModificationsCommand.getModifications(revision)); + } + + void pushOutgoingAndPullIncoming() throws IOException { + GitPushCommand cmd = new GitPushCommand(handler, new GitContext(outgoingDirectory), + outgoingRepository); + PushCommandRequest request = new PushCommandRequest(); + request.setRemoteRepository(incomingRepository); + cmd.push(request); + GitPullCommand pullCommand = new GitPullCommand(handler, new GitContext(incomingDirectory), + incomingRepository); + PullCommandRequest pullRequest = new PullCommandRequest(); + pullRequest.setRemoteRepository(incomingRepository); + pullCommand.pull(pullRequest); + } + + Consumer assertRemovedFiles(String fileName) { + return (modifications) -> { + assertThat(modifications).isNotNull(); + assertThat(modifications.getAdded()) + .as("added files modifications") + .hasSize(0); + assertThat(modifications.getModified()) + .as("modified files modifications") + .hasSize(0); + assertThat(modifications.getRemoved()) + .as("removed files modifications") + .hasSize(1) + .containsOnly(fileName); + }; + } + + + Consumer assertModifiedFiles(String file) { + return (modifications) -> { + assertThat(modifications).isNotNull(); + assertThat(modifications.getAdded()) + .as("added files modifications") + .hasSize(0); + assertThat(modifications.getModified()) + .as("modified files modifications") + .hasSize(1) + .containsOnly(file); + assertThat(modifications.getRemoved()) + .as("removed files modifications") + .hasSize(0); + }; + } + + Consumer assertAddedFiles(String file) { + return (modifications) -> { + assertThat(modifications).isNotNull(); + assertThat(modifications.getAdded()) + .as("added files modifications") + .hasSize(1) + .containsOnly(file); + assertThat(modifications.getModified()) + .as("modified files modifications") + .hasSize(0); + assertThat(modifications.getRemoved()) + .as("removed files modifications") + .hasSize(0); + }; + } +} diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitOutgoingCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitOutgoingCommandTest.java index 58f70109ef..3510650fb4 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitOutgoingCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitOutgoingCommandTest.java @@ -78,7 +78,7 @@ public class GitOutgoingCommandTest extends AbstractRemoteCommandTestBase GitOutgoingCommand cmd = createCommand(); OutgoingCommandRequest request = new OutgoingCommandRequest(); - request.setRemoteRepository(incomgingRepository); + request.setRemoteRepository(incomingRepository); ChangesetPagingResult cpr = cmd.getOutgoingChangesets(request); @@ -98,7 +98,7 @@ public class GitOutgoingCommandTest extends AbstractRemoteCommandTestBase * @throws RepositoryException */ @Test - public void testGetOutgoingChangesetsWithAllreadyPushedChanges() + public void testGetOutgoingChangesetsWithAlreadyPushedChanges() throws IOException, GitAPIException { write(outgoing, outgoingDirectory, "a.txt", "content of a.txt"); @@ -110,7 +110,7 @@ public class GitOutgoingCommandTest extends AbstractRemoteCommandTestBase outgoingRepository); PushCommandRequest req = new PushCommandRequest(); - req.setRemoteRepository(incomgingRepository); + req.setRemoteRepository(incomingRepository); push.push(req); write(outgoing, outgoingDirectory, "b.txt", "content of b.txt"); @@ -120,7 +120,7 @@ public class GitOutgoingCommandTest extends AbstractRemoteCommandTestBase GitOutgoingCommand cmd = createCommand(); OutgoingCommandRequest request = new OutgoingCommandRequest(); - request.setRemoteRepository(incomgingRepository); + request.setRemoteRepository(incomingRepository); ChangesetPagingResult cpr = cmd.getOutgoingChangesets(request); @@ -144,7 +144,7 @@ public class GitOutgoingCommandTest extends AbstractRemoteCommandTestBase GitOutgoingCommand cmd = createCommand(); OutgoingCommandRequest request = new OutgoingCommandRequest(); - request.setRemoteRepository(incomgingRepository); + request.setRemoteRepository(incomingRepository); ChangesetPagingResult cpr = cmd.getOutgoingChangesets(request); diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitPushCommandTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitPushCommandTest.java index 91fad57880..4f3d7e933d 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitPushCommandTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/repository/spi/GitPushCommandTest.java @@ -78,7 +78,7 @@ public class GitPushCommandTest extends AbstractRemoteCommandTestBase GitPushCommand cmd = createCommand(); PushCommandRequest request = new PushCommandRequest(); - request.setRemoteRepository(incomgingRepository); + request.setRemoteRepository(incomingRepository); PushResponse response = cmd.push(request); diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/GitPermissionFilterTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/GitPermissionFilterTest.java index fede8842a9..831402261b 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/GitPermissionFilterTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/GitPermissionFilterTest.java @@ -5,7 +5,7 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.config.ScmConfiguration; -import sonia.scm.repository.RepositoryProvider; +import sonia.scm.repository.spi.ScmProviderHttpServlet; import sonia.scm.util.HttpUtil; import javax.servlet.ServletOutputStream; @@ -29,12 +29,7 @@ import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.class) public class GitPermissionFilterTest { - @Mock - private RepositoryProvider repositoryProvider; - - private final GitPermissionFilter permissionFilter = new GitPermissionFilter( - new ScmConfiguration(), repositoryProvider - ); + private final GitPermissionFilter permissionFilter = new GitPermissionFilter(new ScmConfiguration(), mock(ScmProviderHttpServlet.class)); @Mock private HttpServletResponse response; diff --git a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/servlet/LfsServletFactoryTest.java b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/servlet/LfsServletFactoryTest.java index f6f5803bb7..09a431ea43 100644 --- a/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/servlet/LfsServletFactoryTest.java +++ b/scm-plugins/scm-git-plugin/src/test/java/sonia/scm/web/lfs/servlet/LfsServletFactoryTest.java @@ -23,12 +23,12 @@ public class LfsServletFactoryTest { String repositoryName = "git-lfs-demo"; String result = LfsServletFactory.buildBaseUri(new Repository("", "GIT", repositoryNamespace, repositoryName), RequestWithUri(repositoryName, true)); - assertThat(result, is(equalTo("http://localhost:8081/scm/git/space/git-lfs-demo.git/info/lfs/objects/"))); + assertThat(result, is(equalTo("http://localhost:8081/scm/repo/space/git-lfs-demo.git/info/lfs/objects/"))); //result will be with dot-git suffix, ide result = LfsServletFactory.buildBaseUri(new Repository("", "GIT", repositoryNamespace, repositoryName), RequestWithUri(repositoryName, false)); - assertThat(result, is(equalTo("http://localhost:8081/scm/git/space/git-lfs-demo.git/info/lfs/objects/"))); + assertThat(result, is(equalTo("http://localhost:8081/scm/repo/space/git-lfs-demo.git/info/lfs/objects/"))); } private HttpServletRequest RequestWithUri(String repositoryName, boolean withDotGitSuffix) { @@ -44,12 +44,10 @@ public class LfsServletFactoryTest { //build from valid live request data when(mockedRequest.getRequestURL()).thenReturn( - new StringBuffer(String.format("http://localhost:8081/scm/git/%s%s/info/lfs/objects/batch", repositoryName, suffix))); - when(mockedRequest.getRequestURI()).thenReturn(String.format("/scm/git/%s%s/info/lfs/objects/batch", repositoryName, suffix)); + new StringBuffer(String.format("http://localhost:8081/scm/repo/%s%s/info/lfs/objects/batch", repositoryName, suffix))); + when(mockedRequest.getRequestURI()).thenReturn(String.format("/scm/repo/%s%s/info/lfs/objects/batch", repositoryName, suffix)); when(mockedRequest.getContextPath()).thenReturn("/scm"); return mockedRequest; } - - } diff --git a/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/configuration/shiro.ini b/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/configuration/shiro.ini index 36226edd7d..5d30a000f2 100644 --- a/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/configuration/shiro.ini +++ b/scm-plugins/scm-git-plugin/src/test/resources/sonia/scm/configuration/shiro.ini @@ -2,8 +2,10 @@ readOnly = secret, reader writeOnly = secret, writer readWrite = secret, readerWriter +admin = secret, admin [roles] reader = configuration:read:git writer = configuration:write:git readerWriter = configuration:*:git +admin = * diff --git a/scm-plugins/scm-git-plugin/yarn.lock b/scm-plugins/scm-git-plugin/yarn.lock index 56d3e0ffe2..702e28711f 100644 --- a/scm-plugins/scm-git-plugin/yarn.lock +++ b/scm-plugins/scm-git-plugin/yarn.lock @@ -707,9 +707,9 @@ version "0.0.2" resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85" -"@scm-manager/ui-bundler@^0.0.13": - version "0.0.13" - resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.13.tgz#020e6c8ee870fccb6c451490cb18972ebfb0d2c4" +"@scm-manager/ui-bundler@^0.0.15": + version "0.0.15" + resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.15.tgz#8ed4a557d5ae38d6b99493b29608fd6a4c9cd917" dependencies: "@babel/core" "^7.0.0" "@babel/plugin-proposal-class-properties" "^7.0.0" @@ -737,9 +737,10 @@ flow-bin "^0.79.1" gulp "^3.9.1" gulp-sourcemaps "^2.6.4" - gulp-util "^3.0.8" + gulp-uglify "^3.0.1" jest "^23.5.0" jest-junit "^5.1.0" + mustache "^2.3.2" node-mkdirs "^0.0.1" pom-parser "^1.1.1" prettier "^1.14.2" @@ -1867,7 +1868,7 @@ combined-stream@1.0.6, combined-stream@^1.0.5, combined-stream@~1.0.5, combined- dependencies: delayed-stream "~1.0.0" -commander@^2.11.0, commander@^2.17.1, commander@^2.2.0, commander@^2.9.0: +commander@^2.11.0, commander@^2.17.1, commander@^2.2.0, commander@^2.9.0, commander@~2.17.1: version "2.17.1" resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" @@ -3285,7 +3286,20 @@ gulp-sourcemaps@^2.6.4: strip-bom-string "1.X" through2 "2.X" -gulp-util@^3.0.0, gulp-util@^3.0.8: +gulp-uglify@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/gulp-uglify/-/gulp-uglify-3.0.1.tgz#8d3eee466521bea6b10fd75dff72adf8b7ea2d97" + dependencies: + gulplog "^1.0.0" + has-gulplog "^0.1.0" + lodash "^4.13.1" + make-error-cause "^1.1.1" + safe-buffer "^5.1.2" + through2 "^2.0.0" + uglify-js "^3.0.5" + vinyl-sourcemaps-apply "^0.2.0" + +gulp-util@^3.0.0: version "3.0.8" resolved "https://registry.yarnpkg.com/gulp-util/-/gulp-util-3.0.8.tgz#0054e1e744502e27c04c187c3ecc505dd54bbb4f" dependencies: @@ -4733,6 +4747,16 @@ lru-queue@0.1: dependencies: es5-ext "~0.10.2" +make-error-cause@^1.1.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/make-error-cause/-/make-error-cause-1.2.2.tgz#df0388fcd0b37816dff0a5fb8108939777dcbc9d" + dependencies: + make-error "^1.2.0" + +make-error@^1.2.0: + version "1.3.5" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8" + make-iterator@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/make-iterator/-/make-iterator-1.0.1.tgz#29b33f312aa8f547c4a5e490f56afcec99133ad6" @@ -4955,6 +4979,10 @@ multipipe@^0.1.2: dependencies: duplexer2 "0.0.2" +mustache@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/mustache/-/mustache-2.3.2.tgz#a6d4d9c3f91d13359ab889a812954f9230a3d0c5" + mute-stream@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" @@ -6402,7 +6430,7 @@ source-map@^0.4.4: dependencies: amdefine ">=0.0.4" -source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.1, source-map@~0.5.3: +source-map@^0.5.0, source-map@^0.5.1, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.1, source-map@~0.5.3: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" @@ -6857,6 +6885,13 @@ uglify-js@^2.6: optionalDependencies: uglify-to-browserify "~1.0.0" +uglify-js@^3.0.5: + version "3.4.9" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.9.tgz#af02f180c1207d76432e473ed24a28f4a782bae3" + dependencies: + commander "~2.17.1" + source-map "~0.6.1" + uglify-to-browserify@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" @@ -7033,6 +7068,12 @@ vinyl-source-stream@^2.0.0: through2 "^2.0.3" vinyl "^2.1.0" +vinyl-sourcemaps-apply@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz#ab6549d61d172c2b1b87be5c508d239c8ef87705" + dependencies: + source-map "^0.5.1" + vinyl@^0.4.0: version "0.4.6" resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-0.4.6.tgz#2f356c87a550a255461f36bbeb2a5ba8bf784847" diff --git a/scm-plugins/scm-hg-plugin/package.json b/scm-plugins/scm-hg-plugin/package.json index e6de568588..c5907d38bc 100644 --- a/scm-plugins/scm-hg-plugin/package.json +++ b/scm-plugins/scm-hg-plugin/package.json @@ -9,6 +9,6 @@ "@scm-manager/ui-extensions": "^0.0.7" }, "devDependencies": { - "@scm-manager/ui-bundler": "^0.0.13" + "@scm-manager/ui-bundler": "^0.0.15" } } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigInIndexResource.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigInIndexResource.java new file mode 100644 index 0000000000..3de79b2f81 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigInIndexResource.java @@ -0,0 +1,40 @@ +package sonia.scm.api.v2.resources; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import sonia.scm.config.ConfigurationPermissions; +import sonia.scm.plugin.Extension; +import sonia.scm.web.JsonEnricherBase; +import sonia.scm.web.JsonEnricherContext; + +import javax.inject.Inject; +import javax.inject.Provider; + +import static java.util.Collections.singletonMap; +import static sonia.scm.web.VndMediaType.INDEX; + +@Extension +public class HgConfigInIndexResource extends JsonEnricherBase { + + private final Provider scmPathInfoStore; + + @Inject + public HgConfigInIndexResource(Provider scmPathInfoStore, ObjectMapper objectMapper) { + super(objectMapper); + this.scmPathInfoStore = scmPathInfoStore; + } + + @Override + public void enrich(JsonEnricherContext context) { + if (resultHasMediaType(INDEX, context) && ConfigurationPermissions.list().isPermitted()) { + String hgConfigUrl = new LinkBuilder(scmPathInfoStore.get().get(), HgConfigResource.class) + .method("get") + .parameters() + .href(); + + JsonNode hgConfigRefNode = createObject(singletonMap("href", value(hgConfigUrl))); + + addPropertyNode(context.getResponseEntity().get("_links"), "hgConfig", hgConfigRefNode); + } + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigInstallationsToDtoMapper.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigInstallationsToDtoMapper.java index d2f4aecf7e..da980f75c0 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigInstallationsToDtoMapper.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigInstallationsToDtoMapper.java @@ -8,11 +8,11 @@ import static de.otto.edison.hal.Links.linkingTo; public class HgConfigInstallationsToDtoMapper { - private UriInfoStore uriInfoStore; + private ScmPathInfoStore scmPathInfoStore; @Inject - public HgConfigInstallationsToDtoMapper(UriInfoStore uriInfoStore) { - this.uriInfoStore = uriInfoStore; + public HgConfigInstallationsToDtoMapper(ScmPathInfoStore scmPathInfoStore) { + this.scmPathInfoStore = scmPathInfoStore; } public HgConfigInstallationsDto map(List installations, String path) { @@ -20,7 +20,7 @@ public class HgConfigInstallationsToDtoMapper { } private String createSelfLink(String path) { - LinkBuilder linkBuilder = new LinkBuilder(uriInfoStore.get(), HgConfigResource.class); + LinkBuilder linkBuilder = new LinkBuilder(scmPathInfoStore.get(), HgConfigResource.class); return linkBuilder.method("getInstallationsResource").parameters().href() + '/' + path; } } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigPackagesToDtoMapper.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigPackagesToDtoMapper.java index 67d7e58dff..3ee87cef84 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigPackagesToDtoMapper.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigPackagesToDtoMapper.java @@ -20,7 +20,7 @@ import static de.otto.edison.hal.Links.linkingTo; public abstract class HgConfigPackagesToDtoMapper { @Inject - private UriInfoStore uriInfoStore; + private ScmPathInfoStore scmPathInfoStore; public HgConfigPackagesDto map(HgPackages hgpackages) { return map(new HgPackagesNonIterable(hgpackages)); @@ -40,7 +40,7 @@ public abstract class HgConfigPackagesToDtoMapper { } private String createSelfLink() { - LinkBuilder linkBuilder = new LinkBuilder(uriInfoStore.get(), HgConfigResource.class); + LinkBuilder linkBuilder = new LinkBuilder(scmPathInfoStore.get(), HgConfigResource.class); return linkBuilder.method("getPackagesResource").parameters().href(); } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigToHgConfigDtoMapper.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigToHgConfigDtoMapper.java index 98137aebd5..b2a67e2aa4 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigToHgConfigDtoMapper.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/api/v2/resources/HgConfigToHgConfigDtoMapper.java @@ -18,7 +18,7 @@ import static de.otto.edison.hal.Links.linkingTo; public abstract class HgConfigToHgConfigDtoMapper extends BaseMapper { @Inject - private UriInfoStore uriInfoStore; + private ScmPathInfoStore scmPathInfoStore; @AfterMapping void appendLinks(HgConfig config, @MappingTarget HgConfigDto target) { @@ -30,12 +30,12 @@ public abstract class HgConfigToHgConfigDtoMapper extends BaseMapper 0) - { - - if (line.startsWith("a ")) - { - modifications.getAdded().add(line.substring(2)); - } - else if (line.startsWith("m ")) - { - modifications.getModified().add(line.substring(2)); - } - else if (line.startsWith("d ")) - { - modifications.getRemoved().add(line.substring(2)); - } - else if (line.startsWith("t ")) + while (line.length() > 0) { + if (line.startsWith("t ")) { changeset.getTags().add(line.substring(2)); } - line = in.textUpTo('\n'); } - String message = in.textUpTo('\0'); changeset.setDescription(message); @@ -285,6 +263,36 @@ public abstract class AbstractChangesetCommand extends AbstractCommand return changeset; } + protected Modifications readModificationsFromStream(HgInputStream in) { + try { + boolean found = in.find(CHANGESET_PATTERN); + if (found) { + while (!in.match(CHANGESET_PATTERN)) { + return extractModifications(in); + } + } + } catch (IOException e) { + throw new RuntimeIOException(e); + } + return null; + } + + private Modifications extractModifications(HgInputStream in) throws IOException { + Modifications modifications = new Modifications(); + String line = in.textUpTo('\n'); + while (line.length() > 0) { + if (line.startsWith("a ")) { + modifications.getAdded().add(line.substring(2)); + } else if (line.startsWith("m ")) { + modifications.getModified().add(line.substring(2)); + } else if (line.startsWith("d ")) { + modifications.getRemoved().add(line.substring(2)); + } + line = in.textUpTo('\n'); + } + return modifications; + } + /** * Method description * diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgLogChangesetCommand.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgLogChangesetCommand.java index 4c62fb4f93..f57c2a63d9 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgLogChangesetCommand.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/repository/spi/javahg/HgLogChangesetCommand.java @@ -38,14 +38,14 @@ package sonia.scm.repository.spi.javahg; import com.aragost.javahg.Repository; import com.aragost.javahg.internals.HgInputStream; import com.aragost.javahg.internals.Utils; - import sonia.scm.repository.Changeset; import sonia.scm.repository.HgConfig; - -//~--- JDK imports ------------------------------------------------------------ +import sonia.scm.repository.Modifications; import java.util.List; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -106,11 +106,22 @@ public class HgLogChangesetCommand extends AbstractChangesetCommand */ public List execute(String... files) { - cmdAppend("--style", CHANGESET_EAGER_STYLE_PATH); + return readListFromStream(getHgInputStream(files, CHANGESET_EAGER_STYLE_PATH)); + } - HgInputStream stream = launchStream(files); + /** + * Extract Modifications from the Repository files + * + * @param files repo files + * @return modifications + */ + public Modifications extractModifications(String... files) { + return readModificationsFromStream(getHgInputStream(files, CHANGESET_EAGER_STYLE_PATH)); + } - return readListFromStream(stream); + HgInputStream getHgInputStream(String[] files, String changesetStylePath) { + cmdAppend("--style", changesetStylePath); + return launchStream(files); } /** @@ -138,11 +149,7 @@ public class HgLogChangesetCommand extends AbstractChangesetCommand */ public List loadRevisions(String... files) { - cmdAppend("--style", CHANGESET_LAZY_STYLE_PATH); - - HgInputStream stream = launchStream(files); - - return loadRevisionsFromStream(stream); + return loadRevisionsFromStream(getHgInputStream(files, CHANGESET_LAZY_STYLE_PATH)); } /** diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgBasicAuthenticationFilter.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgBasicAuthenticationFilter.java deleted file mode 100644 index abc295fd68..0000000000 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgBasicAuthenticationFilter.java +++ /dev/null @@ -1,102 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. 2. Redistributions in - * binary form must reproduce the above copyright notice, this list of - * conditions and the following disclaimer in the documentation and/or other - * materials provided with the distribution. 3. Neither the name of SCM-Manager; - * nor the names of its contributors may be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - - -package sonia.scm.web; - -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.inject.Inject; - -import sonia.scm.Priority; -import sonia.scm.config.ScmConfiguration; -import sonia.scm.filter.Filters; -import sonia.scm.filter.WebElement; -import sonia.scm.web.filter.AuthenticationFilter; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; - -import java.util.Set; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -/** - * - * @author Sebastian Sdorra - */ -@Priority(Filters.PRIORITY_AUTHENTICATION) -@WebElement(value = HgServletModule.MAPPING_HG) -public class HgBasicAuthenticationFilter extends AuthenticationFilter -{ - - /** - * Constructs ... - * - * - * @param configuration - * @param webTokenGenerators - */ - @Inject - public HgBasicAuthenticationFilter(ScmConfiguration configuration, - Set webTokenGenerators) - { - super(configuration, webTokenGenerators); - } - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param request - * @param response - * - * @throws IOException - */ - @Override - protected void sendFailedAuthenticationError(HttpServletRequest request, - HttpServletResponse response) - throws IOException - { - if (HgUtil.isHgClient(request) - && (configuration.isLoginAttemptLimitEnabled())) - { - response.sendError(HttpServletResponse.SC_UNAUTHORIZED); - } - else - { - super.sendFailedAuthenticationError(request, response); - } - } -} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgCGIServlet.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgCGIServlet.java index 6cb4d523f0..1821f92fa4 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgCGIServlet.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgCGIServlet.java @@ -33,8 +33,6 @@ package sonia.scm.web; -//~--- non-JDK imports -------------------------------------------------------- - import com.google.common.base.Stopwatch; import com.google.common.base.Strings; import com.google.inject.Inject; @@ -49,8 +47,8 @@ import sonia.scm.repository.HgHookManager; import sonia.scm.repository.HgPythonScript; import sonia.scm.repository.HgRepositoryHandler; import sonia.scm.repository.Repository; -import sonia.scm.repository.RepositoryProvider; import sonia.scm.repository.RepositoryRequestListenerUtil; +import sonia.scm.repository.spi.ScmProviderHttpServlet; import sonia.scm.security.CipherUtil; import sonia.scm.util.AssertUtil; import sonia.scm.util.HttpUtil; @@ -68,14 +66,12 @@ import java.io.IOException; import java.util.Base64; import java.util.Enumeration; -//~--- JDK imports ------------------------------------------------------------ - /** * * @author Sebastian Sdorra */ @Singleton -public class HgCGIServlet extends HttpServlet +public class HgCGIServlet extends HttpServlet implements ScmProviderHttpServlet { /** Field description */ @@ -108,20 +104,18 @@ public class HgCGIServlet extends HttpServlet * * @param cgiExecutorFactory * @param configuration - * @param repositoryProvider * @param handler * @param hookManager * @param requestListenerUtil */ @Inject public HgCGIServlet(CGIExecutorFactory cgiExecutorFactory, - ScmConfiguration configuration, RepositoryProvider repositoryProvider, + ScmConfiguration configuration, HgRepositoryHandler handler, HgHookManager hookManager, RepositoryRequestListenerUtil requestListenerUtil) { this.cgiExecutorFactory = cgiExecutorFactory; this.configuration = configuration; - this.repositoryProvider = repositoryProvider; this.handler = handler; this.hookManager = hookManager; this.requestListenerUtil = requestListenerUtil; @@ -131,46 +125,11 @@ public class HgCGIServlet extends HttpServlet //~--- methods -------------------------------------------------------------- - /** - * Method description - * - * - * @throws ServletException - */ @Override - public void init() throws ServletException + public void service(HttpServletRequest request, + HttpServletResponse response, Repository repository) { - - super.init(); - } - - /** - * Method description - * - * - * @param request - * @param response - * - * @throws IOException - * @throws ServletException - */ - @Override - protected void service(HttpServletRequest request, - HttpServletResponse response) - throws ServletException, IOException - { - Repository repository = repositoryProvider.get(); - - if (repository == null) - { - if (logger.isDebugEnabled()) - { - logger.debug("no hg repository found at {}", request.getRequestURI()); - } - - response.setStatus(HttpServletResponse.SC_NOT_FOUND); - } - else if (!handler.isConfigured()) + if (!handler.isConfigured()) { exceptionHandler.sendFormattedError(request, response, HgCGIExceptionHandler.ERROR_NOT_CONFIGURED); @@ -379,9 +338,6 @@ public class HgCGIServlet extends HttpServlet /** Field description */ private final HgHookManager hookManager; - /** Field description */ - private final RepositoryProvider repositoryProvider; - /** Field description */ private final RepositoryRequestListenerUtil requestListenerUtil; } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgCGIServletProvider.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgCGIServletProvider.java new file mode 100644 index 0000000000..db7a6be7b3 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgCGIServletProvider.java @@ -0,0 +1,23 @@ +package sonia.scm.web; + +import com.google.inject.Inject; +import sonia.scm.repository.HgRepositoryHandler; +import sonia.scm.repository.spi.ScmProviderHttpServlet; +import sonia.scm.repository.spi.ScmProviderHttpServletProvider; + +import javax.inject.Provider; + +public class HgCGIServletProvider extends ScmProviderHttpServletProvider { + + @Inject + private Provider servletProvider; + + public HgCGIServletProvider() { + super(HgRepositoryHandler.TYPE_NAME); + } + + @Override + protected ScmProviderHttpServlet getRootServlet() { + return servletProvider.get(); + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilter.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilter.java index de2835dd8f..7f92cc0357 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilter.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilter.java @@ -33,53 +33,31 @@ package sonia.scm.web; -//~--- non-JDK imports -------------------------------------------------------- - import com.google.common.collect.ImmutableSet; -import com.google.inject.Inject; - -import sonia.scm.Priority; import sonia.scm.config.ScmConfiguration; -import sonia.scm.filter.Filters; -import sonia.scm.filter.WebElement; -import sonia.scm.repository.RepositoryProvider; -import sonia.scm.web.filter.ProviderPermissionFilter; - -//~--- JDK imports ------------------------------------------------------------ - -import java.util.Set; +import sonia.scm.repository.spi.ScmProviderHttpServlet; +import sonia.scm.web.filter.PermissionFilter; import javax.servlet.http.HttpServletRequest; +import java.util.Set; /** * Permission filter for mercurial repositories. * * @author Sebastian Sdorra */ -@Priority(Filters.PRIORITY_AUTHORIZATION) -@WebElement(value = HgServletModule.MAPPING_HG) -public class HgPermissionFilter extends ProviderPermissionFilter +public class HgPermissionFilter extends PermissionFilter { private static final Set READ_METHODS = ImmutableSet.of("GET", "HEAD", "OPTIONS", "TRACE"); - /** - * Constructs a new instance. - * - * @param configuration scm configuration - * @param repositoryProvider repository provider - */ - @Inject - public HgPermissionFilter(ScmConfiguration configuration, - RepositoryProvider repositoryProvider) + public HgPermissionFilter(ScmConfiguration configuration, ScmProviderHttpServlet delegate) { - super(configuration, repositoryProvider); + super(configuration, delegate); } - //~--- get methods ---------------------------------------------------------- - @Override - protected boolean isWriteRequest(HttpServletRequest request) + public boolean isWriteRequest(HttpServletRequest request) { return !READ_METHODS.contains(request.getMethod()); } diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilterFactory.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilterFactory.java new file mode 100644 index 0000000000..90f53a1fea --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgPermissionFilterFactory.java @@ -0,0 +1,30 @@ +package sonia.scm.web; + +import sonia.scm.config.ScmConfiguration; +import sonia.scm.plugin.Extension; +import sonia.scm.repository.HgRepositoryHandler; +import sonia.scm.repository.spi.ScmProviderHttpServlet; +import sonia.scm.repository.spi.ScmProviderHttpServletDecoratorFactory; + +import javax.inject.Inject; + +@Extension +public class HgPermissionFilterFactory implements ScmProviderHttpServletDecoratorFactory { + + private final ScmConfiguration configuration; + + @Inject + public HgPermissionFilterFactory(ScmConfiguration configuration) { + this.configuration = configuration; + } + + @Override + public boolean handlesScmType(String type) { + return HgRepositoryHandler.TYPE_NAME.equals(type); + } + + @Override + public ScmProviderHttpServlet createDecorator(ScmProviderHttpServlet delegate) { + return new HgPermissionFilter(configuration, delegate); + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgScmProtocolProviderWrapper.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgScmProtocolProviderWrapper.java new file mode 100644 index 0000000000..37360a5845 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgScmProtocolProviderWrapper.java @@ -0,0 +1,25 @@ +package sonia.scm.web; + +import sonia.scm.api.v2.resources.ScmPathInfoStore; +import sonia.scm.config.ScmConfiguration; +import sonia.scm.plugin.Extension; +import sonia.scm.repository.HgRepositoryHandler; +import sonia.scm.repository.spi.InitializingHttpScmProtocolWrapper; + +import javax.inject.Inject; +import javax.inject.Provider; +import javax.inject.Singleton; + +@Singleton +@Extension +public class HgScmProtocolProviderWrapper extends InitializingHttpScmProtocolWrapper { + @Inject + public HgScmProtocolProviderWrapper(HgCGIServletProvider servletProvider, Provider uriInfoStore, ScmConfiguration scmConfiguration) { + super(servletProvider, uriInfoStore, scmConfiguration); + } + + @Override + public String getType() { + return HgRepositoryHandler.TYPE_NAME; + } +} diff --git a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgServletModule.java b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgServletModule.java index 357995483d..ba9ae3a0b9 100644 --- a/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgServletModule.java +++ b/scm-plugins/scm-hg-plugin/src/main/java/sonia/scm/web/HgServletModule.java @@ -81,8 +81,5 @@ public class HgServletModule extends ServletModule // bind servlets serve(MAPPING_HOOK).with(HgHookCallbackServlet.class); - - // register hg cgi servlet - serve(MAPPING_HG).with(HgCGIServlet.class); } } diff --git a/scm-plugins/scm-hg-plugin/src/main/js/HgAvatar.js b/scm-plugins/scm-hg-plugin/src/main/js/HgAvatar.js index 27bc10808e..ce10ec293c 100644 --- a/scm-plugins/scm-hg-plugin/src/main/js/HgAvatar.js +++ b/scm-plugins/scm-hg-plugin/src/main/js/HgAvatar.js @@ -1,6 +1,6 @@ //@flow import React from "react"; -import { Image } from "@scm-manager/ui-components"; +import {Image} from "@scm-manager/ui-components"; type Props = { }; @@ -8,7 +8,7 @@ type Props = { class HgAvatar extends React.Component { render() { - return Mercurial Logo; + return Mercurial Logo; } } diff --git a/scm-plugins/scm-hg-plugin/src/main/js/ProtocolInformation.js b/scm-plugins/scm-hg-plugin/src/main/js/ProtocolInformation.js index 28c1e53a07..03fc41450a 100644 --- a/scm-plugins/scm-hg-plugin/src/main/js/ProtocolInformation.js +++ b/scm-plugins/scm-hg-plugin/src/main/js/ProtocolInformation.js @@ -1,5 +1,6 @@ //@flow import React from "react"; +import { repositories } from "@scm-manager/ui-components"; import type { Repository } from "@scm-manager/ui-types"; type Props = { @@ -10,14 +11,15 @@ class ProtocolInformation extends React.Component { render() { const { repository } = this.props; - if (!repository._links.httpProtocol) { + const href = repositories.getProtocolLinkByType(repository, "http"); + if (!href) { return null; } return (

Clone the repository

-          hg clone {repository._links.httpProtocol.href}
+          hg clone {href}
         

Create a new repository

@@ -26,7 +28,7 @@ class ProtocolInformation extends React.Component {
             
echo "[paths]" > .hg/hgrc
- echo "default = {repository._links.httpProtocol.href}" > .hg/hgrc + echo "default = {href}" > .hg/hgrc
echo "# {repository.name}" > README.md
@@ -44,7 +46,7 @@ class ProtocolInformation extends React.Component { # add the repository url as default to your .hg/hgrc e.g:
- default = {repository._links.httpProtocol.href} + default = {href}
# push to remote repository
diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInIndexResourceTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInIndexResourceTest.java new file mode 100644 index 0000000000..27ab74932c --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInIndexResourceTest.java @@ -0,0 +1,64 @@ +package sonia.scm.api.v2.resources; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; +import com.google.inject.util.Providers; +import org.junit.Rule; +import org.junit.Test; +import sonia.scm.web.JsonEnricherContext; +import sonia.scm.web.VndMediaType; + +import javax.ws.rs.core.MediaType; +import java.net.URI; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +@SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini") +public class HgConfigInIndexResourceTest { + + @Rule + public final ShiroRule shiroRule = new ShiroRule(); + + private final ObjectMapper objectMapper = new ObjectMapper(); + private final ObjectNode root = objectMapper.createObjectNode(); + private final HgConfigInIndexResource hgConfigInIndexResource; + + public HgConfigInIndexResourceTest() { + root.put("_links", objectMapper.createObjectNode()); + ScmPathInfoStore pathInfoStore = new ScmPathInfoStore(); + pathInfoStore.set(() -> URI.create("/")); + hgConfigInIndexResource = new HgConfigInIndexResource(Providers.of(pathInfoStore), objectMapper); + } + + @Test + @SubjectAware(username = "admin", password = "secret") + public void admin() { + JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root); + + hgConfigInIndexResource.enrich(context); + + assertEquals("/v2/config/hg", root.get("_links").get("hgConfig").get("href").asText()); + } + + @Test + @SubjectAware(username = "readOnly", password = "secret") + public void user() { + JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root); + + hgConfigInIndexResource.enrich(context); + + assertFalse(root.get("_links").iterator().hasNext()); + } + + @Test + public void anonymous() { + JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root); + + hgConfigInIndexResource.enrich(context); + + assertFalse(root.get("_links").iterator().hasNext()); + } +} diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInstallationsResourceTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInstallationsResourceTest.java index 540a5b6757..65b9c262cb 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInstallationsResourceTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInstallationsResourceTest.java @@ -43,7 +43,7 @@ public class HgConfigInstallationsResourceTest { private final URI baseUri = URI.create("/"); @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private UriInfoStore uriInfoStore; + private ScmPathInfoStore scmPathInfoStore; @InjectMocks private HgConfigInstallationsToDtoMapper mapper; @@ -61,7 +61,7 @@ public class HgConfigInstallationsResourceTest { new HgConfigResource(null, null, null, null, null, resourceProvider)); - when(uriInfoStore.get().getBaseUri()).thenReturn(baseUri); + when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri); } @Test diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInstallationsToDtoMapperTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInstallationsToDtoMapperTest.java index 34048f80b2..7cae1d9f7e 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInstallationsToDtoMapperTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigInstallationsToDtoMapperTest.java @@ -23,7 +23,7 @@ public class HgConfigInstallationsToDtoMapperTest { private URI baseUri = URI.create("http://example.com/base/"); @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private UriInfoStore uriInfoStore; + private ScmPathInfoStore scmPathInfoStore; @InjectMocks private HgConfigInstallationsToDtoMapper mapper; @@ -34,7 +34,7 @@ public class HgConfigInstallationsToDtoMapperTest { @Before public void init() { - when(uriInfoStore.get().getBaseUri()).thenReturn(baseUri); + when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri); expectedBaseUri = baseUri.resolve(HgConfigResource.HG_CONFIG_PATH_V2 + "/installations/" + expectedPath); } diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigPackageResourceTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigPackageResourceTest.java index 044897ad80..f1558b6efb 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigPackageResourceTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigPackageResourceTest.java @@ -57,7 +57,7 @@ public class HgConfigPackageResourceTest { private final URI baseUri = java.net.URI.create("/"); @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private UriInfoStore uriInfoStore; + private ScmPathInfoStore scmPathInfoStore; @InjectMocks private HgConfigPackagesToDtoMapperImpl mapper; @@ -81,7 +81,7 @@ public class HgConfigPackageResourceTest { public void prepareEnvironment() { setupResources(); - when(uriInfoStore.get().getBaseUri()).thenReturn(baseUri); + when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri); when(hgPackageReader.getPackages().getPackages()).thenReturn(createPackages()); } diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigPackagesToDtoMapperTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigPackagesToDtoMapperTest.java index 671d9fb7e1..c4431da6d5 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigPackagesToDtoMapperTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigPackagesToDtoMapperTest.java @@ -1,6 +1,5 @@ package sonia.scm.api.v2.resources; -import de.otto.edison.hal.HalRepresentation; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -13,12 +12,10 @@ import sonia.scm.installer.HgPackages; import java.net.URI; import java.util.Arrays; -import java.util.Collection; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.when; import static sonia.scm.api.v2.resources.HgConfigTests.assertEqualsPackage; import static sonia.scm.api.v2.resources.HgConfigTests.createPackage; @@ -29,7 +26,7 @@ public class HgConfigPackagesToDtoMapperTest { private URI baseUri = URI.create("http://example.com/base/"); @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private UriInfoStore uriInfoStore; + private ScmPathInfoStore scmPathInfoStore; @InjectMocks private HgConfigPackagesToDtoMapperImpl mapper; @@ -38,7 +35,7 @@ public class HgConfigPackagesToDtoMapperTest { @Before public void init() { - when(uriInfoStore.get().getBaseUri()).thenReturn(baseUri); + when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri); expectedBaseUri = baseUri.resolve(HgConfigResource.HG_CONFIG_PATH_V2 + "/packages"); } diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigResourceTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigResourceTest.java index 11a0fb55f9..9cd04a0789 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigResourceTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigResourceTest.java @@ -54,7 +54,7 @@ public class HgConfigResourceTest { private HgConfigDtoToHgConfigMapperImpl dtoToConfigMapper; @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private UriInfoStore uriInfoStore; + private ScmPathInfoStore scmPathInfoStore; @InjectMocks private HgConfigToHgConfigDtoMapperImpl configToDtoMapper; @@ -79,7 +79,7 @@ public class HgConfigResourceTest { new HgConfigResource(dtoToConfigMapper, configToDtoMapper, repositoryHandler, packagesResource, autoconfigResource, installationsResource); dispatcher.getRegistry().addSingletonResource(gitConfigResource); - when(uriInfoStore.get().getBaseUri()).thenReturn(baseUri); + when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri); } @Test diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigToHgConfigDtoMapperTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigToHgConfigDtoMapperTest.java index a12e95926c..81c50f3d58 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigToHgConfigDtoMapperTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/api/v2/resources/HgConfigToHgConfigDtoMapperTest.java @@ -29,7 +29,7 @@ public class HgConfigToHgConfigDtoMapperTest { private URI baseUri = URI.create("http://example.com/base/"); @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private UriInfoStore uriInfoStore; + private ScmPathInfoStore scmPathInfoStore; @InjectMocks private HgConfigToHgConfigDtoMapperImpl mapper; @@ -41,7 +41,7 @@ public class HgConfigToHgConfigDtoMapperTest { @Before public void init() { - when(uriInfoStore.get().getBaseUri()).thenReturn(baseUri); + when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri); expectedBaseUri = baseUri.resolve(HgConfigResource.HG_CONFIG_PATH_V2); subjectThreadState.bind(); ThreadContext.bind(subject); diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgCommitCommand.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgCommitCommand.java index aa611a8ba4..b8967ed15b 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgCommitCommand.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/client/spi/HgCommitCommand.java @@ -32,11 +32,11 @@ package sonia.scm.repository.client.spi; import com.aragost.javahg.Repository; import com.google.common.collect.Lists; -import java.io.IOException; import sonia.scm.repository.Changeset; -import sonia.scm.repository.Modifications; import sonia.scm.repository.Person; +import java.io.IOException; + /** * Mercurial implementation of the {@link CommitCommand}. * @@ -70,9 +70,6 @@ public class HgCommitCommand implements CommitCommand changeset.setBranches(Lists.newArrayList(c.getBranch())); changeset.setTags(c.tags()); - changeset.setModifications( - new Modifications(c.getAddedFiles(), c.getModifiedFiles(), c.getDeletedFiles()) - ); return changeset; } diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgLogCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgLogCommandTest.java index c4d49ba8b5..99e9fc191a 100644 --- a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgLogCommandTest.java +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgLogCommandTest.java @@ -39,6 +39,9 @@ import org.junit.Test; import sonia.scm.repository.Changeset; import sonia.scm.repository.ChangesetPagingResult; import sonia.scm.repository.Modifications; +import sonia.scm.repository.RevisionNotFoundException; + +import java.io.IOException; import static org.hamcrest.Matchers.contains; import static org.junit.Assert.assertEquals; @@ -133,27 +136,28 @@ public class HgLogCommandTest extends AbstractHgCommandTestBase } @Test - public void testGetCommit() { + public void testGetCommit() throws IOException, RevisionNotFoundException { HgLogCommand command = createComamnd(); + String revision = "a9bacaf1b7fa0cebfca71fed4e59ed69a6319427"; Changeset c = - command.getChangeset("a9bacaf1b7fa0cebfca71fed4e59ed69a6319427"); + command.getChangeset(revision); assertNotNull(c); - assertEquals("a9bacaf1b7fa0cebfca71fed4e59ed69a6319427", c.getId()); + assertEquals(revision, c.getId()); assertEquals("added a and b files", c.getDescription()); checkDate(c.getDate()); assertEquals("Douglas Adams", c.getAuthor().getName()); assertEquals("douglas.adams@hitchhiker.com", c.getAuthor().getMail()); assertEquals("added a and b files", c.getDescription()); + ModificationsCommand modificationsCommand = new HgModificationsCommand(cmdContext, repository); + Modifications modifications = modificationsCommand.getModifications(revision); - Modifications mods = c.getModifications(); - - assertNotNull(mods); - assertTrue("modified list should be empty", mods.getModified().isEmpty()); - assertTrue("removed list should be empty", mods.getRemoved().isEmpty()); - assertFalse("added list should not be empty", mods.getAdded().isEmpty()); - assertEquals(2, mods.getAdded().size()); - assertThat(mods.getAdded(), contains("a.txt", "b.txt")); + assertNotNull(modifications); + assertTrue("modified list should be empty", modifications.getModified().isEmpty()); + assertTrue("removed list should be empty", modifications.getRemoved().isEmpty()); + assertFalse("added list should not be empty", modifications.getAdded().isEmpty()); + assertEquals(2, modifications.getAdded().size()); + assertThat(modifications.getAdded(), contains("a.txt", "b.txt")); } @Test diff --git a/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgModificationsCommandTest.java b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgModificationsCommandTest.java new file mode 100644 index 0000000000..eae8319b89 --- /dev/null +++ b/scm-plugins/scm-hg-plugin/src/test/java/sonia/scm/repository/spi/HgModificationsCommandTest.java @@ -0,0 +1,111 @@ +package sonia.scm.repository.spi; + +import com.aragost.javahg.Changeset; +import com.aragost.javahg.commands.RemoveCommand; +import org.junit.Before; +import org.junit.Test; +import sonia.scm.repository.HgTestUtil; +import sonia.scm.repository.Modifications; + +import java.io.File; +import java.util.function.Consumer; + +import static org.assertj.core.api.Java6Assertions.assertThat; + +public class HgModificationsCommandTest extends IncomingOutgoingTestBase { + + + private HgModificationsCommand outgoingModificationsCommand; + + @Before + public void init() { + HgCommandContext outgoingContext = new HgCommandContext(HgTestUtil.createHookManager(), handler, outgoingRepository, outgoingDirectory); + outgoingModificationsCommand = new HgModificationsCommand(outgoingContext, outgoingRepository); + } + + @Test + public void shouldReadAddedFiles() throws Exception { + String fileName = "a.txt"; + writeNewFile(outgoing, outgoingDirectory, fileName, "bal bla"); + Changeset changeset = commit(outgoing, "added a.txt"); + String revision = String.valueOf(changeset.getRevision()); + Consumer assertModifications = assertAddedFile(fileName); + assertModifications.accept(outgoingModificationsCommand.getModifications(revision)); + } + + @Test + public void shouldReadModifiedFiles() throws Exception { + String fileName = "a.txt"; + writeNewFile(outgoing, outgoingDirectory, fileName, "bal bla"); + commit(outgoing, "added a.txt"); + writeNewFile(outgoing, outgoingDirectory, fileName, "new content"); + Changeset changeset = commit(outgoing, "modified a.txt"); + String revision = String.valueOf(changeset.getRevision()); + Consumer assertModifications = assertModifiedFiles(fileName); + assertModifications.accept(outgoingModificationsCommand.getModifications(revision)); + } + + @Test + public void shouldReadRemovedFiles() throws Exception { + String fileName = "a.txt"; + writeNewFile(outgoing, outgoingDirectory, fileName, "bal bla"); + commit(outgoing, "added a.txt"); + File file = new File(outgoingDirectory, fileName); + file.delete(); + RemoveCommand.on(outgoing).execute(file); + Changeset changeset = commit(outgoing, "removed a.txt"); + String revision = String.valueOf(changeset.getRevision()); + Consumer assertModifications = assertRemovedFiles(fileName); + assertModifications.accept(outgoingModificationsCommand.getModifications(revision)); + } + + + Consumer assertRemovedFiles(String fileName) { + return (modifications) -> { + assertThat(modifications).isNotNull(); + assertThat(modifications.getAdded()) + .as("added files modifications") + .hasSize(0); + assertThat(modifications.getModified()) + .as("modified files modifications") + .hasSize(0); + assertThat(modifications.getRemoved()) + .as("removed files modifications") + .hasSize(1) + .containsOnly(fileName); + }; + } + + + Consumer assertModifiedFiles(String file) { + return (modifications) -> { + assertThat(modifications).isNotNull(); + assertThat(modifications.getAdded()) + .as("added files modifications") + .hasSize(0); + assertThat(modifications.getModified()) + .as("modified files modifications") + .hasSize(1) + .containsOnly(file); + assertThat(modifications.getRemoved()) + .as("removed files modifications") + .hasSize(0); + }; + } + + Consumer assertAddedFile(String addedFile) { + return (modifications) -> { + assertThat(modifications).isNotNull(); + assertThat(modifications.getAdded()) + .as("added files modifications") + .hasSize(1) + .containsOnly(addedFile); + assertThat(modifications.getModified()) + .as("modified files modifications") + .hasSize(0); + assertThat(modifications.getRemoved()) + .as("removed files modifications") + .hasSize(0); + }; + } +} diff --git a/scm-plugins/scm-hg-plugin/src/test/resources/sonia/scm/configuration/shiro.ini b/scm-plugins/scm-hg-plugin/src/test/resources/sonia/scm/configuration/shiro.ini index fc08bb83ac..d8083a04c9 100644 --- a/scm-plugins/scm-hg-plugin/src/test/resources/sonia/scm/configuration/shiro.ini +++ b/scm-plugins/scm-hg-plugin/src/test/resources/sonia/scm/configuration/shiro.ini @@ -2,8 +2,10 @@ readOnly = secret, reader writeOnly = secret, writer readWrite = secret, readerWriter +admin = secret, admin [roles] reader = configuration:read:hg writer = configuration:write:hg readerWriter = configuration:*:hg +admin = * diff --git a/scm-plugins/scm-hg-plugin/yarn.lock b/scm-plugins/scm-hg-plugin/yarn.lock index 251b5241a2..8822bd5e57 100644 --- a/scm-plugins/scm-hg-plugin/yarn.lock +++ b/scm-plugins/scm-hg-plugin/yarn.lock @@ -641,9 +641,9 @@ version "0.0.2" resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85" -"@scm-manager/ui-bundler@^0.0.13": - version "0.0.13" - resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.13.tgz#020e6c8ee870fccb6c451490cb18972ebfb0d2c4" +"@scm-manager/ui-bundler@^0.0.15": + version "0.0.15" + resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.15.tgz#8ed4a557d5ae38d6b99493b29608fd6a4c9cd917" dependencies: "@babel/core" "^7.0.0" "@babel/plugin-proposal-class-properties" "^7.0.0" @@ -671,9 +671,10 @@ flow-bin "^0.79.1" gulp "^3.9.1" gulp-sourcemaps "^2.6.4" - gulp-util "^3.0.8" + gulp-uglify "^3.0.1" jest "^23.5.0" jest-junit "^5.1.0" + mustache "^2.3.2" node-mkdirs "^0.0.1" pom-parser "^1.1.1" prettier "^1.14.2" @@ -1801,7 +1802,7 @@ combined-stream@1.0.6, combined-stream@^1.0.5, combined-stream@~1.0.5, combined- dependencies: delayed-stream "~1.0.0" -commander@^2.11.0, commander@^2.17.1, commander@^2.2.0, commander@^2.9.0: +commander@^2.11.0, commander@^2.17.1, commander@^2.2.0, commander@^2.9.0, commander@~2.17.1: version "2.17.1" resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" @@ -3217,7 +3218,20 @@ gulp-sourcemaps@^2.6.4: strip-bom-string "1.X" through2 "2.X" -gulp-util@^3.0.0, gulp-util@^3.0.8: +gulp-uglify@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/gulp-uglify/-/gulp-uglify-3.0.1.tgz#8d3eee466521bea6b10fd75dff72adf8b7ea2d97" + dependencies: + gulplog "^1.0.0" + has-gulplog "^0.1.0" + lodash "^4.13.1" + make-error-cause "^1.1.1" + safe-buffer "^5.1.2" + through2 "^2.0.0" + uglify-js "^3.0.5" + vinyl-sourcemaps-apply "^0.2.0" + +gulp-util@^3.0.0: version "3.0.8" resolved "https://registry.yarnpkg.com/gulp-util/-/gulp-util-3.0.8.tgz#0054e1e744502e27c04c187c3ecc505dd54bbb4f" dependencies: @@ -4636,6 +4650,16 @@ lru-queue@0.1: dependencies: es5-ext "~0.10.2" +make-error-cause@^1.1.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/make-error-cause/-/make-error-cause-1.2.2.tgz#df0388fcd0b37816dff0a5fb8108939777dcbc9d" + dependencies: + make-error "^1.2.0" + +make-error@^1.2.0: + version "1.3.5" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8" + make-iterator@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/make-iterator/-/make-iterator-1.0.1.tgz#29b33f312aa8f547c4a5e490f56afcec99133ad6" @@ -4868,6 +4892,10 @@ multipipe@^0.1.2: dependencies: duplexer2 "0.0.2" +mustache@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/mustache/-/mustache-2.3.2.tgz#a6d4d9c3f91d13359ab889a812954f9230a3d0c5" + mute-stream@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" @@ -6283,7 +6311,7 @@ source-map@^0.4.4: dependencies: amdefine ">=0.0.4" -source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.1, source-map@~0.5.3: +source-map@^0.5.0, source-map@^0.5.1, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.1, source-map@~0.5.3: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" @@ -6739,6 +6767,13 @@ uglify-js@^2.6: optionalDependencies: uglify-to-browserify "~1.0.0" +uglify-js@^3.0.5: + version "3.4.9" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.9.tgz#af02f180c1207d76432e473ed24a28f4a782bae3" + dependencies: + commander "~2.17.1" + source-map "~0.6.1" + uglify-to-browserify@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" @@ -6915,6 +6950,12 @@ vinyl-source-stream@^2.0.0: through2 "^2.0.3" vinyl "^2.1.0" +vinyl-sourcemaps-apply@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz#ab6549d61d172c2b1b87be5c508d239c8ef87705" + dependencies: + source-map "^0.5.1" + vinyl@^0.4.0: version "0.4.6" resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-0.4.6.tgz#2f356c87a550a255461f36bbeb2a5ba8bf784847" diff --git a/scm-plugins/scm-svn-plugin/package.json b/scm-plugins/scm-svn-plugin/package.json index 98f0394833..118108c882 100644 --- a/scm-plugins/scm-svn-plugin/package.json +++ b/scm-plugins/scm-svn-plugin/package.json @@ -9,6 +9,6 @@ "@scm-manager/ui-extensions": "^0.0.7" }, "devDependencies": { - "@scm-manager/ui-bundler": "^0.0.13" + "@scm-manager/ui-bundler": "^0.0.15" } } diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigInIndexResource.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigInIndexResource.java new file mode 100644 index 0000000000..5ee1de3169 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigInIndexResource.java @@ -0,0 +1,40 @@ +package sonia.scm.api.v2.resources; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import sonia.scm.config.ConfigurationPermissions; +import sonia.scm.plugin.Extension; +import sonia.scm.web.JsonEnricherBase; +import sonia.scm.web.JsonEnricherContext; + +import javax.inject.Inject; +import javax.inject.Provider; + +import static java.util.Collections.singletonMap; +import static sonia.scm.web.VndMediaType.INDEX; + +@Extension +public class SvnConfigInIndexResource extends JsonEnricherBase { + + private final Provider scmPathInfoStore; + + @Inject + public SvnConfigInIndexResource(Provider scmPathInfoStore, ObjectMapper objectMapper) { + super(objectMapper); + this.scmPathInfoStore = scmPathInfoStore; + } + + @Override + public void enrich(JsonEnricherContext context) { + if (resultHasMediaType(INDEX, context) && ConfigurationPermissions.list().isPermitted()) { + String svnConfigUrl = new LinkBuilder(scmPathInfoStore.get().get(), SvnConfigResource.class) + .method("get") + .parameters() + .href(); + + JsonNode svnConfigRefNode = createObject(singletonMap("href", value(svnConfigUrl))); + + addPropertyNode(context.getResponseEntity().get("_links"), "svnConfig", svnConfigRefNode); + } + } +} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigToSvnConfigDtoMapper.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigToSvnConfigDtoMapper.java index a71d75151d..c160280822 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigToSvnConfigDtoMapper.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/api/v2/resources/SvnConfigToSvnConfigDtoMapper.java @@ -18,7 +18,7 @@ import static de.otto.edison.hal.Links.linkingTo; public abstract class SvnConfigToSvnConfigDtoMapper extends BaseMapper { @Inject - private UriInfoStore uriInfoStore; + private ScmPathInfoStore scmPathInfoStore; @AfterMapping void appendLinks(SvnConfig config, @MappingTarget SvnConfigDto target) { @@ -30,12 +30,12 @@ public abstract class SvnConfigToSvnConfigDtoMapper extends BaseMapper changeMap = entry.getChangedPaths(); + + if (Util.isNotEmpty(changeMap)) { + + for (SVNLogEntryPath e : changeMap.values()) { + appendModification(modifications, e.getType(), e.getPath()); + } + } + return modifications; } /** @@ -210,19 +216,6 @@ public final class SvnUtil { changeset.getParents().add(String.valueOf(revision - 1)); } - - Map changeMap = entry.getChangedPaths(); - - if (Util.isNotEmpty(changeMap)) - { - Modifications modifications = changeset.getModifications(); - - for (SVNLogEntryPath e : changeMap.values()) - { - appendModification(modifications, e); - } - } - return changeset; } diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnLogCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnLogCommand.java index ededb4e1f0..332dcb55a6 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnLogCommand.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnLogCommand.java @@ -54,6 +54,8 @@ import sonia.scm.util.Util; import java.util.Collection; import java.util.List; +import static sonia.scm.repository.SvnUtil.parseRevision; + //~--- JDK imports ------------------------------------------------------------ public class SvnLogCommand extends AbstractSvnCommand implements LogCommand @@ -144,25 +146,6 @@ public class SvnLogCommand extends AbstractSvnCommand implements LogCommand return changesets; } - //~--- methods -------------------------------------------------------------- - - private long parseRevision(String v) throws RevisionNotFoundException { - long result = -1l; - - if (!Strings.isNullOrEmpty(v)) - { - try - { - result = Long.parseLong(v); - } - catch (NumberFormatException ex) - { - throw new RevisionNotFoundException(v); - } - } - - return result; - } //~--- get methods ---------------------------------------------------------- diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnModificationsCommand.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnModificationsCommand.java new file mode 100644 index 0000000000..e6cedc8ebf --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnModificationsCommand.java @@ -0,0 +1,50 @@ +package sonia.scm.repository.spi; + +import lombok.extern.slf4j.Slf4j; +import org.tmatesoft.svn.core.SVNException; +import org.tmatesoft.svn.core.SVNLogEntry; +import org.tmatesoft.svn.core.io.SVNRepository; +import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.repository.Modifications; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RevisionNotFoundException; +import sonia.scm.repository.SvnUtil; +import sonia.scm.util.Util; + +import java.io.IOException; +import java.util.Collection; + +@Slf4j +public class SvnModificationsCommand extends AbstractSvnCommand implements ModificationsCommand { + + SvnModificationsCommand(SvnContext context, Repository repository) { + super(context, repository); + } + + + @Override + @SuppressWarnings("unchecked") + public Modifications getModifications(String revision) throws IOException, RevisionNotFoundException { + Modifications modifications = null; + log.debug("get modifications {}", revision); + try { + long revisionNumber = SvnUtil.parseRevision(revision); + SVNRepository repo = open(); + Collection entries = repo.log(null, null, revisionNumber, + revisionNumber, true, true); + if (Util.isNotEmpty(entries)) { + modifications = SvnUtil.createModifications(entries.iterator().next(), revision); + } + } catch (SVNException ex) { + throw new InternalRepositoryException("could not open repository", ex); + } + return modifications; + } + + @Override + public Modifications getModifications(ModificationsCommandRequest request) throws IOException, RevisionNotFoundException { + return getModifications(request.getRevision()); + } + + +} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnPreReceiveHookChangesetProvier.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnPreReceiveHookChangesetProvier.java index 93fb88f841..e4efdb71ab 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnPreReceiveHookChangesetProvier.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnPreReceiveHookChangesetProvier.java @@ -37,22 +37,19 @@ package sonia.scm.repository.spi; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import org.tmatesoft.svn.core.SVNLogEntry; import org.tmatesoft.svn.core.wc.ISVNOptions; import org.tmatesoft.svn.core.wc.SVNClientManager; import org.tmatesoft.svn.core.wc.SVNWCUtil; import org.tmatesoft.svn.core.wc.admin.SVNLookClient; - import sonia.scm.repository.Changeset; import sonia.scm.repository.RepositoryHookType; -import sonia.scm.repository.SvnModificationHandler; import sonia.scm.repository.SvnUtil; -//~--- JDK imports ------------------------------------------------------------ - import java.io.File; +//~--- JDK imports ------------------------------------------------------------ + /** * * @author Sebastian Sdorra @@ -123,10 +120,6 @@ public class SvnPreReceiveHookChangesetProvier { changeset = SvnUtil.createChangeset(entry); changeset.setId(SvnUtil.createTransactionEntryId(transaction)); - - clientManager.doGetChanged(repositoryDirectory, transaction, - new SvnModificationHandler(changeset), true); - } else if (logger.isWarnEnabled()) { diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceProvider.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceProvider.java index 24180bfe9e..5c1a076ddc 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceProvider.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceProvider.java @@ -33,19 +33,13 @@ package sonia.scm.repository.spi; -//~--- non-JDK imports -------------------------------------------------------- - import com.google.common.collect.ImmutableSet; import com.google.common.io.Closeables; - import sonia.scm.repository.Repository; import sonia.scm.repository.SvnRepositoryHandler; import sonia.scm.repository.api.Command; -//~--- JDK imports ------------------------------------------------------------ - import java.io.IOException; - import java.util.Set; /** @@ -65,13 +59,6 @@ public class SvnRepositoryServiceProvider extends RepositoryServiceProvider //~--- constructors --------------------------------------------------------- - /** - * Constructs ... - * - * - * @param handler - * @param repository - */ SvnRepositoryServiceProvider(SvnRepositoryHandler handler, Repository repository) { @@ -167,6 +154,10 @@ public class SvnRepositoryServiceProvider extends RepositoryServiceProvider return new SvnLogCommand(context, repository); } + public ModificationsCommand getModificationsCommand() { + return new SvnModificationsCommand(context, repository); + } + /** * Method description * diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceResolver.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceResolver.java index d56398083e..763b5f445e 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceResolver.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/repository/spi/SvnRepositoryServiceResolver.java @@ -32,64 +32,29 @@ package sonia.scm.repository.spi; -//~--- non-JDK imports -------------------------------------------------------- - import com.google.inject.Inject; - import sonia.scm.plugin.Extension; import sonia.scm.repository.Repository; import sonia.scm.repository.SvnRepositoryHandler; -/** - * - * @author Sebastian Sdorra - */ @Extension -public class SvnRepositoryServiceResolver implements RepositoryServiceResolver -{ +public class SvnRepositoryServiceResolver implements RepositoryServiceResolver { - /** Field description */ - public static final String TYPE = "svn"; + private SvnRepositoryHandler handler; - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * - * @param handler - */ @Inject - public SvnRepositoryServiceResolver(SvnRepositoryHandler handler) - { + public SvnRepositoryServiceResolver(SvnRepositoryHandler handler) { this.handler = handler; } - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param repository - * - * @return - */ @Override - public SvnRepositoryServiceProvider resolve(Repository repository) - { + public SvnRepositoryServiceProvider resolve(Repository repository) { SvnRepositoryServiceProvider provider = null; - if (TYPE.equalsIgnoreCase(repository.getType())) - { + if (SvnRepositoryHandler.TYPE_NAME.equalsIgnoreCase(repository.getType())) { provider = new SvnRepositoryServiceProvider(handler, repository); } return provider; } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private SvnRepositoryHandler handler; } diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnBasicAuthenticationFilter.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnBasicAuthenticationFilter.java deleted file mode 100644 index 1d9581f7c1..0000000000 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnBasicAuthenticationFilter.java +++ /dev/null @@ -1,102 +0,0 @@ -/** - * Copyright (c) 2010, Sebastian Sdorra All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. 2. Redistributions in - * binary form must reproduce the above copyright notice, this list of - * conditions and the following disclaimer in the documentation and/or other - * materials provided with the distribution. 3. Neither the name of SCM-Manager; - * nor the names of its contributors may be used to endorse or promote products - * derived from this software without specific prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR - * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR - * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER - * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, - * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * - * http://bitbucket.org/sdorra/scm-manager - * - */ - - - -package sonia.scm.web; - -//~--- non-JDK imports -------------------------------------------------------- - -import com.google.inject.Inject; - -import sonia.scm.Priority; -import sonia.scm.config.ScmConfiguration; -import sonia.scm.filter.Filters; -import sonia.scm.filter.WebElement; -import sonia.scm.repository.SvnUtil; -import sonia.scm.util.HttpUtil; -import sonia.scm.web.filter.AuthenticationFilter; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; - -import java.util.Set; - -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -/** - * - * @author Sebastian Sdorra - */ -@Priority(Filters.PRIORITY_AUTHENTICATION) -@WebElement(value = SvnServletModule.PATTERN_SVN) -public class SvnBasicAuthenticationFilter extends AuthenticationFilter -{ - - /** - * Constructs ... - * - * - * @param configuration - * @param webTokenGenerators - */ - @Inject - public SvnBasicAuthenticationFilter(ScmConfiguration configuration, Set webTokenGenerators) { - super(configuration, webTokenGenerators); - } - - //~--- methods -------------------------------------------------------------- - - /** - * Sends unauthorized instead of forbidden for svn clients, because the - * svn client prompts again for authentication. - * - * - * @param request http request - * @param response http response - * - * @throws IOException - */ - @Override - protected void sendFailedAuthenticationError(HttpServletRequest request, - HttpServletResponse response) - throws IOException - { - if (SvnUtil.isSvnClient(request)) - { - HttpUtil.sendUnauthorized(response, configuration.getRealmDescription()); - } - else - { - super.sendFailedAuthenticationError(request, response); - } - } -} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnDAVServlet.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnDAVServlet.java index c811179255..92d01db5a1 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnDAVServlet.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnDAVServlet.java @@ -33,8 +33,6 @@ package sonia.scm.web; -//~--- non-JDK imports -------------------------------------------------------- - import com.google.inject.Inject; import com.google.inject.Singleton; import org.slf4j.Logger; @@ -45,6 +43,7 @@ import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryProvider; import sonia.scm.repository.RepositoryRequestListenerUtil; import sonia.scm.repository.SvnRepositoryHandler; +import sonia.scm.repository.spi.ScmProviderHttpServlet; import sonia.scm.util.AssertUtil; import sonia.scm.util.HttpUtil; @@ -54,14 +53,12 @@ import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpServletResponse; import java.io.IOException; -//~--- JDK imports ------------------------------------------------------------ - /** * * @author Sebastian Sdorra */ @Singleton -public class SvnDAVServlet extends DAVServlet +public class SvnDAVServlet extends DAVServlet implements ScmProviderHttpServlet { /** Field description */ @@ -110,28 +107,18 @@ public class SvnDAVServlet extends DAVServlet * @throws ServletException */ @Override - public void service(HttpServletRequest request, HttpServletResponse response) + public void service(HttpServletRequest request, HttpServletResponse response, Repository repository) throws ServletException, IOException { - Repository repository = repositoryProvider.get(); - - if (repository != null) - { - if (repositoryRequestListenerUtil.callListeners(request, response, - repository)) - { - super.service(new SvnHttpServletRequestWrapper(request, - repositoryProvider), response); - } - else if (logger.isDebugEnabled()) - { - logger.debug("request aborted by repository request listener"); - } - } - else + if (repositoryRequestListenerUtil.callListeners(request, response, + repository)) { super.service(new SvnHttpServletRequestWrapper(request, - repositoryProvider), response); + repository), response); + } + else if (logger.isDebugEnabled()) + { + logger.debug("request aborted by repository request listener"); } } @@ -163,18 +150,11 @@ public class SvnDAVServlet extends DAVServlet extends HttpServletRequestWrapper { - /** - * Constructs ... - * - * - * @param request - * @param repositoryProvider - */ public SvnHttpServletRequestWrapper(HttpServletRequest request, - RepositoryProvider repositoryProvider) + Repository repository) { super(request); - this.repositoryProvider = repositoryProvider; + this.repository = repository; } //~--- get methods -------------------------------------------------------- @@ -211,8 +191,6 @@ public class SvnDAVServlet extends DAVServlet AssertUtil.assertIsNotEmpty(pathInfo); - Repository repository = repositoryProvider.get(); - if (repository != null) { if (pathInfo.startsWith(HttpUtil.SEPARATOR_PATH)) @@ -236,7 +214,6 @@ public class SvnDAVServlet extends DAVServlet public String getServletPath() { String servletPath = super.getServletPath(); - Repository repository = repositoryProvider.get(); if (repository != null) { @@ -280,10 +257,9 @@ public class SvnDAVServlet extends DAVServlet //~--- fields ------------------------------------------------------------- /** Field description */ - private final RepositoryProvider repositoryProvider; + private final Repository repository; } - //~--- fields --------------------------------------------------------------- /** Field description */ diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnDAVServletProvider.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnDAVServletProvider.java new file mode 100644 index 0000000000..d221504256 --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnDAVServletProvider.java @@ -0,0 +1,23 @@ +package sonia.scm.web; + +import com.google.inject.Inject; +import sonia.scm.repository.SvnRepositoryHandler; +import sonia.scm.repository.spi.ScmProviderHttpServlet; +import sonia.scm.repository.spi.ScmProviderHttpServletProvider; + +import javax.inject.Provider; + +public class SvnDAVServletProvider extends ScmProviderHttpServletProvider { + + @Inject + private Provider servletProvider; + + public SvnDAVServletProvider() { + super(SvnRepositoryHandler.TYPE_NAME); + } + + @Override + protected ScmProviderHttpServlet getRootServlet() { + return servletProvider.get(); + } +} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnGZipFilter.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnGZipFilter.java index 65afefbb28..7cc78180ff 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnGZipFilter.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnGZipFilter.java @@ -34,38 +34,34 @@ package sonia.scm.web; //~--- non-JDK imports -------------------------------------------------------- -import com.google.inject.Inject; -import com.google.inject.Singleton; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import sonia.scm.filter.GZipFilter; +import sonia.scm.repository.Repository; import sonia.scm.repository.SvnRepositoryHandler; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; +import sonia.scm.repository.spi.ScmProviderHttpServlet; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; +import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +//~--- JDK imports ------------------------------------------------------------ /** * * @author Sebastian Sdorra */ -@Singleton -public class SvnGZipFilter extends GZipFilter +public class SvnGZipFilter extends GZipFilter implements ScmProviderHttpServlet { - /** - * the logger for SvnGZipFilter - */ - private static final Logger logger = - LoggerFactory.getLogger(SvnGZipFilter.class); + private static final Logger logger = LoggerFactory.getLogger(SvnGZipFilter.class); + + private final SvnRepositoryHandler handler; + private final ScmProviderHttpServlet delegate; //~--- constructors --------------------------------------------------------- @@ -75,10 +71,10 @@ public class SvnGZipFilter extends GZipFilter * * @param handler */ - @Inject - public SvnGZipFilter(SvnRepositoryHandler handler) + public SvnGZipFilter(SvnRepositoryHandler handler, ScmProviderHttpServlet delegate) { this.handler = handler; + this.delegate = delegate; } //~--- methods -------------------------------------------------------------- @@ -134,8 +130,30 @@ public class SvnGZipFilter extends GZipFilter } } - //~--- fields --------------------------------------------------------------- + @Override + public void service(HttpServletRequest request, HttpServletResponse response, Repository repository) throws ServletException, IOException { + if (handler.getConfig().isEnabledGZip()) + { + if (logger.isTraceEnabled()) + { + logger.trace("encode svn request with gzip"); + } - /** Field description */ - private SvnRepositoryHandler handler; + super.doFilter(request, response, (servletRequest, servletResponse) -> delegate.service((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse, repository)); + } + else + { + if (logger.isTraceEnabled()) + { + logger.trace("skip gzip encoding"); + } + + delegate.service(request, response, repository); + } + } + + @Override + public void init(ServletConfig config) throws ServletException { + delegate.init(config); + } } diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnGZipFilterFactory.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnGZipFilterFactory.java new file mode 100644 index 0000000000..e2774106fa --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnGZipFilterFactory.java @@ -0,0 +1,28 @@ +package sonia.scm.web; + +import com.google.inject.Inject; +import sonia.scm.plugin.Extension; +import sonia.scm.repository.SvnRepositoryHandler; +import sonia.scm.repository.spi.ScmProviderHttpServlet; +import sonia.scm.repository.spi.ScmProviderHttpServletDecoratorFactory; + +@Extension +public class SvnGZipFilterFactory implements ScmProviderHttpServletDecoratorFactory { + + private final SvnRepositoryHandler handler; + + @Inject + public SvnGZipFilterFactory(SvnRepositoryHandler handler) { + this.handler = handler; + } + + @Override + public boolean handlesScmType(String type) { + return SvnRepositoryHandler.TYPE_NAME.equals(type); + } + + @Override + public ScmProviderHttpServlet createDecorator(ScmProviderHttpServlet delegate) { + return new SvnGZipFilter(handler, delegate); + } +} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnPermissionFilter.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnPermissionFilter.java index 30ef3e94c0..2c0a1e65ff 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnPermissionFilter.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnPermissionFilter.java @@ -33,37 +33,24 @@ package sonia.scm.web; -//~--- non-JDK imports -------------------------------------------------------- - import com.google.common.collect.ImmutableSet; -import com.google.inject.Inject; - import sonia.scm.ClientMessages; -import sonia.scm.Priority; import sonia.scm.config.ScmConfiguration; -import sonia.scm.filter.Filters; -import sonia.scm.filter.WebElement; -import sonia.scm.repository.RepositoryProvider; import sonia.scm.repository.ScmSvnErrorCode; import sonia.scm.repository.SvnUtil; -import sonia.scm.web.filter.ProviderPermissionFilter; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; - -import java.util.Set; +import sonia.scm.repository.spi.ScmProviderHttpServlet; +import sonia.scm.web.filter.PermissionFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Set; /** * * @author Sebastian Sdorra */ -@Priority(Filters.PRIORITY_AUTHORIZATION) -@WebElement(value = SvnServletModule.PATTERN_SVN) -public class SvnPermissionFilter extends ProviderPermissionFilter +public class SvnPermissionFilter extends PermissionFilter { /** Field description */ @@ -77,13 +64,10 @@ public class SvnPermissionFilter extends ProviderPermissionFilter * Constructs ... * * @param configuration - * @param repository */ - @Inject - public SvnPermissionFilter(ScmConfiguration configuration, - RepositoryProvider repository) + public SvnPermissionFilter(ScmConfiguration configuration, ScmProviderHttpServlet delegate) { - super(configuration, repository); + super(configuration, delegate); } //~--- methods -------------------------------------------------------------- @@ -132,7 +116,7 @@ public class SvnPermissionFilter extends ProviderPermissionFilter * @return */ @Override - protected boolean isWriteRequest(HttpServletRequest request) + public boolean isWriteRequest(HttpServletRequest request) { return WRITEMETHOD_SET.contains(request.getMethod().toUpperCase()); } diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnPermissionFilterFactory.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnPermissionFilterFactory.java new file mode 100644 index 0000000000..882cb8c54f --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnPermissionFilterFactory.java @@ -0,0 +1,30 @@ +package sonia.scm.web; + +import sonia.scm.config.ScmConfiguration; +import sonia.scm.plugin.Extension; +import sonia.scm.repository.SvnRepositoryHandler; +import sonia.scm.repository.spi.ScmProviderHttpServlet; +import sonia.scm.repository.spi.ScmProviderHttpServletDecoratorFactory; + +import javax.inject.Inject; + +@Extension +public class SvnPermissionFilterFactory implements ScmProviderHttpServletDecoratorFactory { + + private final ScmConfiguration configuration; + + @Inject + public SvnPermissionFilterFactory(ScmConfiguration configuration) { + this.configuration = configuration; + } + + @Override + public boolean handlesScmType(String type) { + return SvnRepositoryHandler.TYPE_NAME.equals(type); + } + + @Override + public ScmProviderHttpServlet createDecorator(ScmProviderHttpServlet delegate) { + return new SvnPermissionFilter(configuration, delegate); + } +} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnScmProtocolProviderWrapper.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnScmProtocolProviderWrapper.java new file mode 100644 index 0000000000..ba7d5e875a --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnScmProtocolProviderWrapper.java @@ -0,0 +1,74 @@ +package sonia.scm.web; + +import sonia.scm.api.v2.resources.ScmPathInfoStore; +import sonia.scm.config.ScmConfiguration; +import sonia.scm.plugin.Extension; +import sonia.scm.repository.SvnRepositoryHandler; +import sonia.scm.repository.spi.InitializingHttpScmProtocolWrapper; +import sonia.scm.repository.spi.ScmProviderHttpServlet; + +import javax.inject.Inject; +import javax.inject.Provider; +import javax.inject.Singleton; +import javax.servlet.ServletConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import java.util.Enumeration; + +@Singleton +@Extension +public class SvnScmProtocolProviderWrapper extends InitializingHttpScmProtocolWrapper { + + public static final String PARAMETER_SVN_PARENTPATH = "SVNParentPath"; + + @Override + public String getType() { + return SvnRepositoryHandler.TYPE_NAME; + } + + @Inject + public SvnScmProtocolProviderWrapper(SvnDAVServletProvider servletProvider, Provider uriInfoStore, ScmConfiguration scmConfiguration) { + super(servletProvider, uriInfoStore, scmConfiguration); + } + + @Override + protected void initializeServlet(ServletConfig config, ScmProviderHttpServlet httpServlet) throws ServletException { + + super.initializeServlet(new SvnConfigEnhancer(config), httpServlet); + } + + private static class SvnConfigEnhancer implements ServletConfig { + + private final ServletConfig originalConfig; + + private SvnConfigEnhancer(ServletConfig originalConfig) { + this.originalConfig = originalConfig; + } + + @Override + public String getServletName() { + return originalConfig.getServletName(); + } + + @Override + public ServletContext getServletContext() { + return originalConfig.getServletContext(); + } + + @Override + /** + * Overridden to return the systems temp directory for the key {@link PARAMETER_SVN_PARENTPATH}. + */ + public String getInitParameter(String key) { + if (PARAMETER_SVN_PARENTPATH.equals(key)) { + return System.getProperty("java.io.tmpdir"); + } + return originalConfig.getInitParameter(key); + } + + @Override + public Enumeration getInitParameterNames() { + return originalConfig.getInitParameterNames(); + } + } +} diff --git a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnServletModule.java b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnServletModule.java index 9b5c8ae556..8526d6380a 100644 --- a/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnServletModule.java +++ b/scm-plugins/scm-svn-plugin/src/main/java/sonia/scm/web/SvnServletModule.java @@ -33,53 +33,22 @@ package sonia.scm.web; -//~--- non-JDK imports -------------------------------------------------------- - import com.google.inject.servlet.ServletModule; import org.mapstruct.factory.Mappers; import sonia.scm.api.v2.resources.SvnConfigDtoToSvnConfigMapper; import sonia.scm.api.v2.resources.SvnConfigToSvnConfigDtoMapper; import sonia.scm.plugin.Extension; -import java.util.HashMap; -import java.util.Map; - -//~--- JDK imports ------------------------------------------------------------ - /** * * @author Sebastian Sdorra */ @Extension -public class SvnServletModule extends ServletModule -{ +public class SvnServletModule extends ServletModule { - /** Field description */ - public static final String PARAMETER_SVN_PARENTPATH = "SVNParentPath"; - - /** Field description */ - public static final String PATTERN_SVN = "/svn/*"; - - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - */ @Override - protected void configureServlets() - { - filter(PATTERN_SVN).through(SvnGZipFilter.class); - filter(PATTERN_SVN).through(SvnBasicAuthenticationFilter.class); - filter(PATTERN_SVN).through(SvnPermissionFilter.class); - + protected void configureServlets() { bind(SvnConfigDtoToSvnConfigMapper.class).to(Mappers.getMapper(SvnConfigDtoToSvnConfigMapper.class).getClass()); bind(SvnConfigToSvnConfigDtoMapper.class).to(Mappers.getMapper(SvnConfigToSvnConfigDtoMapper.class).getClass()); - - Map parameters = new HashMap(); - - parameters.put(PARAMETER_SVN_PARENTPATH, - System.getProperty("java.io.tmpdir")); - serve(PATTERN_SVN).with(SvnDAVServlet.class, parameters); } } diff --git a/scm-plugins/scm-svn-plugin/src/main/js/ProtocolInformation.js b/scm-plugins/scm-svn-plugin/src/main/js/ProtocolInformation.js index ccff4118ba..0ba195887f 100644 --- a/scm-plugins/scm-svn-plugin/src/main/js/ProtocolInformation.js +++ b/scm-plugins/scm-svn-plugin/src/main/js/ProtocolInformation.js @@ -1,5 +1,6 @@ //@flow import React from "react"; +import { repositories } from "@scm-manager/ui-components"; import type { Repository } from "@scm-manager/ui-types"; type Props = { @@ -10,14 +11,15 @@ class ProtocolInformation extends React.Component { render() { const { repository } = this.props; - if (!repository._links.httpProtocol) { + const href = repositories.getProtocolLinkByType(repository, "http"); + if (!href) { return null; } return (

Checkout the repository

-          svn checkout {repository._links.httpProtocol.href}
+          svn checkout {href}
         
); diff --git a/scm-plugins/scm-svn-plugin/src/main/js/SvnAvatar.js b/scm-plugins/scm-svn-plugin/src/main/js/SvnAvatar.js index 06fc2454db..731c9ddf33 100644 --- a/scm-plugins/scm-svn-plugin/src/main/js/SvnAvatar.js +++ b/scm-plugins/scm-svn-plugin/src/main/js/SvnAvatar.js @@ -1,6 +1,6 @@ //@flow import React from "react"; -import { Image } from "@scm-manager/ui-components"; +import {Image} from "@scm-manager/ui-components"; type Props = { }; @@ -8,7 +8,7 @@ type Props = { class SvnAvatar extends React.Component { render() { - return Subversion Logo; + return Subversion Logo; } } diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigInIndexResourceTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigInIndexResourceTest.java new file mode 100644 index 0000000000..8b87b57c6c --- /dev/null +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigInIndexResourceTest.java @@ -0,0 +1,64 @@ +package sonia.scm.api.v2.resources; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; +import com.google.inject.util.Providers; +import org.junit.Rule; +import org.junit.Test; +import sonia.scm.web.JsonEnricherContext; +import sonia.scm.web.VndMediaType; + +import javax.ws.rs.core.MediaType; +import java.net.URI; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +@SubjectAware(configuration = "classpath:sonia/scm/configuration/shiro.ini") +public class SvnConfigInIndexResourceTest { + + @Rule + public final ShiroRule shiroRule = new ShiroRule(); + + private final ObjectMapper objectMapper = new ObjectMapper(); + private final ObjectNode root = objectMapper.createObjectNode(); + private final SvnConfigInIndexResource svnConfigInIndexResource; + + public SvnConfigInIndexResourceTest() { + root.put("_links", objectMapper.createObjectNode()); + ScmPathInfoStore pathInfoStore = new ScmPathInfoStore(); + pathInfoStore.set(() -> URI.create("/")); + svnConfigInIndexResource = new SvnConfigInIndexResource(Providers.of(pathInfoStore), objectMapper); + } + + @Test + @SubjectAware(username = "admin", password = "secret") + public void admin() { + JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root); + + svnConfigInIndexResource.enrich(context); + + assertEquals("/v2/config/svn", root.get("_links").get("svnConfig").get("href").asText()); + } + + @Test + @SubjectAware(username = "readOnly", password = "secret") + public void user() { + JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root); + + svnConfigInIndexResource.enrich(context); + + assertFalse(root.get("_links").iterator().hasNext()); + } + + @Test + public void anonymous() { + JsonEnricherContext context = new JsonEnricherContext(URI.create("/index"), MediaType.valueOf(VndMediaType.INDEX), root); + + svnConfigInIndexResource.enrich(context); + + assertFalse(root.get("_links").iterator().hasNext()); + } +} diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigResourceTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigResourceTest.java index de4d654910..28dbe09e92 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigResourceTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigResourceTest.java @@ -53,7 +53,7 @@ public class SvnConfigResourceTest { private SvnConfigDtoToSvnConfigMapperImpl dtoToConfigMapper; @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private UriInfoStore uriInfoStore; + private ScmPathInfoStore scmPathInfoStore; @InjectMocks private SvnConfigToSvnConfigDtoMapperImpl configToDtoMapper; @@ -67,7 +67,7 @@ public class SvnConfigResourceTest { when(repositoryHandler.getConfig()).thenReturn(gitConfig); SvnConfigResource gitConfigResource = new SvnConfigResource(dtoToConfigMapper, configToDtoMapper, repositoryHandler); dispatcher.getRegistry().addSingletonResource(gitConfigResource); - when(uriInfoStore.get().getBaseUri()).thenReturn(baseUri); + when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri); } @Test diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigToSvnConfigDtoMapperTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigToSvnConfigDtoMapperTest.java index de7b0ecd31..5184aa3d41 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigToSvnConfigDtoMapperTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/api/v2/resources/SvnConfigToSvnConfigDtoMapperTest.java @@ -30,7 +30,7 @@ public class SvnConfigToSvnConfigDtoMapperTest { private URI baseUri = URI.create("http://example.com/base/"); @Mock(answer = Answers.RETURNS_DEEP_STUBS) - private UriInfoStore uriInfoStore; + private ScmPathInfoStore scmPathInfoStore; @InjectMocks private SvnConfigToSvnConfigDtoMapperImpl mapper; @@ -42,7 +42,7 @@ public class SvnConfigToSvnConfigDtoMapperTest { @Before public void init() { - when(uriInfoStore.get().getBaseUri()).thenReturn(baseUri); + when(scmPathInfoStore.get().getApiRestUri()).thenReturn(baseUri); expectedBaseUri = baseUri.resolve(SvnConfigResource.SVN_CONFIG_PATH_V2); subjectThreadState.bind(); ThreadContext.bind(subject); diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/client/spi/SvnChangeWorker.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/client/spi/SvnChangeWorker.java index b54e51c4d6..302ef3e8aa 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/client/spi/SvnChangeWorker.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/client/spi/SvnChangeWorker.java @@ -70,13 +70,15 @@ class SvnChangeWorker { SVNWCClient wClient = client.getWCClient(); // add files - try { - wClient.doAdd(addedFiles.toArray(new File[0]), true, false, false, - SVNDepth.INFINITY, false, false, false); - addedFiles.clear(); + if (!addedFiles.isEmpty()){ + try { + wClient.doAdd(addedFiles.toArray(new File[0]), true, false, false, + SVNDepth.INFINITY, false, false, false); + addedFiles.clear(); - } catch (SVNException ex) { - throw new RepositoryClientException("failed to add files", ex); + } catch (SVNException ex) { + throw new RepositoryClientException("failed to add files", ex); + } } // remove files diff --git a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnLogCommandTest.java b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnLogCommandTest.java index 941687190c..a55138f151 100644 --- a/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnLogCommandTest.java +++ b/scm-plugins/scm-svn-plugin/src/test/java/sonia/scm/repository/spi/SvnLogCommandTest.java @@ -40,6 +40,8 @@ import sonia.scm.repository.ChangesetPagingResult; import sonia.scm.repository.Modifications; import sonia.scm.repository.RevisionNotFoundException; +import java.io.IOException; + import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @@ -128,7 +130,7 @@ public class SvnLogCommandTest extends AbstractSvnCommandTestBase } @Test - public void testGetCommit() throws RevisionNotFoundException { + public void testGetCommit() throws RevisionNotFoundException, IOException { Changeset c = createCommand().getChangeset("3"); assertNotNull(c); @@ -137,15 +139,15 @@ public class SvnLogCommandTest extends AbstractSvnCommandTestBase checkDate(c.getDate()); assertEquals("perfect", c.getAuthor().getName()); assertNull("douglas.adams@hitchhiker.com", c.getAuthor().getMail()); + SvnModificationsCommand modificationsCommand = new SvnModificationsCommand(createContext(), repository); + Modifications modifications = modificationsCommand.getModifications("3"); - Modifications mods = c.getModifications(); - - assertNotNull(mods); - assertEquals(1, mods.getModified().size()); - assertEquals(1, mods.getRemoved().size()); - assertTrue("added list should be empty", mods.getAdded().isEmpty()); - assertEquals("a.txt", mods.getModified().get(0)); - assertEquals("b.txt", mods.getRemoved().get(0)); + assertNotNull(modifications); + assertEquals(1, modifications.getModified().size()); + assertEquals(1, modifications.getRemoved().size()); + assertTrue("added list should be empty", modifications.getAdded().isEmpty()); + assertEquals("a.txt", modifications.getModified().get(0)); + assertEquals("b.txt", modifications.getRemoved().get(0)); } @Test diff --git a/scm-plugins/scm-svn-plugin/src/test/resources/sonia/scm/configuration/shiro.ini b/scm-plugins/scm-svn-plugin/src/test/resources/sonia/scm/configuration/shiro.ini index 7e4233b540..fe84723e0a 100644 --- a/scm-plugins/scm-svn-plugin/src/test/resources/sonia/scm/configuration/shiro.ini +++ b/scm-plugins/scm-svn-plugin/src/test/resources/sonia/scm/configuration/shiro.ini @@ -2,8 +2,10 @@ readOnly = secret, reader writeOnly = secret, writer readWrite = secret, readerWriter +admin = secret, admin [roles] reader = configuration:read:svn writer = configuration:write:svn readerWriter = configuration:*:svn +admin = * diff --git a/scm-plugins/scm-svn-plugin/yarn.lock b/scm-plugins/scm-svn-plugin/yarn.lock index 251b5241a2..8822bd5e57 100644 --- a/scm-plugins/scm-svn-plugin/yarn.lock +++ b/scm-plugins/scm-svn-plugin/yarn.lock @@ -641,9 +641,9 @@ version "0.0.2" resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85" -"@scm-manager/ui-bundler@^0.0.13": - version "0.0.13" - resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.13.tgz#020e6c8ee870fccb6c451490cb18972ebfb0d2c4" +"@scm-manager/ui-bundler@^0.0.15": + version "0.0.15" + resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.15.tgz#8ed4a557d5ae38d6b99493b29608fd6a4c9cd917" dependencies: "@babel/core" "^7.0.0" "@babel/plugin-proposal-class-properties" "^7.0.0" @@ -671,9 +671,10 @@ flow-bin "^0.79.1" gulp "^3.9.1" gulp-sourcemaps "^2.6.4" - gulp-util "^3.0.8" + gulp-uglify "^3.0.1" jest "^23.5.0" jest-junit "^5.1.0" + mustache "^2.3.2" node-mkdirs "^0.0.1" pom-parser "^1.1.1" prettier "^1.14.2" @@ -1801,7 +1802,7 @@ combined-stream@1.0.6, combined-stream@^1.0.5, combined-stream@~1.0.5, combined- dependencies: delayed-stream "~1.0.0" -commander@^2.11.0, commander@^2.17.1, commander@^2.2.0, commander@^2.9.0: +commander@^2.11.0, commander@^2.17.1, commander@^2.2.0, commander@^2.9.0, commander@~2.17.1: version "2.17.1" resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" @@ -3217,7 +3218,20 @@ gulp-sourcemaps@^2.6.4: strip-bom-string "1.X" through2 "2.X" -gulp-util@^3.0.0, gulp-util@^3.0.8: +gulp-uglify@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/gulp-uglify/-/gulp-uglify-3.0.1.tgz#8d3eee466521bea6b10fd75dff72adf8b7ea2d97" + dependencies: + gulplog "^1.0.0" + has-gulplog "^0.1.0" + lodash "^4.13.1" + make-error-cause "^1.1.1" + safe-buffer "^5.1.2" + through2 "^2.0.0" + uglify-js "^3.0.5" + vinyl-sourcemaps-apply "^0.2.0" + +gulp-util@^3.0.0: version "3.0.8" resolved "https://registry.yarnpkg.com/gulp-util/-/gulp-util-3.0.8.tgz#0054e1e744502e27c04c187c3ecc505dd54bbb4f" dependencies: @@ -4636,6 +4650,16 @@ lru-queue@0.1: dependencies: es5-ext "~0.10.2" +make-error-cause@^1.1.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/make-error-cause/-/make-error-cause-1.2.2.tgz#df0388fcd0b37816dff0a5fb8108939777dcbc9d" + dependencies: + make-error "^1.2.0" + +make-error@^1.2.0: + version "1.3.5" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8" + make-iterator@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/make-iterator/-/make-iterator-1.0.1.tgz#29b33f312aa8f547c4a5e490f56afcec99133ad6" @@ -4868,6 +4892,10 @@ multipipe@^0.1.2: dependencies: duplexer2 "0.0.2" +mustache@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/mustache/-/mustache-2.3.2.tgz#a6d4d9c3f91d13359ab889a812954f9230a3d0c5" + mute-stream@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" @@ -6283,7 +6311,7 @@ source-map@^0.4.4: dependencies: amdefine ">=0.0.4" -source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.1, source-map@~0.5.3: +source-map@^0.5.0, source-map@^0.5.1, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.1, source-map@~0.5.3: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" @@ -6739,6 +6767,13 @@ uglify-js@^2.6: optionalDependencies: uglify-to-browserify "~1.0.0" +uglify-js@^3.0.5: + version "3.4.9" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.9.tgz#af02f180c1207d76432e473ed24a28f4a782bae3" + dependencies: + commander "~2.17.1" + source-map "~0.6.1" + uglify-to-browserify@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" @@ -6915,6 +6950,12 @@ vinyl-source-stream@^2.0.0: through2 "^2.0.3" vinyl "^2.1.0" +vinyl-sourcemaps-apply@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz#ab6549d61d172c2b1b87be5c508d239c8ef87705" + dependencies: + source-map "^0.5.1" + vinyl@^0.4.0: version "0.4.6" resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-0.4.6.tgz#2f356c87a550a255461f36bbeb2a5ba8bf784847" diff --git a/scm-ui-components/package.json b/scm-ui-components/package.json index 6fe782506e..d0f1be6ff9 100644 --- a/scm-ui-components/package.json +++ b/scm-ui-components/package.json @@ -4,7 +4,8 @@ "private": true, "scripts": { "bootstrap": "lerna bootstrap", - "link": "lerna exec -- yarn link" + "link": "lerna exec -- yarn link", + "unlink": "lerna exec -- yarn unlink" }, "devDependencies": { "lerna": "^3.2.1" diff --git a/scm-ui-components/packages/ui-components/package.json b/scm-ui-components/packages/ui-components/package.json index 1b8eeb517e..e515002728 100644 --- a/scm-ui-components/packages/ui-components/package.json +++ b/scm-ui-components/packages/ui-components/package.json @@ -12,7 +12,7 @@ "eslint-fix": "eslint src --fix" }, "devDependencies": { - "@scm-manager/ui-bundler": "^0.0.13", + "@scm-manager/ui-bundler": "^0.0.15", "create-index": "^2.3.0", "enzyme": "^3.5.0", "enzyme-adapter-react-16": "^1.3.1", diff --git a/scm-ui-components/packages/ui-components/src/index.js b/scm-ui-components/packages/ui-components/src/index.js index 4865540868..2e1c51b7dc 100644 --- a/scm-ui-components/packages/ui-components/src/index.js +++ b/scm-ui-components/packages/ui-components/src/index.js @@ -2,8 +2,9 @@ import * as validation from "./validation.js"; import * as urls from "./urls"; +import * as repositories from "./repositories.js"; -export { validation, urls }; +export { validation, urls, repositories }; export { default as DateFromNow } from "./DateFromNow.js"; export { default as ErrorNotification } from "./ErrorNotification.js"; @@ -18,6 +19,8 @@ export { default as ProtectedRoute } from "./ProtectedRoute.js"; export { apiClient, NOT_FOUND_ERROR, UNAUTHORIZED_ERROR } from "./apiclient.js"; + + export * from "./buttons"; export * from "./forms"; export * from "./layout"; diff --git a/scm-ui-components/packages/ui-components/src/repositories.js b/scm-ui-components/packages/ui-components/src/repositories.js new file mode 100644 index 0000000000..104cb9a691 --- /dev/null +++ b/scm-ui-components/packages/ui-components/src/repositories.js @@ -0,0 +1,19 @@ +// @flow +import type { Repository } from "@scm-manager/ui-types"; + +// util methods for repositories + +export function getProtocolLinkByType(repository: Repository, type: string) { + let protocols = repository._links.protocol; + if (protocols) { + if (!Array.isArray(protocols)) { + protocols = [protocols]; + } + for (let proto of protocols) { + if (proto.name === type) { + return proto.href; + } + } + } + return null; +} diff --git a/scm-ui-components/packages/ui-components/src/repositories.test.js b/scm-ui-components/packages/ui-components/src/repositories.test.js new file mode 100644 index 0000000000..ccd972ad03 --- /dev/null +++ b/scm-ui-components/packages/ui-components/src/repositories.test.js @@ -0,0 +1,99 @@ +// @flow + +import type { Repository } from "@scm-manager/ui-types"; +import { getProtocolLinkByType, getTypePredicate } from "./repositories"; + +describe("getProtocolLinkByType tests", () => { + + it("should return the http protocol link", () => { + + const repository: Repository = { + namespace: "scm", + name: "core", + type: "git", + _links: { + protocol: [{ + name: "http", + href: "http://scm.scm-manager.org/repo/scm/core" + }] + } + }; + + const link = getProtocolLinkByType(repository, "http"); + expect(link).toBe("http://scm.scm-manager.org/repo/scm/core"); + }); + + it("should return the http protocol link from multiple protocols", () => { + + const repository: Repository = { + namespace: "scm", + name: "core", + type: "git", + _links: { + protocol: [{ + name: "http", + href: "http://scm.scm-manager.org/repo/scm/core" + },{ + name: "ssh", + href: "git@scm.scm-manager.org:scm/core" + }] + } + }; + + const link = getProtocolLinkByType(repository, "http"); + expect(link).toBe("http://scm.scm-manager.org/repo/scm/core"); + }); + + it("should return the http protocol, even if the protocol is a single link", () => { + + const repository: Repository = { + namespace: "scm", + name: "core", + type: "git", + _links: { + protocol: { + name: "http", + href: "http://scm.scm-manager.org/repo/scm/core" + } + } + }; + + const link = getProtocolLinkByType(repository, "http"); + expect(link).toBe("http://scm.scm-manager.org/repo/scm/core"); + }); + + it("should return null, if such a protocol does not exists", () => { + + const repository: Repository = { + namespace: "scm", + name: "core", + type: "git", + _links: { + protocol: [{ + name: "http", + href: "http://scm.scm-manager.org/repo/scm/core" + },{ + name: "ssh", + href: "git@scm.scm-manager.org:scm/core" + }] + } + }; + + const link = getProtocolLinkByType(repository, "awesome"); + expect(link).toBeNull(); + }); + + it("should return null, if no protocols are available", () => { + + const repository: Repository = { + namespace: "scm", + name: "core", + type: "git", + _links: {} + }; + + const link = getProtocolLinkByType(repository, "http"); + expect(link).toBeNull(); + }); + +}); diff --git a/scm-ui-components/packages/ui-components/yarn.lock b/scm-ui-components/packages/ui-components/yarn.lock index e0d20269fd..bab2b75068 100644 --- a/scm-ui-components/packages/ui-components/yarn.lock +++ b/scm-ui-components/packages/ui-components/yarn.lock @@ -728,9 +728,9 @@ version "0.0.2" resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85" -"@scm-manager/ui-bundler@^0.0.13": - version "0.0.13" - resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.13.tgz#020e6c8ee870fccb6c451490cb18972ebfb0d2c4" +"@scm-manager/ui-bundler@^0.0.15": + version "0.0.15" + resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.15.tgz#8ed4a557d5ae38d6b99493b29608fd6a4c9cd917" dependencies: "@babel/core" "^7.0.0" "@babel/plugin-proposal-class-properties" "^7.0.0" @@ -758,9 +758,10 @@ flow-bin "^0.79.1" gulp "^3.9.1" gulp-sourcemaps "^2.6.4" - gulp-util "^3.0.8" + gulp-uglify "^3.0.1" jest "^23.5.0" jest-junit "^5.1.0" + mustache "^2.3.2" node-mkdirs "^0.0.1" pom-parser "^1.1.1" prettier "^1.14.2" @@ -1983,7 +1984,7 @@ combined-stream@1.0.6, combined-stream@^1.0.5, combined-stream@~1.0.5, combined- dependencies: delayed-stream "~1.0.0" -commander@^2.11.0, commander@^2.17.1, commander@^2.2.0, commander@^2.9.0: +commander@^2.11.0, commander@^2.17.1, commander@^2.2.0, commander@^2.9.0, commander@~2.17.1: version "2.17.1" resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" @@ -3605,7 +3606,20 @@ gulp-sourcemaps@^2.6.4: strip-bom-string "1.X" through2 "2.X" -gulp-util@^3.0.0, gulp-util@^3.0.8: +gulp-uglify@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/gulp-uglify/-/gulp-uglify-3.0.1.tgz#8d3eee466521bea6b10fd75dff72adf8b7ea2d97" + dependencies: + gulplog "^1.0.0" + has-gulplog "^0.1.0" + lodash "^4.13.1" + make-error-cause "^1.1.1" + safe-buffer "^5.1.2" + through2 "^2.0.0" + uglify-js "^3.0.5" + vinyl-sourcemaps-apply "^0.2.0" + +gulp-util@^3.0.0: version "3.0.8" resolved "https://registry.yarnpkg.com/gulp-util/-/gulp-util-3.0.8.tgz#0054e1e744502e27c04c187c3ecc505dd54bbb4f" dependencies: @@ -5264,6 +5278,16 @@ lru-queue@0.1: dependencies: es5-ext "~0.10.2" +make-error-cause@^1.1.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/make-error-cause/-/make-error-cause-1.2.2.tgz#df0388fcd0b37816dff0a5fb8108939777dcbc9d" + dependencies: + make-error "^1.2.0" + +make-error@^1.2.0: + version "1.3.5" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8" + make-iterator@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/make-iterator/-/make-iterator-1.0.1.tgz#29b33f312aa8f547c4a5e490f56afcec99133ad6" @@ -5506,6 +5530,10 @@ multipipe@^0.1.2: dependencies: duplexer2 "0.0.2" +mustache@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/mustache/-/mustache-2.3.2.tgz#a6d4d9c3f91d13359ab889a812954f9230a3d0c5" + mute-stream@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" @@ -7143,7 +7171,7 @@ source-map@^0.4.4: dependencies: amdefine ">=0.0.4" -source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.1, source-map@~0.5.3: +source-map@^0.5.0, source-map@^0.5.1, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.1, source-map@~0.5.3: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" @@ -7627,6 +7655,13 @@ uglify-js@^2.6: optionalDependencies: uglify-to-browserify "~1.0.0" +uglify-js@^3.0.5: + version "3.4.9" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.9.tgz#af02f180c1207d76432e473ed24a28f4a782bae3" + dependencies: + commander "~2.17.1" + source-map "~0.6.1" + uglify-to-browserify@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" @@ -7839,6 +7874,12 @@ vinyl-source-stream@^2.0.0: through2 "^2.0.3" vinyl "^2.1.0" +vinyl-sourcemaps-apply@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz#ab6549d61d172c2b1b87be5c508d239c8ef87705" + dependencies: + source-map "^0.5.1" + vinyl@^0.4.0: version "0.4.6" resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-0.4.6.tgz#2f356c87a550a255461f36bbeb2a5ba8bf784847" diff --git a/scm-ui-components/packages/ui-types/package.json b/scm-ui-components/packages/ui-types/package.json index e9db94b9a6..9c06fc7740 100644 --- a/scm-ui-components/packages/ui-types/package.json +++ b/scm-ui-components/packages/ui-types/package.json @@ -14,7 +14,7 @@ "check": "flow check" }, "devDependencies": { - "@scm-manager/ui-bundler": "^0.0.13" + "@scm-manager/ui-bundler": "^0.0.15" }, "browserify": { "transform": [ diff --git a/scm-ui-components/packages/ui-types/src/hal.js b/scm-ui-components/packages/ui-types/src/hal.js index ab22203835..248c5e5453 100644 --- a/scm-ui-components/packages/ui-types/src/hal.js +++ b/scm-ui-components/packages/ui-types/src/hal.js @@ -1,9 +1,10 @@ // @flow export type Link = { - href: string + href: string, + name?: string }; -export type Links = { [string]: Link }; +export type Links = { [string]: Link | Link[] }; export type Collection = { _embedded: Object, diff --git a/scm-ui-components/packages/ui-types/yarn.lock b/scm-ui-components/packages/ui-types/yarn.lock index 6c18546f36..24213220d9 100644 --- a/scm-ui-components/packages/ui-types/yarn.lock +++ b/scm-ui-components/packages/ui-types/yarn.lock @@ -707,9 +707,9 @@ version "0.0.2" resolved "https://registry.yarnpkg.com/@scm-manager/eslint-config/-/eslint-config-0.0.2.tgz#94cc8c3fb4f51f870b235893dc134fc6c423ae85" -"@scm-manager/ui-bundler@^0.0.13": - version "0.0.13" - resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.13.tgz#020e6c8ee870fccb6c451490cb18972ebfb0d2c4" +"@scm-manager/ui-bundler@^0.0.15": + version "0.0.15" + resolved "https://registry.yarnpkg.com/@scm-manager/ui-bundler/-/ui-bundler-0.0.15.tgz#8ed4a557d5ae38d6b99493b29608fd6a4c9cd917" dependencies: "@babel/core" "^7.0.0" "@babel/plugin-proposal-class-properties" "^7.0.0" @@ -737,9 +737,10 @@ flow-bin "^0.79.1" gulp "^3.9.1" gulp-sourcemaps "^2.6.4" - gulp-util "^3.0.8" + gulp-uglify "^3.0.1" jest "^23.5.0" jest-junit "^5.1.0" + mustache "^2.3.2" node-mkdirs "^0.0.1" pom-parser "^1.1.1" prettier "^1.14.2" @@ -1856,7 +1857,7 @@ combined-stream@1.0.6, combined-stream@^1.0.5, combined-stream@~1.0.5, combined- dependencies: delayed-stream "~1.0.0" -commander@^2.11.0, commander@^2.17.1, commander@^2.2.0, commander@^2.9.0: +commander@^2.11.0, commander@^2.17.1, commander@^2.2.0, commander@^2.9.0, commander@~2.17.1: version "2.17.1" resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" @@ -3252,7 +3253,20 @@ gulp-sourcemaps@^2.6.4: strip-bom-string "1.X" through2 "2.X" -gulp-util@^3.0.0, gulp-util@^3.0.8: +gulp-uglify@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/gulp-uglify/-/gulp-uglify-3.0.1.tgz#8d3eee466521bea6b10fd75dff72adf8b7ea2d97" + dependencies: + gulplog "^1.0.0" + has-gulplog "^0.1.0" + lodash "^4.13.1" + make-error-cause "^1.1.1" + safe-buffer "^5.1.2" + through2 "^2.0.0" + uglify-js "^3.0.5" + vinyl-sourcemaps-apply "^0.2.0" + +gulp-util@^3.0.0: version "3.0.8" resolved "https://registry.yarnpkg.com/gulp-util/-/gulp-util-3.0.8.tgz#0054e1e744502e27c04c187c3ecc505dd54bbb4f" dependencies: @@ -4693,6 +4707,16 @@ lru-queue@0.1: dependencies: es5-ext "~0.10.2" +make-error-cause@^1.1.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/make-error-cause/-/make-error-cause-1.2.2.tgz#df0388fcd0b37816dff0a5fb8108939777dcbc9d" + dependencies: + make-error "^1.2.0" + +make-error@^1.2.0: + version "1.3.5" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8" + make-iterator@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/make-iterator/-/make-iterator-1.0.1.tgz#29b33f312aa8f547c4a5e490f56afcec99133ad6" @@ -4915,6 +4939,10 @@ multipipe@^0.1.2: dependencies: duplexer2 "0.0.2" +mustache@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/mustache/-/mustache-2.3.2.tgz#a6d4d9c3f91d13359ab889a812954f9230a3d0c5" + mute-stream@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" @@ -6327,7 +6355,7 @@ source-map@^0.4.4: dependencies: amdefine ">=0.0.4" -source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.1, source-map@~0.5.3: +source-map@^0.5.0, source-map@^0.5.1, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.1, source-map@~0.5.3: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" @@ -6778,6 +6806,13 @@ uglify-js@^2.6: optionalDependencies: uglify-to-browserify "~1.0.0" +uglify-js@^3.0.5: + version "3.4.9" + resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.9.tgz#af02f180c1207d76432e473ed24a28f4a782bae3" + dependencies: + commander "~2.17.1" + source-map "~0.6.1" + uglify-to-browserify@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz#6e0924d6bda6b5afe349e39a6d632850a0f882b7" @@ -6954,6 +6989,12 @@ vinyl-source-stream@^2.0.0: through2 "^2.0.3" vinyl "^2.1.0" +vinyl-sourcemaps-apply@^0.2.0: + version "0.2.1" + resolved "https://registry.yarnpkg.com/vinyl-sourcemaps-apply/-/vinyl-sourcemaps-apply-0.2.1.tgz#ab6549d61d172c2b1b87be5c508d239c8ef87705" + dependencies: + source-map "^0.5.1" + vinyl@^0.4.0: version "0.4.6" resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-0.4.6.tgz#2f356c87a550a255461f36bbeb2a5ba8bf784847" diff --git a/scm-ui-components/pom.xml b/scm-ui-components/pom.xml index 19e8bc7a49..ba2c12d686 100644 --- a/scm-ui-components/pom.xml +++ b/scm-ui-components/pom.xml @@ -58,6 +58,16 @@ + + unlink + clean + + run + + + + + diff --git a/scm-ui/package.json b/scm-ui/package.json index ff90607293..ea7db55753 100644 --- a/scm-ui/package.json +++ b/scm-ui/package.json @@ -29,13 +29,13 @@ "redux-thunk": "^2.3.0" }, "scripts": { - "webfonts": "copyfiles -f node_modules/@fortawesome/fontawesome-free/webfonts/* target/styles/webfonts", - "build-css": "node-sass-chokidar --include-path ./styles --include-path ./node_modules styles/ -o target/styles", - "watch-css": "npm run build-css && node-sass-chokidar --include-path ./styles --include-path ./node_modules styles/ -o target/styles --watch --recursive", - "start-js": "ui-bundler serve --vendor vendor.bundle.js", + "webfonts": "copyfiles -f node_modules/@fortawesome/fontawesome-free/webfonts/* target/scm-ui/styles/webfonts", + "build-css": "node-sass-chokidar --include-path ./styles --include-path ./node_modules styles/ -o target/scm-ui/styles", + "watch-css": "npm run build-css && node-sass-chokidar --include-path ./styles --include-path ./node_modules styles/ -o target/scm-ui/styles --watch --recursive", + "start-js": "ui-bundler serve --target target/scm-ui --vendor vendor.bundle.js", "start": "npm-run-all -p webfonts watch-css start-js", - "build-js": "ui-bundler bundle target/scm-ui.bundle.js", - "build-vendor": "ui-bundler vendor target/vendor.bundle.js", + "build-js": "ui-bundler bundle --mode=production target/scm-ui/scm-ui.bundle.js", + "build-vendor": "ui-bundler vendor --mode=production target/scm-ui/vendor.bundle.js", "build": "npm-run-all -s webfonts build-css build-vendor build-js", "test": "ui-bundler test", "test-ci": "ui-bundler test --ci", @@ -43,7 +43,7 @@ "pre-commit": "jest && flow && eslint src" }, "devDependencies": { - "@scm-manager/ui-bundler": "^0.0.13", + "@scm-manager/ui-bundler": "^0.0.15", "copyfiles": "^2.0.0", "enzyme": "^3.3.0", "enzyme-adapter-react-16": "^1.1.1", diff --git a/scm-ui/pom.xml b/scm-ui/pom.xml index abe87e697f..5e3ca2b6f5 100644 --- a/scm-ui/pom.xml +++ b/scm-ui/pom.xml @@ -10,9 +10,9 @@ 2.0.0-SNAPSHOT - sonia.scm.clients + sonia.scm scm-ui - pom + war 2.0.0-SNAPSHOT scm-ui @@ -26,6 +26,7 @@ + scm-ui @@ -93,6 +94,20 @@ + + org.apache.maven.plugins + maven-war-plugin + 3.1.0 + + false + + + public + + + + + diff --git a/scm-ui/public/index.html b/scm-ui/public/index.mustache similarity index 56% rename from scm-ui/public/index.html rename to scm-ui/public/index.mustache index 23dc1b9782..802be2ca97 100644 --- a/scm-ui/public/index.html +++ b/scm-ui/public/index.mustache @@ -8,20 +8,11 @@ manifest.json provides metadata used when your web app is added to the homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/ --> - - - - - - + + SCM-Manager @@ -41,9 +32,9 @@ To create a production bundle, use `npm run build` or `yarn build`. --> - - + + diff --git a/scm-ui/src/users/modules/users.test.js b/scm-ui/src/users/modules/users.test.js index 895e28a7b0..c8c56f2ef5 100644 --- a/scm-ui/src/users/modules/users.test.js +++ b/scm-ui/src/users/modules/users.test.js @@ -56,7 +56,7 @@ const userZaphod = { displayName: "Z. Beeblebrox", mail: "president@heartofgold.universe", name: "zaphod", - password: "__dummypassword__", + password: "", type: "xml", properties: {}, _links: { @@ -79,7 +79,7 @@ const userFord = { displayName: "F. Prefect", mail: "ford@prefect.universe", name: "ford", - password: "__dummypassword__", + password: "", type: "xml", properties: {}, _links: { diff --git a/scm-webapp/pom.xml b/scm-webapp/pom.xml index fb5cc57e44..644a1bc0f9 100644 --- a/scm-webapp/pom.xml +++ b/scm-webapp/pom.xml @@ -49,7 +49,7 @@ scm-core 2.0.0-SNAPSHOT - + sonia.scm scm-dao-xml @@ -237,6 +237,12 @@ ${mustache.version} + + com.github.sdorra + web-resources + 1.0.2 + + com.github.sdorra spotter-core @@ -515,7 +521,7 @@ sonia.scm.ui.proxy - http://localhost:3000 + ${livereload.proxy} @@ -546,7 +552,42 @@ - + + + livereload + + + + livereload + + + + + http://localhost:3000 + + + + + ui-overlay + + + + !livereload + + + + + + + sonia.scm + scm-ui + 2.0.0-SNAPSHOT + war + + + + + release diff --git a/scm-webapp/src/main/java/sonia/scm/ForwardingPushStateDispatcher.java b/scm-webapp/src/main/java/sonia/scm/ForwardingPushStateDispatcher.java deleted file mode 100644 index 0b80f158f3..0000000000 --- a/scm-webapp/src/main/java/sonia/scm/ForwardingPushStateDispatcher.java +++ /dev/null @@ -1,24 +0,0 @@ -package sonia.scm; - -import javax.servlet.RequestDispatcher; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; -import java.io.IOException; - -/** - * This dispatcher forwards every request to the index.html of the application. - * - * @since 2.0.0 - */ -public class ForwardingPushStateDispatcher implements PushStateDispatcher { - @Override - public void dispatch(HttpServletRequest request, HttpServletResponse response, String uri) throws IOException { - RequestDispatcher dispatcher = request.getRequestDispatcher("/index.html"); - try { - dispatcher.forward(request, response); - } catch (ServletException e) { - throw new IOException("failed to forward request", e); - } - } -} diff --git a/scm-webapp/src/main/java/sonia/scm/PushStateDispatcherProvider.java b/scm-webapp/src/main/java/sonia/scm/PushStateDispatcherProvider.java index 653f7b4bdc..f0d2807497 100644 --- a/scm-webapp/src/main/java/sonia/scm/PushStateDispatcherProvider.java +++ b/scm-webapp/src/main/java/sonia/scm/PushStateDispatcherProvider.java @@ -3,12 +3,13 @@ package sonia.scm; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; +import javax.inject.Inject; import javax.inject.Provider; /** * Injection Provider for the {@link PushStateDispatcher}. The provider will return a {@link ProxyPushStateDispatcher} * if the system property {@code PushStateDispatcherProvider#PROPERTY_TARGET} is set to a proxy target url, otherwise - * a {@link ForwardingPushStateDispatcher} is used. + * a {@link TemplatingPushStateDispatcher} is used. * * @since 2.0.0 */ @@ -17,11 +18,18 @@ public class PushStateDispatcherProvider implements Provider templatingPushStateDispatcherProvider; + + @Inject + public PushStateDispatcherProvider(Provider templatingPushStateDispatcherProvider) { + this.templatingPushStateDispatcherProvider = templatingPushStateDispatcherProvider; + } + @Override public PushStateDispatcher get() { String target = System.getProperty(PROPERTY_TARGET); if (Strings.isNullOrEmpty(target)) { - return new ForwardingPushStateDispatcher(); + return templatingPushStateDispatcherProvider.get(); } return new ProxyPushStateDispatcher(target); } diff --git a/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java b/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java index 1eaef333a1..5a087a1c70 100644 --- a/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java +++ b/scm-webapp/src/main/java/sonia/scm/ScmContextListener.java @@ -33,8 +33,6 @@ package sonia.scm; -//~--- non-JDK imports -------------------------------------------------------- - import com.google.common.base.Throwables; import com.google.common.collect.Lists; import com.google.inject.Injector; @@ -63,8 +61,6 @@ import java.util.Collections; import java.util.List; import java.util.Set; -//~--- JDK imports ------------------------------------------------------------ - /** * * @author Sebastian Sdorra @@ -135,7 +131,7 @@ public class ScmContextListener extends GuiceResteasyBootstrapServletContextList moduleList.add(new EagerSingletonModule()); moduleList.add(ShiroWebModule.guiceFilterModule()); moduleList.add(new WebElementModule(pluginLoader)); - moduleList.add(new ScmServletModule(context, pluginLoader, overrides, pluginLoader.getExtensionProcessor())); + moduleList.add(new ScmServletModule(context, pluginLoader, overrides)); moduleList.add( new ScmSecurityModule(context, pluginLoader.getExtensionProcessor()) ); diff --git a/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java b/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java index 1d627de8ed..e9ec9e4a39 100644 --- a/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java +++ b/scm-webapp/src/main/java/sonia/scm/ScmServletModule.java @@ -33,8 +33,6 @@ package sonia.scm; -//~--- non-JDK imports -------------------------------------------------------- - import com.fasterxml.jackson.databind.ObjectMapper; import com.google.inject.Provider; import com.google.inject.multibindings.Multibinder; @@ -56,17 +54,48 @@ import sonia.scm.group.xml.XmlGroupDAO; import sonia.scm.io.DefaultFileSystem; import sonia.scm.io.FileSystem; import sonia.scm.net.SSLContextProvider; -import sonia.scm.net.ahc.*; -import sonia.scm.plugin.*; -import sonia.scm.repository.*; +import sonia.scm.net.ahc.AdvancedHttpClient; +import sonia.scm.net.ahc.ContentTransformer; +import sonia.scm.net.ahc.DefaultAdvancedHttpClient; +import sonia.scm.net.ahc.JsonContentTransformer; +import sonia.scm.net.ahc.XmlContentTransformer; +import sonia.scm.plugin.DefaultPluginLoader; +import sonia.scm.plugin.DefaultPluginManager; +import sonia.scm.plugin.PluginLoader; +import sonia.scm.plugin.PluginManager; +import sonia.scm.repository.DefaultRepositoryManager; +import sonia.scm.repository.DefaultRepositoryProvider; +import sonia.scm.repository.HealthCheckContextListener; +import sonia.scm.repository.NamespaceStrategy; +import sonia.scm.repository.NamespaceStrategyProvider; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryDAO; +import sonia.scm.repository.RepositoryManager; +import sonia.scm.repository.RepositoryManagerProvider; +import sonia.scm.repository.RepositoryProvider; import sonia.scm.repository.api.HookContextFactory; import sonia.scm.repository.api.RepositoryServiceFactory; import sonia.scm.repository.spi.HookEventFacade; import sonia.scm.repository.xml.XmlRepositoryDAO; import sonia.scm.schedule.QuartzScheduler; import sonia.scm.schedule.Scheduler; -import sonia.scm.security.*; -import sonia.scm.store.*; +import sonia.scm.security.AuthorizationChangedEventProducer; +import sonia.scm.security.CipherHandler; +import sonia.scm.security.CipherUtil; +import sonia.scm.security.ConfigurableLoginAttemptHandler; +import sonia.scm.security.DefaultKeyGenerator; +import sonia.scm.security.DefaultSecuritySystem; +import sonia.scm.security.KeyGenerator; +import sonia.scm.security.LoginAttemptHandler; +import sonia.scm.security.SecuritySystem; +import sonia.scm.store.BlobStoreFactory; +import sonia.scm.store.ConfigurationEntryStoreFactory; +import sonia.scm.store.ConfigurationStoreFactory; +import sonia.scm.store.DataStoreFactory; +import sonia.scm.store.FileBlobStoreFactory; +import sonia.scm.store.JAXBConfigurationEntryStoreFactory; +import sonia.scm.store.JAXBConfigurationStoreFactory; +import sonia.scm.store.JAXBDataStoreFactory; import sonia.scm.template.MustacheTemplateEngine; import sonia.scm.template.TemplateEngine; import sonia.scm.template.TemplateEngineFactory; @@ -81,14 +110,16 @@ import sonia.scm.util.ScmConfigurationUtil; import sonia.scm.web.UserAgentParser; import sonia.scm.web.cgi.CGIExecutorFactory; import sonia.scm.web.cgi.DefaultCGIExecutorFactory; +import sonia.scm.web.filter.AuthenticationFilter; import sonia.scm.web.filter.LoggingFilter; +import sonia.scm.web.protocol.HttpProtocolServlet; import sonia.scm.web.security.AdministrationContext; import sonia.scm.web.security.DefaultAdministrationContext; import javax.net.ssl.SSLContext; import javax.servlet.ServletContext; -//~--- JDK imports ------------------------------------------------------------ +import static sonia.scm.api.v2.resources.ScmPathInfo.REST_API_PATH; /** * @@ -99,14 +130,14 @@ public class ScmServletModule extends ServletModule /** Field description */ public static final String[] PATTERN_ADMIN = new String[] { - "/api/rest/groups*", - "/api/rest/users*", "/api/rest/plguins*" }; + REST_API_PATH + "/groups*", + REST_API_PATH + "/users*", REST_API_PATH + "/plguins*" }; /** Field description */ public static final String PATTERN_ALL = "/*"; /** Field description */ - public static final String PATTERN_CONFIG = "/api/rest/config*"; + public static final String PATTERN_CONFIG = REST_API_PATH + "/config*"; /** Field description */ public static final String PATTERN_DEBUG = "/debug.html"; @@ -155,22 +186,11 @@ public class ScmServletModule extends ServletModule //~--- constructors --------------------------------------------------------- - /** - * Constructs ... - * - * - * @param servletContext - * @param pluginLoader - * @param overrides - * @param extensionProcessor - */ - ScmServletModule(ServletContext servletContext, - DefaultPluginLoader pluginLoader, ClassOverrides overrides, ExtensionProcessor extensionProcessor) + ScmServletModule(ServletContext servletContext, DefaultPluginLoader pluginLoader, ClassOverrides overrides) { this.servletContext = servletContext; this.pluginLoader = pluginLoader; this.overrides = overrides; - this.extensionProcessor = extensionProcessor; } //~--- methods -------------------------------------------------------------- @@ -293,6 +313,8 @@ public class ScmServletModule extends ServletModule bind(TemplateEngineFactory.class); bind(ObjectMapper.class).toProvider(ObjectMapperProvider.class); + filter(HttpProtocolServlet.PATTERN).through(AuthenticationFilter.class); + // bind events // bind(LastModifiedUpdateListener.class); @@ -389,11 +411,6 @@ public class ScmServletModule extends ServletModule /** * Load ScmConfiguration with JAXB - * - * - * @param context - * - * @return */ private ScmConfiguration getScmConfiguration() { @@ -414,6 +431,4 @@ public class ScmServletModule extends ServletModule /** Field description */ private final ServletContext servletContext; - - private final ExtensionProcessor extensionProcessor; } diff --git a/scm-webapp/src/main/java/sonia/scm/TemplatingPushStateDispatcher.java b/scm-webapp/src/main/java/sonia/scm/TemplatingPushStateDispatcher.java new file mode 100644 index 0000000000..6652975c4a --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/TemplatingPushStateDispatcher.java @@ -0,0 +1,61 @@ +package sonia.scm; + +import com.google.common.annotations.VisibleForTesting; +import sonia.scm.template.Template; +import sonia.scm.template.TemplateEngine; +import sonia.scm.template.TemplateEngineFactory; + +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.Writer; + +/** + * This dispatcher renders the /index.mustache template, which is merged in from the scm-ui package. + * + * @since 2.0.0 + */ +public class TemplatingPushStateDispatcher implements PushStateDispatcher { + + @VisibleForTesting + static final String TEMPLATE = "/index.mustache"; + + private final TemplateEngine templateEngine; + + @Inject + public TemplatingPushStateDispatcher(TemplateEngineFactory templateEngineFactory) { + this(templateEngineFactory.getDefaultEngine()); + } + + @VisibleForTesting + TemplatingPushStateDispatcher(TemplateEngine templateEngine) { + this.templateEngine = templateEngine; + } + + @Override + public void dispatch(HttpServletRequest request, HttpServletResponse response, String uri) throws IOException { + response.setContentType("text/html"); + response.setCharacterEncoding("UTF-8"); + + Template template = templateEngine.getTemplate(TEMPLATE); + try (Writer writer = response.getWriter()) { + template.execute(writer, new IndexHtmlModel(request)); + } + } + + @VisibleForTesting + static class IndexHtmlModel { + + private final HttpServletRequest request; + + private IndexHtmlModel(HttpServletRequest request) { + this.request = request; + } + + public String getContextPath() { + return request.getContextPath(); + } + + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/WebResourceServlet.java b/scm-webapp/src/main/java/sonia/scm/WebResourceServlet.java index 764e4f18d2..059c990718 100644 --- a/scm-webapp/src/main/java/sonia/scm/WebResourceServlet.java +++ b/scm-webapp/src/main/java/sonia/scm/WebResourceServlet.java @@ -1,7 +1,7 @@ package sonia.scm; +import com.github.sdorra.webresources.WebResourceSender; import com.google.common.annotations.VisibleForTesting; -import com.google.common.io.Resources; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.filter.WebElement; @@ -15,7 +15,6 @@ import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; -import java.io.OutputStream; import java.net.URL; /** @@ -27,16 +26,22 @@ import java.net.URL; @WebElement(value = WebResourceServlet.PATTERN, regex = true) public class WebResourceServlet extends HttpServlet { + /** * exclude api requests and the old frontend servlets. * * TODO remove old protocol servlets and hook. Move /hook/hg to api? */ @VisibleForTesting - static final String PATTERN = "/(?!api/|git/|hg/|svn/|hook/).*"; + static final String PATTERN = "/(?!api/|git/|hg/|svn/|hook/|repo/).*"; private static final Logger LOG = LoggerFactory.getLogger(WebResourceServlet.class); + private final WebResourceSender sender = WebResourceSender.create() + .withGZIP() + .withGZIPMinLength(512) + .withBufferSize(16384); + private final UberWebResourceLoader webResourceLoader; private final PushStateDispatcher pushStateDispatcher; @@ -53,7 +58,7 @@ public class WebResourceServlet extends HttpServlet { LOG.trace("try to load {}", uri); URL url = webResourceLoader.getResource(uri); if (url != null) { - serveResource(response, url); + serveResource(request, response, url); } else { dispatch(request, response, uri); } @@ -72,10 +77,9 @@ public class WebResourceServlet extends HttpServlet { return HttpUtil.getStrippedURI(request); } - private void serveResource(HttpServletResponse response, URL url) { - // TODO lastModifiedDate, if-... ??? - try (OutputStream output = response.getOutputStream()) { - Resources.copy(url, output); + private void serveResource(HttpServletRequest request, HttpServletResponse response, URL url) { + try { + sender.resource(url).send(request, response); } catch (IOException ex) { LOG.warn("failed to serve resource: {}", url); response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/UriInfoFilter.java b/scm-webapp/src/main/java/sonia/scm/api/rest/UriInfoFilter.java index de0f1dd626..b602e918ea 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/UriInfoFilter.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/UriInfoFilter.java @@ -1,24 +1,26 @@ package sonia.scm.api.rest; -import sonia.scm.api.v2.resources.UriInfoStore; +import sonia.scm.api.v2.resources.ScmPathInfoStore; import javax.inject.Inject; import javax.ws.rs.container.ContainerRequestContext; import javax.ws.rs.container.ContainerRequestFilter; +import javax.ws.rs.core.UriInfo; import javax.ws.rs.ext.Provider; @Provider public class UriInfoFilter implements ContainerRequestFilter { - private final javax.inject.Provider storeProvider; + private final javax.inject.Provider storeProvider; @Inject - public UriInfoFilter(javax.inject.Provider storeProvider) { + public UriInfoFilter(javax.inject.Provider storeProvider) { this.storeProvider = storeProvider; } @Override public void filter(ContainerRequestContext requestContext) { - storeProvider.get().set(requestContext.getUriInfo()); + UriInfo uriInfo = requestContext.getUriInfo(); + storeProvider.get().set(uriInfo::getBaseUri); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryRootResource.java index cec1894764..d5ea6c88de 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/rest/resources/RepositoryRootResource.java @@ -33,37 +33,28 @@ package sonia.scm.api.rest.resources; -//~--- non-JDK imports -------------------------------------------------------- - import com.google.common.base.Function; import com.google.common.collect.Collections2; import com.google.common.collect.Maps; import com.google.common.collect.Ordering; import com.google.inject.Inject; - import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryManager; import sonia.scm.repository.RepositoryTypePredicate; -import sonia.scm.util.HttpUtil; - -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; - -import java.util.Collection; -import java.util.Comparator; -import java.util.List; -import java.util.Map; +import sonia.scm.template.Viewable; import javax.servlet.http.HttpServletRequest; - import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; -import sonia.scm.template.Viewable; +import java.io.IOException; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Map; /** * @@ -106,13 +97,12 @@ public class RepositoryRootResource @Produces(MediaType.TEXT_HTML) public Viewable renderRepositoriesRoot(@Context HttpServletRequest request, @PathParam("type") final String type) { - String baseUrl = HttpUtil.getCompleteUrl(request); //J- Collection unsortedRepositories = Collections2.transform( Collections2.filter( repositoryManager.getAll(), new RepositoryTypePredicate(type)) - , new RepositoryTransformFunction(baseUrl) + , new RepositoryTransformFunction() ); List repositories = Ordering.from( @@ -138,17 +128,9 @@ public class RepositoryRootResource public static class RepositoryTemplateElement { - /** - * Constructs ... - * - * - * @param repository - * @param baseUrl - */ - public RepositoryTemplateElement(Repository repository, String baseUrl) + public RepositoryTemplateElement(Repository repository) { this.repository = repository; - this.baseUrl = baseUrl; } //~--- get methods -------------------------------------------------------- @@ -175,22 +157,8 @@ public class RepositoryRootResource return repository; } - /** - * Method description - * - * - * @return - */ - public String getUrl() - { - return repository.createUrl(baseUrl); - } - //~--- fields ------------------------------------------------------------- - /** Field description */ - private String baseUrl; - /** Field description */ private Repository repository; @@ -236,31 +204,10 @@ public class RepositoryRootResource private static class RepositoryTransformFunction implements Function { - - public RepositoryTransformFunction(String baseUrl) - { - this.baseUrl = baseUrl; - } - - //~--- methods ------------------------------------------------------------ - - /** - * Method description - * - * - * @param repository - * - * @return - */ @Override public RepositoryTemplateElement apply(Repository repository) { - return new RepositoryTemplateElement(repository, baseUrl); + return new RepositoryTemplateElement(repository); } - - //~--- fields ------------------------------------------------------------- - - /** Field description */ - private String baseUrl; } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/ValidationConstraints.java b/scm-webapp/src/main/java/sonia/scm/api/v2/ValidationConstraints.java new file mode 100644 index 0000000000..b136ee1bf7 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/ValidationConstraints.java @@ -0,0 +1,14 @@ +package sonia.scm.api.v2; + +public final class ValidationConstraints { + + private ValidationConstraints() {} + + /** + * A user or group name should not start with @ or a whitespace + * and it not contains whitespaces + * and the characters: . - _ @ are allowed + */ + public static final String USER_GROUP_PATTERN = "^[A-Za-z0-9\\.\\-_][A-Za-z0-9\\.\\-_@]*$"; + +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BasicCollectionToDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BasicCollectionToDtoMapper.java index b189a22d1c..011c78b413 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BasicCollectionToDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BasicCollectionToDtoMapper.java @@ -1,73 +1,21 @@ package sonia.scm.api.v2.resources; -import de.otto.edison.hal.Embedded; import de.otto.edison.hal.HalRepresentation; -import de.otto.edison.hal.Links; -import de.otto.edison.hal.paging.NumberedPaging; -import de.otto.edison.hal.paging.PagingRel; import sonia.scm.ModelObject; import sonia.scm.PageResult; -import javax.inject.Inject; -import java.util.EnumSet; -import java.util.List; import java.util.Optional; -import java.util.function.Function; -import static com.damnhandy.uri.template.UriTemplate.fromTemplate; -import static de.otto.edison.hal.Embedded.embeddedBuilder; -import static de.otto.edison.hal.Link.link; -import static de.otto.edison.hal.Links.linkingTo; -import static de.otto.edison.hal.paging.NumberedPaging.zeroBasedNumberedPaging; -import static java.util.stream.Collectors.toList; - -abstract class BasicCollectionToDtoMapper> { - - private final String collectionName; +public class BasicCollectionToDtoMapper> extends PagedCollectionToDtoMapper { private final M entityToDtoMapper; - @Inject public BasicCollectionToDtoMapper(String collectionName, M entityToDtoMapper) { - this.collectionName = collectionName; + super(collectionName); this.entityToDtoMapper = entityToDtoMapper; } CollectionDto map(int pageNumber, int pageSize, PageResult pageResult, String selfLink, Optional createLink) { return map(pageNumber, pageSize, pageResult, selfLink, createLink, entityToDtoMapper::map); } - - CollectionDto map(int pageNumber, int pageSize, PageResult pageResult, String selfLink, Optional createLink, Function mapper) { - NumberedPaging paging = zeroBasedNumberedPaging(pageNumber, pageSize, pageResult.getOverallCount()); - List dtos = pageResult.getEntities().stream().map(mapper).collect(toList()); - CollectionDto collectionDto = new CollectionDto( - createLinks(paging, selfLink, createLink), - embedDtos(dtos)); - collectionDto.setPage(pageNumber); - collectionDto.setPageTotal(computePageTotal(pageSize, pageResult)); - return collectionDto; - } - - private int computePageTotal(int pageSize, PageResult pageResult) { - if (pageResult.getOverallCount() % pageSize > 0) { - return pageResult.getOverallCount() / pageSize + 1; - } else { - return pageResult.getOverallCount() / pageSize; - } - } - - private Links createLinks(NumberedPaging page, String selfLink, Optional createLink) { - Links.Builder linksBuilder = linkingTo() - .with(page.links( - fromTemplate(selfLink + "{?page,pageSize}"), - EnumSet.allOf(PagingRel.class))); - createLink.ifPresent(link -> linksBuilder.single(link("create", link))); - return linksBuilder.build(); - } - - private Embedded embedDtos(List dtos) { - return embeddedBuilder() - .with(collectionName, dtos) - .build(); - } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchChangesetCollectionToDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchChangesetCollectionToDtoMapper.java new file mode 100644 index 0000000000..afe8ad318b --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchChangesetCollectionToDtoMapper.java @@ -0,0 +1,26 @@ +package sonia.scm.api.v2.resources; + +import sonia.scm.PageResult; +import sonia.scm.repository.Changeset; +import sonia.scm.repository.Repository; + +import javax.inject.Inject; + +public class BranchChangesetCollectionToDtoMapper extends ChangesetCollectionToDtoMapperBase { + + private final ResourceLinks resourceLinks; + + @Inject + public BranchChangesetCollectionToDtoMapper(ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper, ResourceLinks resourceLinks) { + super(changesetToChangesetDtoMapper); + this.resourceLinks = resourceLinks; + } + + public CollectionDto map(int pageNumber, int pageSize, PageResult pageResult, Repository repository, String branch) { + return this.map(pageNumber, pageSize, pageResult, repository, () -> createSelfLink(repository, branch)); + } + + private String createSelfLink(Repository repository, String branch) { + return resourceLinks.branch().history(repository.getNamespaceAndName(), branch); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java index 9763f11def..7414f5c21f 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/BranchRootResource.java @@ -3,6 +3,7 @@ package sonia.scm.api.v2.resources; import com.webcohesion.enunciate.metadata.rs.ResponseCode; import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; +import sonia.scm.NotFoundException; import sonia.scm.PageResult; import sonia.scm.repository.Branches; import sonia.scm.repository.Changeset; @@ -32,14 +33,14 @@ public class BranchRootResource { private final BranchToBranchDtoMapper branchToDtoMapper; private final BranchCollectionToDtoMapper branchCollectionToDtoMapper; - private final ChangesetCollectionToDtoMapper changesetCollectionToDtoMapper; + private final BranchChangesetCollectionToDtoMapper branchChangesetCollectionToDtoMapper; @Inject - public BranchRootResource(RepositoryServiceFactory serviceFactory, BranchToBranchDtoMapper branchToDtoMapper, BranchCollectionToDtoMapper branchCollectionToDtoMapper, ChangesetCollectionToDtoMapper changesetCollectionToDtoMapper) { + public BranchRootResource(RepositoryServiceFactory serviceFactory, BranchToBranchDtoMapper branchToDtoMapper, BranchCollectionToDtoMapper branchCollectionToDtoMapper, BranchChangesetCollectionToDtoMapper changesetCollectionToDtoMapper) { this.serviceFactory = serviceFactory; this.branchToDtoMapper = branchToDtoMapper; this.branchCollectionToDtoMapper = branchCollectionToDtoMapper; - this.changesetCollectionToDtoMapper = changesetCollectionToDtoMapper; + this.branchChangesetCollectionToDtoMapper = changesetCollectionToDtoMapper; } /** @@ -98,6 +99,14 @@ public class BranchRootResource { @DefaultValue("0") @QueryParam("page") int page, @DefaultValue("10") @QueryParam("pageSize") int pageSize) throws Exception { try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { + boolean branchExists = repositoryService.getBranchesCommand() + .getBranches() + .getBranches() + .stream() + .anyMatch(branch -> branchName.equals(branch.getName())); + if (!branchExists){ + throw new NotFoundException("branch", branchName); + } Repository repository = repositoryService.getRepository(); RepositoryPermissions.read(repository).check(); ChangesetPagingResult changesets = repositoryService.getLogCommand() @@ -107,7 +116,7 @@ public class BranchRootResource { .getChangesets(); if (changesets != null && changesets.getChangesets() != null) { PageResult pageResult = new PageResult<>(changesets.getChangesets(), changesets.getTotal()); - return Response.ok(changesetCollectionToDtoMapper.map(page, pageSize, pageResult, repository)).build(); + return Response.ok(branchChangesetCollectionToDtoMapper.map(page, pageSize, pageResult, repository, branchName)).build(); } else { return Response.ok().build(); } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangePasswordNotAllowedExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangePasswordNotAllowedExceptionMapper.java new file mode 100644 index 0000000000..e9bb5304a5 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangePasswordNotAllowedExceptionMapper.java @@ -0,0 +1,17 @@ +package sonia.scm.api.v2.resources; + +import sonia.scm.user.ChangePasswordNotAllowedException; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +@Provider +public class ChangePasswordNotAllowedExceptionMapper implements ExceptionMapper { + @Override + public Response toResponse(ChangePasswordNotAllowedException exception) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(exception.getMessage()) + .build(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetCollectionToDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetCollectionToDtoMapper.java index fcc4085486..24ee9b0ce1 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetCollectionToDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetCollectionToDtoMapper.java @@ -5,22 +5,19 @@ import sonia.scm.repository.Changeset; import sonia.scm.repository.Repository; import javax.inject.Inject; -import java.util.Optional; -public class ChangesetCollectionToDtoMapper extends BasicCollectionToDtoMapper { +public class ChangesetCollectionToDtoMapper extends ChangesetCollectionToDtoMapperBase { - private final ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper; private final ResourceLinks resourceLinks; @Inject public ChangesetCollectionToDtoMapper(ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper, ResourceLinks resourceLinks) { - super("changesets", changesetToChangesetDtoMapper); - this.changesetToChangesetDtoMapper = changesetToChangesetDtoMapper; + super(changesetToChangesetDtoMapper); this.resourceLinks = resourceLinks; } public CollectionDto map(int pageNumber, int pageSize, PageResult pageResult, Repository repository) { - return super.map(pageNumber, pageSize, pageResult, createSelfLink(repository), Optional.empty(), changeset -> changesetToChangesetDtoMapper.map(changeset, repository)); + return super.map(pageNumber, pageSize, pageResult, repository, () -> createSelfLink(repository)); } private String createSelfLink(Repository repository) { diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetCollectionToDtoMapperBase.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetCollectionToDtoMapperBase.java new file mode 100644 index 0000000000..e29a0a92b2 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetCollectionToDtoMapperBase.java @@ -0,0 +1,23 @@ +package sonia.scm.api.v2.resources; + +import sonia.scm.PageResult; +import sonia.scm.repository.Changeset; +import sonia.scm.repository.Repository; + +import java.util.Optional; +import java.util.function.Supplier; + +class ChangesetCollectionToDtoMapperBase extends PagedCollectionToDtoMapper { + + private final ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper; + + ChangesetCollectionToDtoMapperBase(ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper) { + super("changesets"); + this.changesetToChangesetDtoMapper = changesetToChangesetDtoMapper; + } + + CollectionDto map(int pageNumber, int pageSize, PageResult pageResult, Repository repository, Supplier selfLinkSupplier) { + return super.map(pageNumber, pageSize, pageResult, selfLinkSupplier.get(), Optional.empty(), changeset -> changesetToChangesetDtoMapper.map(changeset, repository)); + } +} + diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetToChangesetDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetToChangesetDtoMapper.java index f047e3bbaa..a189dcec97 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetToChangesetDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ChangesetToChangesetDtoMapper.java @@ -23,7 +23,7 @@ import static de.otto.edison.hal.Link.link; import static de.otto.edison.hal.Links.linkingTo; @Mapper -public abstract class ChangesetToChangesetDtoMapper extends BaseMapper { +public abstract class ChangesetToChangesetDtoMapper implements InstantAttributeMapper { @Inject private RepositoryServiceFactory serviceFactory; @@ -65,7 +65,8 @@ public abstract class ChangesetToChangesetDtoMapper extends BaseMapper { +public abstract class ChangesetToParentDtoMapper { @Inject private ResourceLinks resourceLinks; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileHistoryCollectionToDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileHistoryCollectionToDtoMapper.java new file mode 100644 index 0000000000..af7fb2ed83 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileHistoryCollectionToDtoMapper.java @@ -0,0 +1,27 @@ +package sonia.scm.api.v2.resources; + +import sonia.scm.PageResult; +import sonia.scm.repository.Changeset; +import sonia.scm.repository.Repository; + +import javax.inject.Inject; + +public class FileHistoryCollectionToDtoMapper extends ChangesetCollectionToDtoMapperBase { + + + private final ResourceLinks resourceLinks; + + @Inject + public FileHistoryCollectionToDtoMapper(ChangesetToChangesetDtoMapper changesetToChangesetDtoMapper, ResourceLinks resourceLinks) { + super(changesetToChangesetDtoMapper); + this.resourceLinks = resourceLinks; + } + + public CollectionDto map(int pageNumber, int pageSize, PageResult pageResult, Repository repository, String revision, String path) { + return super.map(pageNumber, pageSize, pageResult, repository, () -> createSelfLink(repository, revision, path)); + } + + private String createSelfLink(Repository repository, String revision, String path) { + return resourceLinks.fileHistory().self(repository.getNamespace(), repository.getName(), revision, path); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileHistoryRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileHistoryRootResource.java new file mode 100644 index 0000000000..118cc4167a --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileHistoryRootResource.java @@ -0,0 +1,92 @@ +package sonia.scm.api.v2.resources; + +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import com.webcohesion.enunciate.metadata.rs.TypeHint; +import lombok.extern.slf4j.Slf4j; +import sonia.scm.PageResult; +import sonia.scm.repository.Changeset; +import sonia.scm.repository.ChangesetPagingResult; +import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryNotFoundException; +import sonia.scm.repository.RevisionNotFoundException; +import sonia.scm.repository.api.RepositoryService; +import sonia.scm.repository.api.RepositoryServiceFactory; +import sonia.scm.web.VndMediaType; + +import javax.inject.Inject; +import javax.ws.rs.DefaultValue; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import javax.ws.rs.core.Response; +import java.io.IOException; + +@Slf4j +public class FileHistoryRootResource { + + private final RepositoryServiceFactory serviceFactory; + + private final FileHistoryCollectionToDtoMapper fileHistoryCollectionToDtoMapper; + + + @Inject + public FileHistoryRootResource(RepositoryServiceFactory serviceFactory, FileHistoryCollectionToDtoMapper fileHistoryCollectionToDtoMapper) { + this.serviceFactory = serviceFactory; + this.fileHistoryCollectionToDtoMapper = fileHistoryCollectionToDtoMapper; + } + + /** + * Get all changesets related to the given file starting with the given revision + * + * @param namespace the repository namespace + * @param name the repository name + * @param revision the revision + * @param path the path of the file + * @param page pagination + * @param pageSize pagination + * @return all changesets related to the given file starting with the given revision + * @throws IOException on io error + * @throws RevisionNotFoundException on missing revision + * @throws RepositoryNotFoundException on missing repository + */ + @GET + @Path("{revision}/{path: .*}") + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the changeset"), + @ResponseCode(code = 404, condition = "not found, no changesets available in the repository"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @Produces(VndMediaType.CHANGESET_COLLECTION) + @TypeHint(CollectionDto.class) + public Response getAll(@PathParam("namespace") String namespace, @PathParam("name") String name, + @PathParam("revision") String revision, + @PathParam("path") String path, + @DefaultValue("0") @QueryParam("page") int page, + @DefaultValue("10") @QueryParam("pageSize") int pageSize) throws IOException, RevisionNotFoundException, RepositoryNotFoundException { + try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { + log.info("Get changesets of the file {} and revision {}", path, revision); + Repository repository = repositoryService.getRepository(); + ChangesetPagingResult changesets = repositoryService.getLogCommand() + .setPagingStart(page) + .setPagingLimit(pageSize) + .setPath(path) + .setStartChangeset(revision) + .getChangesets(); + if (changesets != null && changesets.getChangesets() != null) { + PageResult pageResult = new PageResult<>(changesets.getChangesets(), changesets.getTotal()); + return Response.ok(fileHistoryCollectionToDtoMapper.map(page, pageSize, pageResult, repository, revision, path)).build(); + } else { + String message = String.format("for the revision %s and the file %s there is no changesets", revision, path); + log.error(message); + throw new InternalRepositoryException(message); + } + } + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectToFileObjectDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectToFileObjectDtoMapper.java index fdcc5c56ca..365c0ad4cb 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectToFileObjectDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/FileObjectToFileObjectDtoMapper.java @@ -4,6 +4,7 @@ import de.otto.edison.hal.Links; import org.mapstruct.AfterMapping; import org.mapstruct.Context; import org.mapstruct.Mapper; +import org.mapstruct.Mapping; import org.mapstruct.MappingTarget; import sonia.scm.repository.FileObject; import sonia.scm.repository.NamespaceAndName; @@ -11,12 +12,15 @@ import sonia.scm.repository.SubRepository; import javax.inject.Inject; +import static de.otto.edison.hal.Link.link; + @Mapper -public abstract class FileObjectToFileObjectDtoMapper extends BaseMapper { +public abstract class FileObjectToFileObjectDtoMapper implements InstantAttributeMapper { @Inject private ResourceLinks resourceLinks; + @Mapping(target = "attributes", ignore = true) // We do not map HAL attributes protected abstract FileObjectDto map(FileObject fileObject, @Context NamespaceAndName namespaceAndName, @Context String revision); abstract SubRepositoryDto mapSubrepository(SubRepository subRepository); @@ -29,6 +33,7 @@ public abstract class FileObjectToFileObjectDtoMapper extends BaseMapper applyChanges, Consumer checker) throws NotFoundException, ConcurrentModificationException { + return singleAdapter.update( + loadBy(id), + applyChanges, + idStaysTheSame(id), + checker + ); + } + public Response update(String id, Function applyChanges) throws NotFoundException, ConcurrentModificationException { return singleAdapter.update( loadBy(id), diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDto.java new file mode 100644 index 0000000000..9346420f58 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDto.java @@ -0,0 +1,16 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.HalRepresentation; +import de.otto.edison.hal.Links; +import lombok.Getter; + +@Getter +public class IndexDto extends HalRepresentation { + + private final String version; + + IndexDto(String version, Links links) { + super(links); + this.version = version; + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java new file mode 100644 index 0000000000..c8159f072c --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexDtoGenerator.java @@ -0,0 +1,50 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.Links; +import org.apache.shiro.SecurityUtils; +import sonia.scm.SCMContextProvider; +import sonia.scm.config.ConfigurationPermissions; +import sonia.scm.group.GroupPermissions; +import sonia.scm.user.UserPermissions; + +import javax.inject.Inject; + +import static de.otto.edison.hal.Link.link; + +public class IndexDtoGenerator { + + private final ResourceLinks resourceLinks; + private final SCMContextProvider scmContextProvider; + + @Inject + public IndexDtoGenerator(ResourceLinks resourceLinks, SCMContextProvider scmContextProvider) { + this.resourceLinks = resourceLinks; + this.scmContextProvider = scmContextProvider; + } + + public IndexDto generate() { + Links.Builder builder = Links.linkingTo(); + builder.self(resourceLinks.index().self()); + builder.single(link("uiPlugins", resourceLinks.uiPluginCollection().self())); + if (SecurityUtils.getSubject().isAuthenticated()) { + builder.single( + link("me", resourceLinks.me().self()), + link("logout", resourceLinks.authentication().logout()) + ); + if (UserPermissions.list().isPermitted()) { + builder.single(link("users", resourceLinks.userCollection().self())); + } + if (GroupPermissions.list().isPermitted()) { + builder.single(link("groups", resourceLinks.groupCollection().self())); + } + if (ConfigurationPermissions.list().isPermitted()) { + builder.single(link("config", resourceLinks.config().self())); + } + builder.single(link("repositories", resourceLinks.repositoryCollection().self())); + } else { + builder.single(link("login", resourceLinks.authentication().jsonLogin())); + } + + return new IndexDto(scmContextProvider.getVersion(), builder.build()); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexResource.java new file mode 100644 index 0000000000..088558c7dc --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/IndexResource.java @@ -0,0 +1,29 @@ +package sonia.scm.api.v2.resources; + +import com.webcohesion.enunciate.metadata.rs.TypeHint; +import sonia.scm.web.VndMediaType; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; + +@Path(IndexResource.INDEX_PATH_V2) +public class IndexResource { + public static final String INDEX_PATH_V2 = "v2/"; + + private final IndexDtoGenerator indexDtoGenerator; + + @Inject + public IndexResource(IndexDtoGenerator indexDtoGenerator) { + this.indexDtoGenerator = indexDtoGenerator; + } + + @GET + @Path("") + @Produces(VndMediaType.INDEX) + @TypeHint(IndexDto.class) + public IndexDto getIndex() { + return indexDtoGenerator.generate(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/InternalRepositoryExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/InternalRepositoryExceptionMapper.java new file mode 100644 index 0000000000..d30677c5f7 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/InternalRepositoryExceptionMapper.java @@ -0,0 +1,15 @@ +package sonia.scm.api.v2.resources; + +import sonia.scm.api.rest.StatusExceptionMapper; +import sonia.scm.repository.InternalRepositoryException; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.Provider; + +@Provider +public class InternalRepositoryExceptionMapper extends StatusExceptionMapper { + + public InternalRepositoryExceptionMapper() { + super(InternalRepositoryException.class, Response.Status.INTERNAL_SERVER_ERROR); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/InvalidPasswordExceptionMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/InvalidPasswordExceptionMapper.java new file mode 100644 index 0000000000..7c5364ba03 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/InvalidPasswordExceptionMapper.java @@ -0,0 +1,17 @@ +package sonia.scm.api.v2.resources; + +import sonia.scm.user.InvalidPasswordException; + +import javax.ws.rs.core.Response; +import javax.ws.rs.ext.ExceptionMapper; +import javax.ws.rs.ext.Provider; + +@Provider +public class InvalidPasswordExceptionMapper implements ExceptionMapper { + @Override + public Response toResponse(InvalidPasswordException exception) { + return Response.status(Response.Status.BAD_REQUEST) + .entity(exception.getMessage()) + .build(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java index 9cadbfb6ff..03b5728627 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MapperModule.java @@ -8,6 +8,7 @@ public class MapperModule extends AbstractModule { @Override protected void configure() { bind(UserDtoToUserMapper.class).to(Mappers.getMapper(UserDtoToUserMapper.class).getClass()); + bind(MeToUserDtoMapper.class).to(Mappers.getMapper(MeToUserDtoMapper.class).getClass()); bind(UserToUserDtoMapper.class).to(Mappers.getMapper(UserToUserDtoMapper.class).getClass()); bind(UserCollectionToDtoMapper.class); @@ -34,11 +35,12 @@ public class MapperModule extends AbstractModule { bind(TagToTagDtoMapper.class).to(Mappers.getMapper(TagToTagDtoMapper.class).getClass()); bind(FileObjectToFileObjectDtoMapper.class).to(Mappers.getMapper(FileObjectToFileObjectDtoMapper.class).getClass()); + bind(ModificationsToDtoMapper.class).to(Mappers.getMapper(ModificationsToDtoMapper.class).getClass()); // no mapstruct required bind(UIPluginDtoMapper.class); bind(UIPluginDtoCollectionMapper.class); - bind(UriInfoStore.class).in(ServletScopes.REQUEST); + bind(ScmPathInfoStore.class).in(ServletScopes.REQUEST); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeResource.java index f016f4604c..e684bc25db 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeResource.java @@ -4,19 +4,27 @@ import com.webcohesion.enunciate.metadata.rs.ResponseCode; import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; import org.apache.shiro.SecurityUtils; +import org.apache.shiro.authc.credential.PasswordService; +import sonia.scm.ConcurrentModificationException; import sonia.scm.NotFoundException; +import sonia.scm.user.InvalidPasswordException; import sonia.scm.user.User; import sonia.scm.user.UserManager; import sonia.scm.web.VndMediaType; import javax.inject.Inject; +import javax.ws.rs.Consumes; import javax.ws.rs.GET; +import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.Context; import javax.ws.rs.core.Request; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; +import java.util.function.Consumer; + +import static sonia.scm.user.InvalidPasswordException.INVALID_MATCHING; /** @@ -24,15 +32,20 @@ import javax.ws.rs.core.UriInfo; */ @Path(MeResource.ME_PATH_V2) public class MeResource { - static final String ME_PATH_V2 = "v2/me/"; + public static final String ME_PATH_V2 = "v2/me/"; - private final UserToUserDtoMapper userToDtoMapper; + private final MeToUserDtoMapper meToUserDtoMapper; private final IdResourceManagerAdapter adapter; + private final PasswordService passwordService; + private final UserManager userManager; + @Inject - public MeResource(UserToUserDtoMapper userToDtoMapper, UserManager manager) { - this.userToDtoMapper = userToDtoMapper; + public MeResource(MeToUserDtoMapper meToUserDtoMapper, UserManager manager, PasswordService passwordService) { + this.meToUserDtoMapper = meToUserDtoMapper; this.adapter = new IdResourceManagerAdapter<>(manager, User.class); + this.passwordService = passwordService; + this.userManager = manager; } /** @@ -50,6 +63,34 @@ public class MeResource { public Response get(@Context Request request, @Context UriInfo uriInfo) throws NotFoundException { String id = (String) SecurityUtils.getSubject().getPrincipals().getPrimaryPrincipal(); - return adapter.get(id, userToDtoMapper::map); + return adapter.get(id, meToUserDtoMapper::map); + } + + /** + * Change password of the current user + */ + @PUT + @Path("password") + @StatusCodes({ + @ResponseCode(code = 204, condition = "update success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @TypeHint(TypeHint.NO_CONTENT.class) + @Consumes(VndMediaType.PASSWORD_CHANGE) + public Response changePassword(PasswordChangeDto passwordChangeDto) throws NotFoundException, ConcurrentModificationException { + String name = (String) SecurityUtils.getSubject().getPrincipals().getPrimaryPrincipal(); + return adapter.update(name, user -> user.changePassword(passwordService.encryptPassword(passwordChangeDto.getNewPassword())), userManager.getUserTypeChecker().andThen(getOldOriginalPasswordChecker(passwordChangeDto.getOldPassword()))); + } + + /** + * Match given old password from the dto with the stored password before updating + */ + private Consumer getOldOriginalPasswordChecker(String oldPassword) { + return user -> { + if (!user.getPassword().equals(passwordService.encryptPassword(oldPassword))) { + throw new InvalidPasswordException(INVALID_MATCHING); + } + }; } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeToUserDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeToUserDtoMapper.java new file mode 100644 index 0000000000..2a872eadd9 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/MeToUserDtoMapper.java @@ -0,0 +1,42 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.Links; +import org.mapstruct.AfterMapping; +import org.mapstruct.Mapper; +import org.mapstruct.MappingTarget; +import sonia.scm.user.User; +import sonia.scm.user.UserManager; +import sonia.scm.user.UserPermissions; + +import javax.inject.Inject; + +import static de.otto.edison.hal.Link.link; +import static de.otto.edison.hal.Links.linkingTo; + +@Mapper +public abstract class MeToUserDtoMapper extends UserToUserDtoMapper{ + + @Inject + private UserManager userManager; + + @Inject + private ResourceLinks resourceLinks; + + + @Override + @AfterMapping + protected void appendLinks(User user, @MappingTarget UserDto target) { + Links.Builder linksBuilder = linkingTo().self(resourceLinks.me().self()); + if (UserPermissions.delete(user).isPermitted()) { + linksBuilder.single(link("delete", resourceLinks.me().delete(target.getName()))); + } + if (UserPermissions.modify(user).isPermitted()) { + linksBuilder.single(link("update", resourceLinks.me().update(target.getName()))); + } + if (userManager.isTypeDefault(user)) { + linksBuilder.single(link("password", resourceLinks.me().passwordChange())); + } + target.add(linksBuilder.build()); + } + +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ModificationsDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ModificationsDto.java new file mode 100644 index 0000000000..9ea0359157 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ModificationsDto.java @@ -0,0 +1,39 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.HalRepresentation; +import de.otto.edison.hal.Links; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +@NoArgsConstructor +public class ModificationsDto extends HalRepresentation { + + + private String revision; + /** + * list of added files + */ + private List added; + + /** + * list of modified files + */ + private List modified; + + /** + * list of removed files + */ + private List removed; + + @Override + @SuppressWarnings("squid:S1185") // We want to have this method available in this package + protected HalRepresentation add(Links links) { + return super.add(links); + } + +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ModificationsRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ModificationsRootResource.java new file mode 100644 index 0000000000..28f855f40c --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ModificationsRootResource.java @@ -0,0 +1,62 @@ +package sonia.scm.api.v2.resources; + +import com.webcohesion.enunciate.metadata.rs.ResponseCode; +import com.webcohesion.enunciate.metadata.rs.StatusCodes; +import com.webcohesion.enunciate.metadata.rs.TypeHint; +import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.repository.Modifications; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.RepositoryNotFoundException; +import sonia.scm.repository.RevisionNotFoundException; +import sonia.scm.repository.api.RepositoryService; +import sonia.scm.repository.api.RepositoryServiceFactory; +import sonia.scm.web.VndMediaType; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Response; +import java.io.IOException; + +public class ModificationsRootResource { + + private final RepositoryServiceFactory serviceFactory; + private final ModificationsToDtoMapper modificationsToDtoMapper; + + @Inject + public ModificationsRootResource(RepositoryServiceFactory serviceFactory, ModificationsToDtoMapper modificationsToDtoMapper) { + this.serviceFactory = serviceFactory; + this.modificationsToDtoMapper = modificationsToDtoMapper; + } + + /** + * Get the file modifications related to a revision. + * file modifications are for example: Modified, Added or Removed. + */ + @GET + @StatusCodes({ + @ResponseCode(code = 200, condition = "success"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user has no privileges to read the modifications"), + @ResponseCode(code = 404, condition = "not found, no changeset with the specified id is available in the repository"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @Produces(VndMediaType.MODIFICATIONS) + @TypeHint(ModificationsDto.class) + @Path("{revision}") + public Response get(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("revision") String revision) throws IOException, RevisionNotFoundException, RepositoryNotFoundException , InternalRepositoryException { + try (RepositoryService repositoryService = serviceFactory.create(new NamespaceAndName(namespace, name))) { + Modifications modifications = repositoryService.getModificationsCommand() + .revision(revision) + .getModifications(); + ModificationsDto output = modificationsToDtoMapper.map(modifications, repositoryService.getRepository()); + if (modifications != null ) { + return Response.ok(output).build(); + } else { + return Response.status(Response.Status.NOT_FOUND).build(); + } + } + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ModificationsToDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ModificationsToDtoMapper.java new file mode 100644 index 0000000000..422d8fc4d9 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/ModificationsToDtoMapper.java @@ -0,0 +1,31 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.Links; +import org.mapstruct.AfterMapping; +import org.mapstruct.Context; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.MappingTarget; +import sonia.scm.repository.Modifications; +import sonia.scm.repository.Repository; + +import javax.inject.Inject; + +import static de.otto.edison.hal.Links.linkingTo; + +@Mapper +public abstract class ModificationsToDtoMapper { + + @Inject + private ResourceLinks resourceLinks; + + @Mapping(target = "attributes", ignore = true) // We do not map HAL attributes + public abstract ModificationsDto map(Modifications modifications, @Context Repository repository); + + @AfterMapping + void appendLinks(@MappingTarget ModificationsDto target, @Context Repository repository) { + Links.Builder linksBuilder = linkingTo() + .self(resourceLinks.modifications().self(repository.getNamespace(), repository.getName(), target.getRevision())); + target.add(linksBuilder.build()); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PagedCollectionToDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PagedCollectionToDtoMapper.java new file mode 100644 index 0000000000..c05ec0b1f3 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PagedCollectionToDtoMapper.java @@ -0,0 +1,64 @@ +package sonia.scm.api.v2.resources; + +import de.otto.edison.hal.Embedded; +import de.otto.edison.hal.HalRepresentation; +import de.otto.edison.hal.Links; +import de.otto.edison.hal.paging.NumberedPaging; +import de.otto.edison.hal.paging.PagingRel; +import sonia.scm.ModelObject; +import sonia.scm.PageResult; + +import java.util.EnumSet; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; + +import static com.damnhandy.uri.template.UriTemplate.fromTemplate; +import static de.otto.edison.hal.Embedded.embeddedBuilder; +import static de.otto.edison.hal.Link.link; +import static de.otto.edison.hal.Links.linkingTo; +import static de.otto.edison.hal.paging.NumberedPaging.zeroBasedNumberedPaging; +import static java.util.stream.Collectors.toList; + +abstract class PagedCollectionToDtoMapper { + + private final String collectionName; + + PagedCollectionToDtoMapper(String collectionName) { + this.collectionName = collectionName; + } + + CollectionDto map(int pageNumber, int pageSize, PageResult pageResult, String selfLink, Optional createLink, Function mapper) { + NumberedPaging paging = zeroBasedNumberedPaging(pageNumber, pageSize, pageResult.getOverallCount()); + List dtos = pageResult.getEntities().stream().map(mapper).collect(toList()); + CollectionDto collectionDto = new CollectionDto( + createLinks(paging, selfLink, createLink), + embedDtos(dtos)); + collectionDto.setPage(pageNumber); + collectionDto.setPageTotal(computePageTotal(pageSize, pageResult)); + return collectionDto; + } + + private int computePageTotal(int pageSize, PageResult pageResult) { + if (pageResult.getOverallCount() % pageSize > 0) { + return pageResult.getOverallCount() / pageSize + 1; + } else { + return pageResult.getOverallCount() / pageSize; + } + } + + private Links createLinks(NumberedPaging page, String selfLink, Optional createLink) { + Links.Builder linksBuilder = linkingTo() + .with(page.links( + fromTemplate(selfLink + "{?page,pageSize}"), + EnumSet.allOf(PagingRel.class))); + createLink.ifPresent(link -> linksBuilder.single(link("create", link))); + return linksBuilder.build(); + } + + private Embedded embedDtos(List dtos) { + return embeddedBuilder() + .with(collectionName, dtos) + .build(); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PasswordChangeDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PasswordChangeDto.java new file mode 100644 index 0000000000..47ad6cd147 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PasswordChangeDto.java @@ -0,0 +1,17 @@ +package sonia.scm.api.v2.resources; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.hibernate.validator.constraints.NotEmpty; + +@Getter +@Setter +@ToString +public class PasswordChangeDto { + + private String oldPassword; + + @NotEmpty + private String newPassword; +} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDto.java index 581b2c24cd..82405a6ac2 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionDto.java @@ -4,15 +4,20 @@ import com.fasterxml.jackson.annotation.JsonInclude; import de.otto.edison.hal.HalRepresentation; import de.otto.edison.hal.Links; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.Setter; import lombok.ToString; -@Getter @Setter @ToString +import javax.validation.constraints.Pattern; + +import static sonia.scm.api.v2.ValidationConstraints.USER_GROUP_PATTERN; + +@Getter @Setter @ToString @NoArgsConstructor public class PermissionDto extends HalRepresentation { public static final String GROUP_PREFIX = "@"; - @JsonInclude(JsonInclude.Include.NON_NULL) + @Pattern(regexp = USER_GROUP_PATTERN) private String name; /** @@ -28,9 +33,6 @@ public class PermissionDto extends HalRepresentation { private boolean groupPermission = false; - public PermissionDto() { - } - public PermissionDto(String permissionName, boolean groupPermission) { name = permissionName; this.groupPermission = groupPermission; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java index 6c45629851..af1ba3bf64 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PermissionRootResource.java @@ -16,6 +16,7 @@ import sonia.scm.repository.RepositoryPermissions; import sonia.scm.web.VndMediaType; import javax.inject.Inject; +import javax.validation.Valid; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.GET; @@ -70,7 +71,7 @@ public class PermissionRootResource { @TypeHint(TypeHint.NO_CONTENT.class) @Consumes(VndMediaType.PERMISSION) @Path("") - public Response create(@PathParam("namespace") String namespace, @PathParam("name") String name, PermissionDto permission) throws AlreadyExistsException, NotFoundException { + public Response create(@PathParam("namespace") String namespace, @PathParam("name") String name,@Valid PermissionDto permission) throws AlreadyExistsException, NotFoundException { log.info("try to add new permission: {}", permission); Repository repository = load(namespace, name); RepositoryPermissions.permissionWrite(repository).check(); @@ -157,13 +158,13 @@ public class PermissionRootResource { public Response update(@PathParam("namespace") String namespace, @PathParam("name") String name, @PathParam("permission-name") String permissionName, - PermissionDto permission) throws NotFoundException, AlreadyExistsException { + @Valid PermissionDto permission) throws NotFoundException, AlreadyExistsException { log.info("try to update the permission with name: {}. the modified permission is: {}", permissionName, permission); Repository repository = load(namespace, name); RepositoryPermissions.permissionWrite(repository).check(); String extractedPermissionName = getPermissionName(permissionName); if (!isPermissionExist(new PermissionDto(extractedPermissionName, isGroupPermission(permissionName)), repository)) { - throw new NotFoundException("the permission " + extractedPermissionName + " does not exist"); + throw new NotFoundException("permission", extractedPermissionName); } permission.setGroupPermission(isGroupPermission(permissionName)); if (!extractedPermissionName.equals(permission.getName())) { @@ -239,8 +240,9 @@ public class PermissionRootResource { * @throws RepositoryNotFoundException if the repository does not exists */ private Repository load(String namespace, String name) throws RepositoryNotFoundException { - return Optional.ofNullable(manager.get(new NamespaceAndName(namespace, name))) - .orElseThrow(() -> new RepositoryNotFoundException(name)); + NamespaceAndName namespaceAndName = new NamespaceAndName(namespace, name); + return Optional.ofNullable(manager.get(namespaceAndName)) + .orElseThrow(() -> new RepositoryNotFoundException(namespaceAndName)); } /** diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionToDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionToDtoMapper.java index 77674270d7..4e52ca29ac 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionToDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryCollectionToDtoMapper.java @@ -10,8 +10,6 @@ import java.util.Optional; import static java.util.Optional.empty; import static java.util.Optional.of; -// Mapstruct does not support parameterized (i.e. non-default) constructors. Thus, we need to use field injection. -@SuppressWarnings("squid:S3306") public class RepositoryCollectionToDtoMapper extends BasicCollectionToDtoMapper { private final ResourceLinks resourceLinks; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java index c597e12d4f..ddfe432d73 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryDto.java @@ -24,7 +24,7 @@ public class RepositoryDto extends HalRepresentation { @JsonInclude(JsonInclude.Include.NON_NULL) private Instant lastModified; private String namespace; - @Pattern(regexp = "(?!^\\.\\.$)(?!^\\.$)(?!.*[\\\\\\[\\]])^[A-z0-9\\.][A-z0-9\\.\\-_/]*$") + @Pattern(regexp = "^[A-z0-9\\-_]+$") private String name; private boolean archived = false; @NotEmpty diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java index c47fc4f3d1..f9328525a9 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryResource.java @@ -40,6 +40,8 @@ public class RepositoryResource { private final Provider contentResource; private final Provider permissionRootResource; private final Provider diffRootResource; + private final Provider modificationsRootResource; + private final Provider fileHistoryRootResource; @Inject public RepositoryResource( @@ -50,7 +52,10 @@ public class RepositoryResource { Provider changesetRootResource, Provider sourceRootResource, Provider contentResource, Provider permissionRootResource, - Provider diffRootResource) { + Provider diffRootResource, + Provider modificationsRootResource, + Provider fileHistoryRootResource + ) { this.dtoToRepositoryMapper = dtoToRepositoryMapper; this.manager = manager; this.repositoryToDtoMapper = repositoryToDtoMapper; @@ -62,6 +67,8 @@ public class RepositoryResource { this.contentResource = contentResource; this.permissionRootResource = permissionRootResource; this.diffRootResource = diffRootResource; + this.modificationsRootResource = modificationsRootResource; + this.fileHistoryRootResource = fileHistoryRootResource; } /** @@ -165,6 +172,11 @@ public class RepositoryResource { return changesetRootResource.get(); } + @Path("history/") + public FileHistoryRootResource history() { + return fileHistoryRootResource.get(); + } + @Path("sources/") public SourceRootResource sources() { return sourceRootResource.get(); @@ -180,6 +192,9 @@ public class RepositoryResource { return permissionRootResource.get(); } + @Path("modifications/") + public ModificationsRootResource modifications() {return modificationsRootResource.get(); } + private Optional handleNotArchived(Throwable throwable) { if (throwable instanceof RepositoryIsNotArchivedException) { return Optional.of(Response.status(Response.Status.PRECONDITION_FAILED).build()); diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java index f76669fbb9..29a4107aad 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapper.java @@ -1,6 +1,7 @@ package sonia.scm.api.v2.resources; import com.google.inject.Inject; +import de.otto.edison.hal.Link; import de.otto.edison.hal.Links; import org.mapstruct.AfterMapping; import org.mapstruct.Mapper; @@ -11,9 +12,13 @@ import sonia.scm.repository.RepositoryPermissions; import sonia.scm.repository.api.Command; import sonia.scm.repository.api.RepositoryService; import sonia.scm.repository.api.RepositoryServiceFactory; +import sonia.scm.repository.api.ScmProtocol; + +import java.util.List; import static de.otto.edison.hal.Link.link; import static de.otto.edison.hal.Links.linkingTo; +import static java.util.stream.Collectors.toList; // Mapstruct does not support parameterized (i.e. non-default) constructors. Thus, we need to use field injection. @SuppressWarnings("squid:S3306") @@ -30,7 +35,6 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper protocolLinks = repositoryService.getSupportedProtocols() + .map(this::createProtocolLink) + .collect(toList()); + linksBuilder.array(protocolLinks); + } if (repositoryService.isSupported(Command.TAGS)) { linksBuilder.single(link("tags", resourceLinks.tag().all(target.getNamespace(), target.getName()))); } @@ -50,4 +60,8 @@ public abstract class RepositoryToRepositoryDtoMapper extends BaseMapper> reader, Function applyChanges, Predicate hasSameKey, Consumer checker) throws NotFoundException, ConcurrentModificationException { + MODEL_OBJECT existingModelObject = reader.get().orElseThrow(NotFoundException::new); + checker.accept(existingModelObject); + return update(reader,applyChanges,hasSameKey); + } /** * Update the model object for the given id according to the given function and returns a corresponding http response. diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagToTagDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagToTagDtoMapper.java index 917b4b7789..ee0488e037 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagToTagDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/TagToTagDtoMapper.java @@ -28,7 +28,7 @@ public abstract class TagToTagDtoMapper { Links.Builder linksBuilder = linkingTo() .self(resourceLinks.tag().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getName())) .single(link("sources", resourceLinks.source().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getRevision()))) - .single(link("changesets", resourceLinks.changeset().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getRevision()))); + .single(link("changeset", resourceLinks.changeset().self(namespaceAndName.getNamespace(), namespaceAndName.getName(), target.getRevision()))); target.add(linksBuilder.build()); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java index 81c3a66c6d..9b39888104 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionResource.java @@ -5,6 +5,7 @@ import com.webcohesion.enunciate.metadata.rs.ResponseHeader; import com.webcohesion.enunciate.metadata.rs.ResponseHeaders; import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; +import org.apache.shiro.authc.credential.PasswordService; import sonia.scm.AlreadyExistsException; import sonia.scm.user.User; import sonia.scm.user.UserManager; @@ -29,14 +30,16 @@ public class UserCollectionResource { private final ResourceLinks resourceLinks; private final IdResourceManagerAdapter adapter; + private final PasswordService passwordService; @Inject public UserCollectionResource(UserManager manager, UserDtoToUserMapper dtoToUserMapper, - UserCollectionToDtoMapper userCollectionToDtoMapper, ResourceLinks resourceLinks) { + UserCollectionToDtoMapper userCollectionToDtoMapper, ResourceLinks resourceLinks, PasswordService passwordService) { this.dtoToUserMapper = dtoToUserMapper; this.userCollectionToDtoMapper = userCollectionToDtoMapper; this.adapter = new IdResourceManagerAdapter<>(manager, User.class); this.resourceLinks = resourceLinks; + this.passwordService = passwordService; } /** @@ -89,8 +92,6 @@ public class UserCollectionResource { @TypeHint(TypeHint.NO_CONTENT.class) @ResponseHeaders(@ResponseHeader(name = "Location", description = "uri to the created user")) public Response create(@Valid UserDto userDto) throws AlreadyExistsException { - return adapter.create(userDto, - () -> dtoToUserMapper.map(userDto, ""), - user -> resourceLinks.user().self(user.getName())); + return adapter.create(userDto, () -> dtoToUserMapper.map(userDto, passwordService.encryptPassword(userDto.getPassword())), user -> resourceLinks.user().self(user.getName())); } } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionToDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionToDtoMapper.java index c70bd268ff..464da8ee23 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionToDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserCollectionToDtoMapper.java @@ -10,8 +10,6 @@ import java.util.Optional; import static java.util.Optional.empty; import static java.util.Optional.of; -// Mapstruct does not support parameterized (i.e. non-default) constructors. Thus, we need to use field injection. -@SuppressWarnings("squid:S3306") public class UserCollectionToDtoMapper extends BasicCollectionToDtoMapper { private final ResourceLinks resourceLinks; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserDto.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserDto.java index 74f43c61c2..9dc5b850bd 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserDto.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserDto.java @@ -13,6 +13,8 @@ import javax.validation.constraints.Pattern; import java.time.Instant; import java.util.Map; +import static sonia.scm.api.v2.ValidationConstraints.USER_GROUP_PATTERN; + @NoArgsConstructor @Getter @Setter public class UserDto extends HalRepresentation { private boolean active; @@ -24,8 +26,9 @@ public class UserDto extends HalRepresentation { private Instant lastModified; @NotEmpty @Email private String mail; - @Pattern(regexp = "^[A-z0-9\\.\\-_@]|[^ ]([A-z0-9\\.\\-_@ ]*[A-z0-9\\.\\-_@]|[^ ])?$") + @Pattern(regexp = USER_GROUP_PATTERN) private String name; + @JsonInclude(JsonInclude.Include.NON_NULL) private String password; private String type; private Map properties; diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserDtoToUserMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserDtoToUserMapper.java index 6d266bea5a..e6608f0804 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserDtoToUserMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserDtoToUserMapper.java @@ -1,37 +1,35 @@ package sonia.scm.api.v2.resources; -import org.apache.shiro.authc.credential.PasswordService; +import org.mapstruct.AfterMapping; import org.mapstruct.Context; import org.mapstruct.Mapper; import org.mapstruct.Mapping; -import org.mapstruct.Named; +import org.mapstruct.MappingTarget; import sonia.scm.user.User; -import javax.inject.Inject; - -import java.time.Instant; - -import static sonia.scm.api.rest.resources.UserResource.DUMMY_PASSWORT; - // Mapstruct does not support parameterized (i.e. non-default) constructors. Thus, we need to use field injection. @SuppressWarnings("squid:S3306") @Mapper public abstract class UserDtoToUserMapper extends BaseDtoMapper { - @Inject - private PasswordService passwordService; - - @Mapping(source = "password", target = "password", qualifiedByName = "encrypt") @Mapping(target = "creationDate", ignore = true) - public abstract User map(UserDto userDto, @Context String originalPassword); + public abstract User map(UserDto userDto, @Context String usedPassword); - @Named("encrypt") - String encrypt(String password, @Context String originalPassword) { - if (DUMMY_PASSWORT.equals(password)) { - return originalPassword; - } else { - return passwordService.encryptPassword(password); - } + /** + * depends on the use case the right password will be mapped. + * The given Password in the context parameter will be set. + * The mapper consumer have the control of what password should be set. + *

+ * eg. for update user action the password will be set to the original password + * for create user and change password actions the password is the user input + * + * @param usedPassword the password to be set + * @param user the target + */ + @AfterMapping + void overridePassword(@MappingTarget User user, @Context String usedPassword) { + user.setPassword(usedPassword); } + } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java index 151abfa20f..c0bb1e65ad 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserResource.java @@ -3,6 +3,7 @@ package sonia.scm.api.v2.resources; import com.webcohesion.enunciate.metadata.rs.ResponseCode; import com.webcohesion.enunciate.metadata.rs.StatusCodes; import com.webcohesion.enunciate.metadata.rs.TypeHint; +import org.apache.shiro.authc.credential.PasswordService; import sonia.scm.ConcurrentModificationException; import sonia.scm.NotFoundException; import sonia.scm.user.User; @@ -26,12 +27,16 @@ public class UserResource { private final UserToUserDtoMapper userToDtoMapper; private final IdResourceManagerAdapter adapter; + private final UserManager userManager; + private final PasswordService passwordService; @Inject - public UserResource(UserDtoToUserMapper dtoToUserMapper, UserToUserDtoMapper userToDtoMapper, UserManager manager) { + public UserResource(UserDtoToUserMapper dtoToUserMapper, UserToUserDtoMapper userToDtoMapper, UserManager manager, PasswordService passwordService) { this.dtoToUserMapper = dtoToUserMapper; this.userToDtoMapper = userToDtoMapper; this.adapter = new IdResourceManagerAdapter<>(manager, User.class); + this.userManager = manager; + this.passwordService = passwordService; } /** @@ -40,7 +45,6 @@ public class UserResource { * Note: This method requires "user" privilege. * * @param id the id/name of the user - * */ @GET @Path("") @@ -63,7 +67,6 @@ public class UserResource { * Note: This method requires "user" privilege. * * @param name the name of the user to delete. - * */ @DELETE @Path("") @@ -80,10 +83,11 @@ public class UserResource { /** * Modifies the given user. + * The given Password in the payload will be ignored. To Change Password use the changePassword endpoint * * Note: This method requires "user" privilege. * - * @param name name of the user to be modified + * @param name name of the user to be modified * @param userDto user object to modify */ @PUT @@ -101,4 +105,30 @@ public class UserResource { public Response update(@PathParam("id") String name, @Valid UserDto userDto) throws NotFoundException, ConcurrentModificationException { return adapter.update(name, existing -> dtoToUserMapper.map(userDto, existing.getPassword())); } + + /** + * This Endpoint is for Admin user to modify a user password. + * The oldPassword property of the DTO is not needed here. it will be ignored. + * The oldPassword property is needed in the MeResources when the actual user change the own password. + * + * Note: This method requires "user:modify" privilege. + * @param name name of the user to be modified + * @param passwordChangeDto change password object to modify password. the old password is here not required + */ + @PUT + @Path("password") + @Consumes(VndMediaType.PASSWORD_CHANGE) + @StatusCodes({ + @ResponseCode(code = 204, condition = "update success"), + @ResponseCode(code = 400, condition = "Invalid body, e.g. the user type is not xml or the given oldPassword do not match the stored one"), + @ResponseCode(code = 401, condition = "not authenticated / invalid credentials"), + @ResponseCode(code = 403, condition = "not authorized, the current user does not have the \"user\" privilege"), + @ResponseCode(code = 404, condition = "not found, no user with the specified id/name available"), + @ResponseCode(code = 500, condition = "internal server error") + }) + @TypeHint(TypeHint.NO_CONTENT.class) + public Response changePassword(@PathParam("id") String name, @Valid PasswordChangeDto passwordChangeDto) throws NotFoundException, ConcurrentModificationException { + return adapter.update(name, user -> user.changePassword(passwordService.encryptPassword(passwordChangeDto.getNewPassword())), userManager.getUserTypeChecker()); + } + } diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserToUserDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserToUserDtoMapper.java index 00aba5a700..832829883b 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserToUserDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/UserToUserDtoMapper.java @@ -4,9 +4,10 @@ import com.google.common.annotations.VisibleForTesting; import de.otto.edison.hal.Links; import org.mapstruct.AfterMapping; import org.mapstruct.Mapper; +import org.mapstruct.Mapping; import org.mapstruct.MappingTarget; -import sonia.scm.api.rest.resources.UserResource; import sonia.scm.user.User; +import sonia.scm.user.UserManager; import sonia.scm.user.UserPermissions; import javax.inject.Inject; @@ -19,21 +20,19 @@ import static de.otto.edison.hal.Links.linkingTo; @Mapper public abstract class UserToUserDtoMapper extends BaseMapper { + @Inject + private UserManager userManager; + + @Override + @Mapping(target = "attributes", ignore = true) + @Mapping(target = "password", ignore = true) + public abstract UserDto map(User modelObject); + @Inject private ResourceLinks resourceLinks; - @VisibleForTesting - void setResourceLinks(ResourceLinks resourceLinks) { - this.resourceLinks = resourceLinks; - } - @AfterMapping - void removePassword(@MappingTarget UserDto target) { - target.setPassword(UserResource.DUMMY_PASSWORT); - } - - @AfterMapping - void appendLinks(User user, @MappingTarget UserDto target) { + protected void appendLinks(User user, @MappingTarget UserDto target) { Links.Builder linksBuilder = linkingTo().self(resourceLinks.user().self(target.getName())); if (UserPermissions.delete(user).isPermitted()) { linksBuilder.single(link("delete", resourceLinks.user().delete(target.getName()))); @@ -41,6 +40,9 @@ public abstract class UserToUserDtoMapper extends BaseMapper { if (UserPermissions.modify(user).isPermitted()) { linksBuilder.single(link("update", resourceLinks.user().update(target.getName()))); } + if (userManager.isTypeDefault(user)) { + linksBuilder.single(link("password", resourceLinks.user().passwordChange(target.getName()))); + } target.add(linksBuilder.build()); } diff --git a/scm-webapp/src/main/java/sonia/scm/filter/SecurityFilter.java b/scm-webapp/src/main/java/sonia/scm/filter/SecurityFilter.java index 0d59d77027..d97a5b050e 100644 --- a/scm-webapp/src/main/java/sonia/scm/filter/SecurityFilter.java +++ b/scm-webapp/src/main/java/sonia/scm/filter/SecurityFilter.java @@ -37,10 +37,8 @@ package sonia.scm.filter; import com.google.common.annotations.VisibleForTesting; import com.google.inject.Inject; - import org.apache.shiro.SecurityUtils; import org.apache.shiro.subject.Subject; - import sonia.scm.Priority; import sonia.scm.SCMContext; import sonia.scm.config.ScmConfiguration; @@ -48,14 +46,15 @@ import sonia.scm.security.SecurityRequests; import sonia.scm.web.filter.HttpFilter; import sonia.scm.web.filter.SecurityHttpServletRequestWrapper; -//~--- JDK imports ------------------------------------------------------------ - -import java.io.IOException; - import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +import static sonia.scm.api.v2.resources.ScmPathInfo.REST_API_PATH; + +//~--- JDK imports ------------------------------------------------------------ /** * @@ -63,7 +62,8 @@ import javax.servlet.http.HttpServletResponse; */ @Priority(Filters.PRIORITY_AUTHORIZATION) // TODO find a better way for unprotected resources -@WebElement(value = "/api/rest/(?!v2/ui).*", regex = true) +@WebElement(value = REST_API_PATH + "" + + "/(?!v2/ui).*", regex = true) public class SecurityFilter extends HttpFilter { @@ -84,7 +84,7 @@ public class SecurityFilter extends HttpFilter HttpServletResponse response, FilterChain chain) throws IOException, ServletException { - if (!SecurityRequests.isAuthenticationRequest(request)) + if (!SecurityRequests.isAuthenticationRequest(request) && !SecurityRequests.isIndexRequest(request)) { Subject subject = SecurityUtils.getSubject(); if (hasPermission(subject)) diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/PathWebResourceLoader.java b/scm-webapp/src/main/java/sonia/scm/plugin/PathWebResourceLoader.java index 230cfa6c7a..4b9e502c4d 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/PathWebResourceLoader.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/PathWebResourceLoader.java @@ -31,18 +31,11 @@ package sonia.scm.plugin; -//~--- non-JDK imports -------------------------------------------------------- - import org.slf4j.Logger; import org.slf4j.LoggerFactory; -//~--- JDK imports ------------------------------------------------------------ - -import java.io.File; - import java.net.MalformedURLException; import java.net.URL; - import java.nio.file.Files; import java.nio.file.Path; @@ -55,47 +48,27 @@ import java.nio.file.Path; public class PathWebResourceLoader implements WebResourceLoader { - /** Field description */ - private static final String DEFAULT_SEPARATOR = "/"; + private static final String SEPARATOR = "/"; /** * the logger for PathWebResourceLoader */ - private static final Logger logger = + private static final Logger LOG = LoggerFactory.getLogger(PathWebResourceLoader.class); - //~--- constructors --------------------------------------------------------- - - /** - * Constructs ... - * - * - * @param directory - */ public PathWebResourceLoader(Path directory) { this.directory = directory; } - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @param path - * - * @return - */ @Override - public URL getResource(String path) - { + public URL getResource(String path) { URL resource = null; Path file = directory.resolve(filePath(path)); if (Files.exists(file) && ! Files.isDirectory(file)) { - logger.trace("found path {} at {}", path, file); + LOG.trace("found path {} at {}", path, file); try { @@ -103,56 +76,20 @@ public class PathWebResourceLoader implements WebResourceLoader } catch (MalformedURLException ex) { - logger.error("could not transform path to url", ex); + LOG.error("could not transform path to url", ex); } + } else { + LOG.trace("could not find file {}", file); } return resource; } - //~--- methods -------------------------------------------------------------- - - /** - * Method description - * - * - * @param path - * - * @return - */ - private String filePath(String path) - { - - // TODO handle illegal path parts, such as .. - String filePath = filePath(DEFAULT_SEPARATOR, path); - - if (!DEFAULT_SEPARATOR.equals(File.separator)) - { - filePath = filePath(File.separator, path); + private String filePath(String path) { + if (path.startsWith(SEPARATOR)) { + return path.substring(1); } - - return filePath; - } - - /** - * Method description - * - * - * @param separator - * @param path - * - * @return - */ - private String filePath(String separator, String path) - { - String filePath = path; - - if (filePath.startsWith(separator)) - { - filePath = filePath.substring(separator.length()); - } - - return filePath; + return path; } //~--- fields --------------------------------------------------------------- diff --git a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java index 02ae67719b..8cb325b818 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryManager.java @@ -31,10 +31,7 @@ package sonia.scm.repository; -//~--- non-JDK imports -------------------------------------------------------- - import com.github.sdorra.ssp.PermissionActionCheck; -import com.google.common.base.Strings; import com.google.common.collect.Lists; import com.google.common.util.concurrent.ThreadFactoryBuilder; import com.google.inject.Inject; @@ -43,7 +40,6 @@ import org.apache.shiro.concurrent.SubjectAwareExecutorService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.AlreadyExistsException; -import sonia.scm.ArgumentIsInvalidException; import sonia.scm.ConfigurationException; import sonia.scm.HandlerEventType; import sonia.scm.ManagerDaoAdapter; @@ -54,11 +50,9 @@ import sonia.scm.config.ScmConfiguration; import sonia.scm.security.KeyGenerator; import sonia.scm.util.AssertUtil; import sonia.scm.util.CollectionAppender; -import sonia.scm.util.HttpUtil; import sonia.scm.util.IOUtil; import sonia.scm.util.Util; -import javax.servlet.http.HttpServletRequest; import java.util.Collection; import java.util.Collections; import java.util.Comparator; @@ -71,8 +65,6 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; -//~--- JDK imports ------------------------------------------------------------ - /** * Default implementation of {@link RepositoryManager}. * @@ -90,7 +82,6 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager { private final KeyGenerator keyGenerator; private final RepositoryDAO repositoryDAO; private final Set types; - private RepositoryMatcher repositoryMatcher; private NamespaceStrategy namespaceStrategy; private final ManagerDaoAdapter managerDaoAdapter; @@ -99,12 +90,10 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager { public DefaultRepositoryManager(ScmConfiguration configuration, SCMContextProvider contextProvider, KeyGenerator keyGenerator, RepositoryDAO repositoryDAO, Set handlerSet, - RepositoryMatcher repositoryMatcher, NamespaceStrategy namespaceStrategy) { this.configuration = configuration; this.keyGenerator = keyGenerator; this.repositoryDAO = repositoryDAO; - this.repositoryMatcher = repositoryMatcher; this.namespaceStrategy = namespaceStrategy; ThreadFactory factory = new ThreadFactoryBuilder() @@ -317,71 +306,6 @@ public class DefaultRepositoryManager extends AbstractRepositoryManager { return validTypes; } - @Override - public Repository getFromRequest(HttpServletRequest request) { - AssertUtil.assertIsNotNull(request); - - return getFromUri(HttpUtil.getStrippedURI(request)); - } - - @Override - public Repository getFromUri(String uri) { - AssertUtil.assertIsNotEmpty(uri); - - if (uri.startsWith(HttpUtil.SEPARATOR_PATH)) { - uri = uri.substring(1); - } - - int typeSeparator = uri.indexOf(HttpUtil.SEPARATOR_PATH); - Repository repository = null; - - if (typeSeparator > 0) { - String type = uri.substring(0, typeSeparator); - - uri = uri.substring(typeSeparator + 1); - repository = getFromTypeAndUri(type, uri); - } - - return repository; - } - - private Repository getFromTypeAndUri(String type, String uri) { - if (Strings.isNullOrEmpty(type)) { - throw new ArgumentIsInvalidException("argument type is required"); - } - - if (Strings.isNullOrEmpty(uri)) { - throw new ArgumentIsInvalidException("argument uri is required"); - } - - // remove ;jsessionid, jetty bug? - uri = HttpUtil.removeMatrixParameter(uri); - - Repository repository = null; - - if (handlerMap.containsKey(type)) { - Collection repositories = repositoryDAO.getAll(); - - PermissionActionCheck check = RepositoryPermissions.read(); - - for (Repository r : repositories) { - if (repositoryMatcher.matches(r, type, uri)) { - check.check(r); - repository = r.clone(); - - break; - } - } - } - - if ((repository == null) && logger.isDebugEnabled()) { - logger.debug("could not find repository with type {} and uri {}", type, - uri); - } - - return repository; - } - @Override public RepositoryHandler getHandler(String type) { return handlerMap.get(type); diff --git a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryProvider.java b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryProvider.java index c3341bbd22..62067db172 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryProvider.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/DefaultRepositoryProvider.java @@ -33,86 +33,32 @@ package sonia.scm.repository; -//~--- non-JDK imports -------------------------------------------------------- - import com.google.inject.Inject; import com.google.inject.Provider; import com.google.inject.servlet.RequestScoped; -import sonia.scm.security.ScmSecurityException; - -//~--- JDK imports ------------------------------------------------------------ - import javax.servlet.http.HttpServletRequest; -/** - * - * @author Sebastian Sdorra - */ @RequestScoped -public class DefaultRepositoryProvider implements RepositoryProvider -{ +public class DefaultRepositoryProvider implements RepositoryProvider { - /** Field description */ public static final String ATTRIBUTE_NAME = "scm.request.repository"; - //~--- constructors --------------------------------------------------------- + private final Provider requestProvider; - /** - * Constructs ... - * - * - * @param requestProvider - * @param manager - */ @Inject - public DefaultRepositoryProvider( - Provider requestProvider, - RepositoryManager manager) - { + public DefaultRepositoryProvider(Provider requestProvider) { this.requestProvider = requestProvider; - this.manager = manager; } - //~--- get methods ---------------------------------------------------------- - - /** - * Method description - * - * - * @return - * - * @throws ScmSecurityException - */ @Override - public Repository get() throws ScmSecurityException - { - Repository repository = null; + public Repository get() { HttpServletRequest request = requestProvider.get(); - if (request != null) - { - repository = (Repository) request.getAttribute(ATTRIBUTE_NAME); - - if (repository == null) - { - repository = manager.getFromRequest(request); - - if (repository != null) - { - request.setAttribute(ATTRIBUTE_NAME, repository); - } - } + if (request != null) { + return (Repository) request.getAttribute(ATTRIBUTE_NAME); } - return repository; + throw new IllegalStateException("request not found"); } - - //~--- fields --------------------------------------------------------------- - - /** Field description */ - private final RepositoryManager manager; - - /** Field description */ - private final Provider requestProvider; } diff --git a/scm-webapp/src/main/java/sonia/scm/repository/HealthChecker.java b/scm-webapp/src/main/java/sonia/scm/repository/HealthChecker.java index 0d27c6d250..52dea4223b 100644 --- a/scm-webapp/src/main/java/sonia/scm/repository/HealthChecker.java +++ b/scm-webapp/src/main/java/sonia/scm/repository/HealthChecker.java @@ -61,8 +61,7 @@ public final class HealthChecker { Repository repository = repositoryManager.get(id); if (repository == null) { - throw new RepositoryNotFoundException( - "could not find repository with id ".concat(id)); + throw new RepositoryNotFoundException(id); } doCheck(repository); diff --git a/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java b/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java index 1b9abb058b..bf46eb2a6f 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java +++ b/scm-webapp/src/main/java/sonia/scm/security/DefaultAuthorizationCollector.java @@ -56,6 +56,7 @@ import sonia.scm.plugin.Extension; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryDAO; import sonia.scm.user.User; +import sonia.scm.user.UserPermissions; import sonia.scm.util.Util; import java.util.List; @@ -74,7 +75,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector // TODO move to util class private static final String SEPARATOR = System.getProperty("line.separator", "\n"); - + /** Field description */ private static final String ADMIN_PERMISSION = "*"; @@ -88,7 +89,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector LoggerFactory.getLogger(DefaultAuthorizationCollector.class); //~--- constructors --------------------------------------------------------- - + /** * Constructs ... * @@ -209,7 +210,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector String perm = permission.getType().getPermissionPrefix().concat(repository.getId()); if (logger.isTraceEnabled()) { - logger.trace("add repository permission {} for user {} at repository {}", + logger.trace("add repository permission {} for user {} at repository {}", perm, user.getName(), repository.getName()); } @@ -254,6 +255,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector collectGlobalPermissions(builder, user, groups); collectRepositoryPermissions(builder, user, groups); + builder.add(canReadOwnUser(user)); permissions = builder.build(); } @@ -262,6 +264,10 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector return info; } + private String canReadOwnUser(User user) { + return UserPermissions.read(user.getName()).asShiroString(); + } + //~--- get methods ---------------------------------------------------------- private boolean isUserPermitted(User user, GroupNames groups, @@ -272,7 +278,7 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector || ((!perm.isGroupPermission()) && user.getName().equals(perm.getName())); //J+ } - + @Subscribe public void invalidateCache(AuthorizationChangedEvent event) { if (event.isEveryUserAffected()) { @@ -281,12 +287,12 @@ public class DefaultAuthorizationCollector implements AuthorizationCollector invalidateCache(); } } - + private void invalidateUserCache(final String username) { logger.info("invalidate cache for user {}, because of a received authorization event", username); cache.removeAll((CacheKey item) -> username.equalsIgnoreCase(item.username)); } - + private void invalidateCache() { logger.info("invalidate cache, because of a received authorization event"); cache.clear(); diff --git a/scm-webapp/src/main/java/sonia/scm/security/SecurityRequests.java b/scm-webapp/src/main/java/sonia/scm/security/SecurityRequests.java index 225767cd3b..49d03f598b 100644 --- a/scm-webapp/src/main/java/sonia/scm/security/SecurityRequests.java +++ b/scm-webapp/src/main/java/sonia/scm/security/SecurityRequests.java @@ -3,12 +3,15 @@ package sonia.scm.security; import javax.servlet.http.HttpServletRequest; import java.util.regex.Pattern; +import static sonia.scm.api.v2.resources.ScmPathInfo.REST_API_PATH; + /** * Created by masuewer on 04.07.18. */ public final class SecurityRequests { - private static final Pattern URI_LOGIN_PATTERN = Pattern.compile("/api/rest(?:/v2)?/auth/access_token"); + private static final Pattern URI_LOGIN_PATTERN = Pattern.compile(REST_API_PATH + "(?:/v2)?/auth/access_token"); + private static final Pattern URI_INDEX_PATTERN = Pattern.compile(REST_API_PATH + "/v2/?"); private SecurityRequests() {} @@ -21,4 +24,13 @@ public final class SecurityRequests { return URI_LOGIN_PATTERN.matcher(uri).matches(); } + public static boolean isIndexRequest(HttpServletRequest request) { + String uri = request.getRequestURI().substring(request.getContextPath().length()); + return isIndexRequest(uri); + } + + public static boolean isIndexRequest(String uri) { + return URI_INDEX_PATTERN.matcher(uri).matches(); + } + } diff --git a/scm-webapp/src/main/java/sonia/scm/web/protocol/HttpProtocolServlet.java b/scm-webapp/src/main/java/sonia/scm/web/protocol/HttpProtocolServlet.java new file mode 100644 index 0000000000..bc085d2cc7 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/web/protocol/HttpProtocolServlet.java @@ -0,0 +1,79 @@ +package sonia.scm.web.protocol; + +import com.google.inject.Inject; +import com.google.inject.Singleton; +import lombok.extern.slf4j.Slf4j; +import org.apache.http.HttpStatus; +import sonia.scm.PushStateDispatcher; +import sonia.scm.filter.WebElement; +import sonia.scm.repository.DefaultRepositoryProvider; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.RepositoryNotFoundException; +import sonia.scm.repository.api.RepositoryService; +import sonia.scm.repository.api.RepositoryServiceFactory; +import sonia.scm.repository.spi.HttpScmProtocol; +import sonia.scm.web.UserAgent; +import sonia.scm.web.UserAgentParser; + +import javax.inject.Provider; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Optional; + +@Singleton +@WebElement(value = HttpProtocolServlet.PATTERN) +@Slf4j +public class HttpProtocolServlet extends HttpServlet { + + public static final String PATH = "/repo"; + public static final String PATTERN = PATH + "/*"; + + private final RepositoryServiceFactory serviceFactory; + + private final Provider requestProvider; + + private final PushStateDispatcher dispatcher; + private final UserAgentParser userAgentParser; + + + @Inject + public HttpProtocolServlet(RepositoryServiceFactory serviceFactory, Provider requestProvider, PushStateDispatcher dispatcher, UserAgentParser userAgentParser) { + this.serviceFactory = serviceFactory; + this.requestProvider = requestProvider; + this.dispatcher = dispatcher; + this.userAgentParser = userAgentParser; + } + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { + UserAgent userAgent = userAgentParser.parse(request); + if (userAgent.isBrowser()) { + log.trace("dispatch browser request for user agent {}", userAgent); + dispatcher.dispatch(request, response, request.getRequestURI()); + } else { + + String pathInfo = request.getPathInfo(); + Optional namespaceAndName = NamespaceAndNameFromPathExtractor.fromUri(pathInfo); + if (namespaceAndName.isPresent()) { + service(request, response, namespaceAndName.get()); + } else { + log.debug("namespace and name not found in request path {}", pathInfo); + response.setStatus(HttpStatus.SC_BAD_REQUEST); + } + } + } + + private void service(HttpServletRequest req, HttpServletResponse resp, NamespaceAndName namespaceAndName) throws IOException, ServletException { + try (RepositoryService repositoryService = serviceFactory.create(namespaceAndName)) { + requestProvider.get().setAttribute(DefaultRepositoryProvider.ATTRIBUTE_NAME, repositoryService.getRepository()); + HttpScmProtocol protocol = repositoryService.getProtocol(HttpScmProtocol.class); + protocol.serve(req, resp, getServletConfig()); + } catch (RepositoryNotFoundException e) { + log.debug("Repository not found for namespace and name {}", namespaceAndName, e); + resp.setStatus(HttpStatus.SC_NOT_FOUND); + } + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/web/protocol/NamespaceAndNameFromPathExtractor.java b/scm-webapp/src/main/java/sonia/scm/web/protocol/NamespaceAndNameFromPathExtractor.java new file mode 100644 index 0000000000..22e2433561 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/web/protocol/NamespaceAndNameFromPathExtractor.java @@ -0,0 +1,41 @@ +package sonia.scm.web.protocol; + +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.util.HttpUtil; + +import java.util.Optional; + +import static java.util.Optional.empty; +import static java.util.Optional.of; + +final class NamespaceAndNameFromPathExtractor { + + private NamespaceAndNameFromPathExtractor() {} + + static Optional fromUri(String uri) { + if (uri.startsWith(HttpUtil.SEPARATOR_PATH)) { + uri = uri.substring(1); + } + + int endOfNamespace = uri.indexOf(HttpUtil.SEPARATOR_PATH); + if (endOfNamespace < 1) { + return empty(); + } + + String namespace = uri.substring(0, endOfNamespace); + int nameSeparatorIndex = uri.indexOf(HttpUtil.SEPARATOR_PATH, endOfNamespace + 1); + int nameIndex = nameSeparatorIndex > 0 ? nameSeparatorIndex : uri.length(); + if (nameIndex == endOfNamespace + 1) { + return empty(); + } + + String name = uri.substring(endOfNamespace + 1, nameIndex); + + int nameDotIndex = name.indexOf('.'); + if (nameDotIndex >= 0) { + return of(new NamespaceAndName(namespace, name.substring(0, nameDotIndex))); + } else { + return of(new NamespaceAndName(namespace, name)); + } + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/web/security/ApiAuthenticationFilter.java b/scm-webapp/src/main/java/sonia/scm/web/security/ApiAuthenticationFilter.java index d8fe469af9..c2444b43f5 100644 --- a/scm-webapp/src/main/java/sonia/scm/web/security/ApiAuthenticationFilter.java +++ b/scm-webapp/src/main/java/sonia/scm/web/security/ApiAuthenticationFilter.java @@ -99,7 +99,7 @@ public class ApiAuthenticationFilter extends AuthenticationFilter throws IOException, ServletException { // skip filter on login resource - if (SecurityRequests.isAuthenticationRequest(request)) + if (SecurityRequests.isAuthenticationRequest(request) ) { chain.doFilter(request, response); } diff --git a/scm-webapp/src/main/webapp/index.html b/scm-webapp/src/main/webapp/index.html deleted file mode 100644 index e149a39434..0000000000 --- a/scm-webapp/src/main/webapp/index.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - Title - - - - - diff --git a/scm-webapp/src/test/java/sonia/scm/ForwardingPushStateDispatcherTest.java b/scm-webapp/src/test/java/sonia/scm/ForwardingPushStateDispatcherTest.java deleted file mode 100644 index e96464ee98..0000000000 --- a/scm-webapp/src/test/java/sonia/scm/ForwardingPushStateDispatcherTest.java +++ /dev/null @@ -1,51 +0,0 @@ -package sonia.scm; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; - -import javax.servlet.RequestDispatcher; -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import java.io.IOException; - -import static org.junit.Assert.*; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -@RunWith(MockitoJUnitRunner.class) -public class ForwardingPushStateDispatcherTest { - - @Mock - private HttpServletRequest request; - - @Mock - private RequestDispatcher requestDispatcher; - - @Mock - private HttpServletResponse response; - - private ForwardingPushStateDispatcher dispatcher = new ForwardingPushStateDispatcher(); - - @Test - public void testDispatch() throws ServletException, IOException { - when(request.getRequestDispatcher("/index.html")).thenReturn(requestDispatcher); - - dispatcher.dispatch(request, response, "/something"); - - verify(requestDispatcher).forward(request, response); - } - - @Test(expected = IOException.class) - public void testWrapServletException() throws ServletException, IOException { - when(request.getRequestDispatcher("/index.html")).thenReturn(requestDispatcher); - doThrow(ServletException.class).when(requestDispatcher).forward(request, response); - - dispatcher.dispatch(request, response, "/something"); - } - -} diff --git a/scm-webapp/src/test/java/sonia/scm/PushStateDispatcherProviderTest.java b/scm-webapp/src/test/java/sonia/scm/PushStateDispatcherProviderTest.java index 31e5f7c6dc..4316d9bc06 100644 --- a/scm-webapp/src/test/java/sonia/scm/PushStateDispatcherProviderTest.java +++ b/scm-webapp/src/test/java/sonia/scm/PushStateDispatcherProviderTest.java @@ -1,14 +1,23 @@ package sonia.scm; +import com.google.inject.util.Providers; import org.assertj.core.api.Assertions; import org.junit.After; import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.template.TemplateEngine; -import static org.junit.Assert.*; - +@RunWith(MockitoJUnitRunner.class) public class PushStateDispatcherProviderTest { - private PushStateDispatcherProvider provider = new PushStateDispatcherProvider(); + @Mock + private TemplateEngine templateEngine; + + private PushStateDispatcherProvider provider = new PushStateDispatcherProvider( + Providers.of(new TemplatingPushStateDispatcher(templateEngine)) + ); @Test public void testGetProxyPushStateWithPropertySet() { @@ -20,7 +29,7 @@ public class PushStateDispatcherProviderTest { @Test public void testGetProxyPushStateWithoutProperty() { PushStateDispatcher dispatcher = provider.get(); - Assertions.assertThat(dispatcher).isInstanceOf(ForwardingPushStateDispatcher.class); + Assertions.assertThat(dispatcher).isInstanceOf(TemplatingPushStateDispatcher.class); } @After diff --git a/scm-webapp/src/test/java/sonia/scm/TemplatingPushStateDispatcherTest.java b/scm-webapp/src/test/java/sonia/scm/TemplatingPushStateDispatcherTest.java new file mode 100644 index 0000000000..126ba9ac0f --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/TemplatingPushStateDispatcherTest.java @@ -0,0 +1,66 @@ +package sonia.scm; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.template.Template; +import sonia.scm.template.TemplateEngine; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.Writer; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class TemplatingPushStateDispatcherTest { + + @Mock + private HttpServletRequest request; + + @Mock + private HttpServletResponse response; + + @Mock + private TemplateEngine templateEngine; + + @Mock + private Template template; + + private TemplatingPushStateDispatcher dispatcher; + + @Before + public void setUpMocks() { + dispatcher = new TemplatingPushStateDispatcher(templateEngine); + } + + @Test + public void testDispatch() throws IOException { + when(request.getContextPath()).thenReturn("/scm"); + when(templateEngine.getTemplate(TemplatingPushStateDispatcher.TEMPLATE)).thenReturn(template); + + when(response.getWriter()).thenReturn(new PrintWriter(new StringWriter())); + + dispatcher.dispatch(request, response, "/someurl"); + + verify(response).setContentType("text/html"); + verify(response).setCharacterEncoding("UTF-8"); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Object.class); + + verify(template).execute(any(Writer.class), captor.capture()); + + TemplatingPushStateDispatcher.IndexHtmlModel model = (TemplatingPushStateDispatcher.IndexHtmlModel) captor.getValue(); + assertEquals("/scm", model.getContextPath()); + } + +} diff --git a/scm-webapp/src/test/java/sonia/scm/WebResourceServletTest.java b/scm-webapp/src/test/java/sonia/scm/WebResourceServletTest.java index fa39239d5d..7da9c7b263 100644 --- a/scm-webapp/src/test/java/sonia/scm/WebResourceServletTest.java +++ b/scm-webapp/src/test/java/sonia/scm/WebResourceServletTest.java @@ -19,10 +19,17 @@ import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; -import static org.junit.Assert.*; -import static org.mockito.Mockito.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; -@RunWith(MockitoJUnitRunner.class) +@RunWith(MockitoJUnitRunner.Silent.class) public class WebResourceServletTest { @Rule diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/ValidationConstraints_IllegalCharactersTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/ValidationConstraints_IllegalCharactersTest.java new file mode 100644 index 0000000000..c56f6195fe --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/ValidationConstraints_IllegalCharactersTest.java @@ -0,0 +1,47 @@ +package sonia.scm.api.v2; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.Collection; +import java.util.List; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import static java.util.Arrays.asList; +import static org.junit.Assert.assertFalse; +import static sonia.scm.api.v2.ValidationConstraints.USER_GROUP_PATTERN; + +@RunWith(Parameterized.class) +public class ValidationConstraints_IllegalCharactersTest { + + private static final List ACCEPTED_CHARS = asList('@', '_', '-', '.'); + + private final Pattern userGroupPattern=Pattern.compile(USER_GROUP_PATTERN); + + private final String expression; + + public ValidationConstraints_IllegalCharactersTest(String expression) { + this.expression = expression; + } + + @Parameterized.Parameters(name = "{0}") + public static Collection createParameters() { + return Stream.concat(IntStream.range(0x20, 0x2f).mapToObj(i -> (char) i), // chars before '0' + Stream.concat(IntStream.range(0x3a, 0x40).mapToObj(i -> (char) i), // chars between '9' and 'A' + Stream.concat(IntStream.range(0x5b, 0x60).mapToObj(i -> (char) i), // chars between 'Z' and 'a' + IntStream.range(0x7b, 0xff).mapToObj(i -> (char) i)))) // chars after 'z' + .filter(c -> !ACCEPTED_CHARS.contains(c)) + .flatMap(c -> Stream.of("abc" + c + "xyz", "@" + c, c + "tail")) + .map(c -> new String[] {c}) + .collect(Collectors.toList()); + } + + @Test + public void shouldNotAcceptSpecialCharacters() { + assertFalse(userGroupPattern.matcher(expression).matches()); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java index e23d3dc39b..4994c11b08 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/BranchRootResourceTest.java @@ -1,5 +1,6 @@ package sonia.scm.api.v2.resources; +import com.google.inject.util.Providers; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.subject.Subject; import org.apache.shiro.subject.support.SubjectThreadState; @@ -44,7 +45,7 @@ import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.Silent.class) @Slf4j -public class BranchRootResourceTest { +public class BranchRootResourceTest extends RepositoryTestBase { public static final String BRANCH_PATH = "space/repo/branches/master"; public static final String BRANCH_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + BRANCH_PATH; @@ -66,7 +67,7 @@ public class BranchRootResourceTest { @InjectMocks private BranchToBranchDtoMapperImpl branchToDtoMapper; - private ChangesetCollectionToDtoMapper changesetCollectionToDtoMapper; + private BranchChangesetCollectionToDtoMapper changesetCollectionToDtoMapper; private BranchRootResource branchRootResource; @@ -89,12 +90,11 @@ public class BranchRootResourceTest { @Before public void prepareEnvironment() throws Exception { - changesetCollectionToDtoMapper = new ChangesetCollectionToDtoMapper(changesetToChangesetDtoMapper, resourceLinks); + changesetCollectionToDtoMapper = new BranchChangesetCollectionToDtoMapper(changesetToChangesetDtoMapper, resourceLinks); BranchCollectionToDtoMapper branchCollectionToDtoMapper = new BranchCollectionToDtoMapper(branchToDtoMapper, resourceLinks); branchRootResource = new BranchRootResource(serviceFactory, branchToDtoMapper, branchCollectionToDtoMapper, changesetCollectionToDtoMapper); - RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider.of(new RepositoryResource(null, null, null, null, MockProvider.of(branchRootResource), null, null, null, null, null)), null); - dispatcher.getRegistry().addSingletonResource(repositoryRootResource); - + super.branchRootResource = Providers.of(branchRootResource); + dispatcher.getRegistry().addSingletonResource(getRepositoryRootResource()); when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(service); when(serviceFactory.create(any(Repository.class))).thenReturn(service); when(service.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo")); @@ -152,6 +152,10 @@ public class BranchRootResourceTest { when(logCommandBuilder.setPagingLimit(anyInt())).thenReturn(logCommandBuilder); when(logCommandBuilder.setBranch(anyString())).thenReturn(logCommandBuilder); when(logCommandBuilder.getChangesets()).thenReturn(changesetPagingResult); + Branches branches = mock(Branches.class); + List branchList = Lists.newArrayList(new Branch("master",id)); + when(branches.getBranches()).thenReturn(branchList); + when(branchesCommandBuilder.getBranches()).thenReturn(branches); MockHttpRequest request = MockHttpRequest.get(BRANCH_URL + "/changesets/"); MockHttpResponse response = new MockHttpResponse(); dispatcher.invoke(request, response); @@ -161,6 +165,5 @@ public class BranchRootResourceTest { assertTrue(response.getContentAsString().contains(String.format("\"name\":\"%s\"", authorName))); assertTrue(response.getContentAsString().contains(String.format("\"mail\":\"%s\"", authorEmail))); assertTrue(response.getContentAsString().contains(String.format("\"description\":\"%s\"", commit))); - assertTrue(response.getContentAsString().contains(String.format("\"description\":\"%s\"", commit))); } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ChangesetRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ChangesetRootResourceTest.java index 40aa61852a..b80b62167b 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ChangesetRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ChangesetRootResourceTest.java @@ -1,6 +1,7 @@ package sonia.scm.api.v2.resources; +import com.google.inject.util.Providers; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.subject.Subject; import org.apache.shiro.subject.support.SubjectThreadState; @@ -44,7 +45,7 @@ import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.Silent.class) @Slf4j -public class ChangesetRootResourceTest { +public class ChangesetRootResourceTest extends RepositoryTestBase { public static final String CHANGESET_PATH = "space/repo/changesets/"; @@ -79,10 +80,8 @@ public class ChangesetRootResourceTest { public void prepareEnvironment() throws Exception { changesetCollectionToDtoMapper = new ChangesetCollectionToDtoMapper(changesetToChangesetDtoMapper, resourceLinks); changesetRootResource = new ChangesetRootResource(serviceFactory, changesetCollectionToDtoMapper, changesetToChangesetDtoMapper); - RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider - .of(new RepositoryResource(null, null, null, null, null, - MockProvider.of(changesetRootResource), null, null, null, null)), null); - dispatcher.getRegistry().addSingletonResource(repositoryRootResource); + super.changesetRootResource = Providers.of(changesetRootResource); + dispatcher.getRegistry().addSingletonResource(getRepositoryRootResource()); when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(repositoryService); when(serviceFactory.create(any(Repository.class))).thenReturn(repositoryService); when(repositoryService.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo")); @@ -125,7 +124,6 @@ public class ChangesetRootResourceTest { assertTrue(response.getContentAsString().contains(String.format("\"name\":\"%s\"", authorName))); assertTrue(response.getContentAsString().contains(String.format("\"mail\":\"%s\"", authorEmail))); assertTrue(response.getContentAsString().contains(String.format("\"description\":\"%s\"", commit))); - assertTrue(response.getContentAsString().contains(String.format("\"description\":\"%s\"", commit))); } @Test @@ -155,7 +153,6 @@ public class ChangesetRootResourceTest { assertTrue(response.getContentAsString().contains(String.format("\"name\":\"%s\"", authorName))); assertTrue(response.getContentAsString().contains(String.format("\"mail\":\"%s\"", authorEmail))); assertTrue(response.getContentAsString().contains(String.format("\"description\":\"%s\"", commit))); - assertTrue(response.getContentAsString().contains(String.format("\"description\":\"%s\"", commit))); } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DiffResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DiffResourceTest.java index 0daeb07b7c..fe22c944c4 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DiffResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DiffResourceTest.java @@ -1,6 +1,7 @@ package sonia.scm.api.v2.resources; +import com.google.inject.util.Providers; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.subject.Subject; import org.apache.shiro.subject.support.SubjectThreadState; @@ -37,7 +38,7 @@ import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.Silent.class) @Slf4j -public class DiffResourceTest { +public class DiffResourceTest extends RepositoryTestBase { public static final String DIFF_PATH = "space/repo/diff/"; @@ -63,10 +64,8 @@ public class DiffResourceTest { @Before public void prepareEnvironment() throws Exception { diffRootResource = new DiffRootResource(serviceFactory); - RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider - .of(new RepositoryResource(null, null, null, null, null, - null, null, null, null, MockProvider.of(diffRootResource))), null); - dispatcher.getRegistry().addSingletonResource(repositoryRootResource); + super.diffRootResource = Providers.of(diffRootResource); + dispatcher.getRegistry().addSingletonResource(getRepositoryRootResource()); when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(service); when(serviceFactory.create(any(Repository.class))).thenReturn(service); when(service.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo")); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DispatcherMock.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DispatcherMock.java index 4d4ed435df..a1abdb6ff4 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DispatcherMock.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/DispatcherMock.java @@ -14,6 +14,9 @@ public class DispatcherMock { dispatcher.getProviderFactory().registerProvider(AlreadyExistsExceptionMapper.class); dispatcher.getProviderFactory().registerProvider(AuthorizationExceptionMapper.class); dispatcher.getProviderFactory().registerProvider(ConcurrentModificationExceptionMapper.class); + dispatcher.getProviderFactory().registerProvider(InternalRepositoryExceptionMapper.class); + dispatcher.getProviderFactory().registerProvider(ChangePasswordNotAllowedExceptionMapper.class); + dispatcher.getProviderFactory().registerProvider(InvalidPasswordExceptionMapper.class); return dispatcher; } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/FileHistoryResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/FileHistoryResourceTest.java new file mode 100644 index 0000000000..934e05d0d1 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/FileHistoryResourceTest.java @@ -0,0 +1,202 @@ +package sonia.scm.api.v2.resources; + +import com.google.inject.util.Providers; +import lombok.extern.slf4j.Slf4j; +import org.apache.shiro.subject.Subject; +import org.apache.shiro.subject.support.SubjectThreadState; +import org.apache.shiro.util.ThreadContext; +import org.apache.shiro.util.ThreadState; +import org.assertj.core.util.Lists; +import org.jboss.resteasy.core.Dispatcher; +import org.jboss.resteasy.mock.MockDispatcherFactory; +import org.jboss.resteasy.mock.MockHttpRequest; +import org.jboss.resteasy.mock.MockHttpResponse; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.api.rest.AuthorizationExceptionMapper; +import sonia.scm.repository.Changeset; +import sonia.scm.repository.ChangesetPagingResult; +import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Person; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryNotFoundException; +import sonia.scm.repository.RevisionNotFoundException; +import sonia.scm.repository.api.LogCommandBuilder; +import sonia.scm.repository.api.RepositoryService; +import sonia.scm.repository.api.RepositoryServiceFactory; +import sonia.scm.web.VndMediaType; + +import java.net.URI; +import java.net.URISyntaxException; +import java.time.Instant; +import java.util.Date; +import java.util.List; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.Silent.class) +@Slf4j +public class FileHistoryResourceTest extends RepositoryTestBase { + + public static final String FILE_HISTORY_PATH = "space/repo/history/"; + public static final String FILE_HISTORY_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + FILE_HISTORY_PATH; + private final Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); + + private final URI baseUri = URI.create("/"); + private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); + + @Mock + private RepositoryServiceFactory serviceFactory; + + @Mock + private RepositoryService service; + + @Mock + private LogCommandBuilder logCommandBuilder; + + private FileHistoryCollectionToDtoMapper fileHistoryCollectionToDtoMapper; + + @InjectMocks + private ChangesetToChangesetDtoMapperImpl changesetToChangesetDtoMapper; + + private FileHistoryRootResource fileHistoryRootResource; + + + private final Subject subject = mock(Subject.class); + private final ThreadState subjectThreadState = new SubjectThreadState(subject); + + + @Before + public void prepareEnvironment() throws Exception { + fileHistoryCollectionToDtoMapper = new FileHistoryCollectionToDtoMapper(changesetToChangesetDtoMapper, resourceLinks); + fileHistoryRootResource = new FileHistoryRootResource(serviceFactory, fileHistoryCollectionToDtoMapper); + super.fileHistoryRootResource = Providers.of(fileHistoryRootResource); + dispatcher.getRegistry().addSingletonResource(getRepositoryRootResource()); + when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(service); + when(serviceFactory.create(any(Repository.class))).thenReturn(service); + when(service.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo")); + dispatcher.getProviderFactory().registerProvider(NotFoundExceptionMapper.class); + dispatcher.getProviderFactory().registerProvider(AuthorizationExceptionMapper.class); + dispatcher.getProviderFactory().registerProvider(InternalRepositoryExceptionMapper.class); + when(service.getLogCommand()).thenReturn(logCommandBuilder); + subjectThreadState.bind(); + ThreadContext.bind(subject); + when(subject.isPermitted(any(String.class))).thenReturn(true); + } + + @After + public void cleanupContext() { + ThreadContext.unbindSubject(); + } + + @Test + public void shouldGetFileHistory() throws Exception { + String id = "revision_123"; + String path = "root_dir/sub_dir/file-to-inspect.txt"; + Instant creationDate = Instant.now(); + String authorName = "name"; + String authorEmail = "em@i.l"; + String commit = "my branch commit"; + ChangesetPagingResult changesetPagingResult = mock(ChangesetPagingResult.class); + List changesetList = Lists.newArrayList(new Changeset(id, Date.from(creationDate).getTime(), new Person(authorName, authorEmail), commit)); + when(changesetPagingResult.getChangesets()).thenReturn(changesetList); + when(changesetPagingResult.getTotal()).thenReturn(1); + when(logCommandBuilder.setPagingStart(anyInt())).thenReturn(logCommandBuilder); + when(logCommandBuilder.setPagingLimit(anyInt())).thenReturn(logCommandBuilder); + when(logCommandBuilder.setStartChangeset(eq(id))).thenReturn(logCommandBuilder); + when(logCommandBuilder.setPath(eq(path))).thenReturn(logCommandBuilder); + when(logCommandBuilder.getChangesets()).thenReturn(changesetPagingResult); + MockHttpRequest request = MockHttpRequest + .get(FILE_HISTORY_URL + id + "/" + path) + .accept(VndMediaType.CHANGESET_COLLECTION); + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + assertEquals(200, response.getStatus()); + log.info("Response :{}", response.getContentAsString()); + assertTrue(response.getContentAsString().contains(String.format("\"id\":\"%s\"", id))); + assertTrue(response.getContentAsString().contains(String.format("\"name\":\"%s\"", authorName))); + assertTrue(response.getContentAsString().contains(String.format("\"mail\":\"%s\"", authorEmail))); + assertTrue(response.getContentAsString().contains(String.format("\"description\":\"%s\"", commit))); + } + + + @Test + public void shouldGet404OnMissingRepository() throws URISyntaxException, RepositoryNotFoundException { + when(serviceFactory.create(any(NamespaceAndName.class))).thenThrow(RepositoryNotFoundException.class); + MockHttpRequest request = MockHttpRequest + .get(FILE_HISTORY_URL + "revision/a.txt") + .accept(VndMediaType.CHANGESET_COLLECTION); + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + assertEquals(404, response.getStatus()); + } + + @Test + public void shouldGet404OnMissingRevision() throws Exception { + String id = "revision_123"; + String path = "root_dir/sub_dir/file-to-inspect.txt"; + + when(logCommandBuilder.setPagingStart(anyInt())).thenReturn(logCommandBuilder); + when(logCommandBuilder.setPagingLimit(anyInt())).thenReturn(logCommandBuilder); + when(logCommandBuilder.setStartChangeset(eq(id))).thenReturn(logCommandBuilder); + when(logCommandBuilder.setPath(eq(path))).thenReturn(logCommandBuilder); + when(logCommandBuilder.getChangesets()).thenThrow(RevisionNotFoundException.class); + + MockHttpRequest request = MockHttpRequest + .get(FILE_HISTORY_URL + id + "/" + path) + .accept(VndMediaType.CHANGESET_COLLECTION); + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + assertEquals(404, response.getStatus()); + } + + @Test + public void shouldGet500OnInternalRepositoryException() throws Exception { + String id = "revision_123"; + String path = "root_dir/sub_dir/file-to-inspect.txt"; + + when(logCommandBuilder.setPagingStart(anyInt())).thenReturn(logCommandBuilder); + when(logCommandBuilder.setPagingLimit(anyInt())).thenReturn(logCommandBuilder); + when(logCommandBuilder.setStartChangeset(eq(id))).thenReturn(logCommandBuilder); + when(logCommandBuilder.setPath(eq(path))).thenReturn(logCommandBuilder); + when(logCommandBuilder.getChangesets()).thenThrow(InternalRepositoryException.class); + + MockHttpRequest request = MockHttpRequest + .get(FILE_HISTORY_URL + id + "/" + path) + .accept(VndMediaType.CHANGESET_COLLECTION); + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + assertEquals(500, response.getStatus()); + } + + @Test + public void shouldGet500OnNullChangesets() throws Exception { + String id = "revision_123"; + String path = "root_dir/sub_dir/file-to-inspect.txt"; + + when(logCommandBuilder.setPagingStart(anyInt())).thenReturn(logCommandBuilder); + when(logCommandBuilder.setPagingLimit(anyInt())).thenReturn(logCommandBuilder); + when(logCommandBuilder.setStartChangeset(eq(id))).thenReturn(logCommandBuilder); + when(logCommandBuilder.setPath(eq(path))).thenReturn(logCommandBuilder); + when(logCommandBuilder.getChangesets()).thenReturn(null); + + MockHttpRequest request = MockHttpRequest + .get(FILE_HISTORY_URL + id + "/" + path) + .accept(VndMediaType.CHANGESET_COLLECTION); + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + assertEquals(500, response.getStatus()); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupCollectionToDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupCollectionToDtoMapperTest.java index 5b457ce4e9..5066f56ff7 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupCollectionToDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupCollectionToDtoMapperTest.java @@ -11,7 +11,6 @@ import org.junit.Test; import sonia.scm.PageResult; import sonia.scm.group.Group; -import javax.ws.rs.core.UriInfo; import java.net.URI; import java.net.URISyntaxException; import java.util.Arrays; @@ -28,9 +27,9 @@ import static sonia.scm.PageResult.createPage; public class GroupCollectionToDtoMapperTest { - private final UriInfo uriInfo = mock(UriInfo.class); - private final UriInfoStore uriInfoStore = new UriInfoStore(); - private final ResourceLinks resourceLinks = new ResourceLinks(uriInfoStore); + private final ScmPathInfo uriInfo = mock(ScmPathInfo.class); + private final ScmPathInfoStore scmPathInfoStore = new ScmPathInfoStore(); + private final ResourceLinks resourceLinks = new ResourceLinks(scmPathInfoStore); private final GroupToGroupDtoMapper groupToDtoMapper = mock(GroupToGroupDtoMapper.class); private final Subject subject = mock(Subject.class); private final ThreadState subjectThreadState = new SubjectThreadState(subject); @@ -41,10 +40,10 @@ public class GroupCollectionToDtoMapperTest { @Before public void init() throws URISyntaxException { - uriInfoStore.set(uriInfo); + scmPathInfoStore.set(uriInfo); URI baseUri = new URI("http://example.com/base/"); expectedBaseUri = baseUri.resolve(GroupRootResource.GROUPS_PATH_V2 + "/"); - when(uriInfo.getBaseUri()).thenReturn(baseUri); + when(uriInfo.getApiRestUri()).thenReturn(baseUri); subjectThreadState.bind(); ThreadContext.bind(subject); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java index 1e42016ace..036579e9dd 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/GroupRootResourceTest.java @@ -3,6 +3,7 @@ package sonia.scm.api.v2.resources; import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.SubjectAware; import com.google.common.io.Resources; +import com.google.inject.util.Providers; import org.jboss.resteasy.core.Dispatcher; import org.jboss.resteasy.mock.MockHttpRequest; import org.jboss.resteasy.mock.MockHttpResponse; @@ -73,7 +74,7 @@ public class GroupRootResourceTest { GroupCollectionToDtoMapper groupCollectionToDtoMapper = new GroupCollectionToDtoMapper(groupToDtoMapper, resourceLinks); GroupCollectionResource groupCollectionResource = new GroupCollectionResource(groupManager, dtoToGroupMapper, groupCollectionToDtoMapper, resourceLinks); GroupResource groupResource = new GroupResource(groupManager, groupToDtoMapper, dtoToGroupMapper); - GroupRootResource groupRootResource = new GroupRootResource(MockProvider.of(groupCollectionResource), MockProvider.of(groupResource)); + GroupRootResource groupRootResource = new GroupRootResource(Providers.of(groupCollectionResource), Providers.of(groupResource)); dispatcher = createDispatcher(groupRootResource); dispatcher.getProviderFactory().registerProviderInstance(new JSONContextResolver(new ObjectMapperProvider().get())); @@ -223,6 +224,64 @@ public class GroupRootResourceTest { assertEquals("user1", createdGroup.getMembers().get(0)); } + @Test + public void shouldGet400OnCreatingNewGroupWithNotAllowedCharacters() throws URISyntaxException { + // the @ character at the begin of the name is not allowed + String groupJson = "{ \"name\": \"@grpname\", \"type\": \"admin\" }"; + MockHttpRequest request = MockHttpRequest + .post("/" + GroupRootResource.GROUPS_PATH_V2) + .contentType(VndMediaType.GROUP) + .content(groupJson.getBytes()); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(400, response.getStatus()); + + // the whitespace at the begin of the name is not allowed + groupJson = "{ \"name\": \" grpname\", \"type\": \"admin\" }"; + request = MockHttpRequest + .post("/" + GroupRootResource.GROUPS_PATH_V2) + .contentType(VndMediaType.GROUP) + .content(groupJson.getBytes()); + + dispatcher.invoke(request, response); + + assertEquals(400, response.getStatus()); + + // the characters {[ are not allowed + groupJson = "{ \"name\": \"grp{name}\", \"type\": \"admin\" }"; + request = MockHttpRequest + .post("/" + GroupRootResource.GROUPS_PATH_V2) + .contentType(VndMediaType.GROUP) + .content(groupJson.getBytes()); + + dispatcher.invoke(request, response); + + assertEquals(400, response.getStatus()); + + groupJson = "{ \"name\": \"grp[name]\", \"type\": \"admin\" }"; + request = MockHttpRequest + .post("/" + GroupRootResource.GROUPS_PATH_V2) + .contentType(VndMediaType.GROUP) + .content(groupJson.getBytes()); + + dispatcher.invoke(request, response); + + assertEquals(400, response.getStatus()); + + groupJson = "{ \"name\": \"grp/name\", \"type\": \"admin\" }"; + request = MockHttpRequest + .post("/" + GroupRootResource.GROUPS_PATH_V2) + .contentType(VndMediaType.GROUP) + .content(groupJson.getBytes()); + + dispatcher.invoke(request, response); + + assertEquals(400, response.getStatus()); + + } + @Test public void shouldFailForMissingContent() throws URISyntaxException { MockHttpRequest request = MockHttpRequest diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/IndexResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/IndexResourceTest.java new file mode 100644 index 0000000000..83697a3c4a --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/IndexResourceTest.java @@ -0,0 +1,115 @@ +package sonia.scm.api.v2.resources; + +import com.github.sdorra.shiro.ShiroRule; +import com.github.sdorra.shiro.SubjectAware; +import org.assertj.core.api.Assertions; +import org.junit.Rule; +import org.junit.Test; +import sonia.scm.SCMContextProvider; + +import java.net.URI; +import java.util.Optional; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@SubjectAware(configuration = "classpath:sonia/scm/shiro-001.ini") +public class IndexResourceTest { + + @Rule + public final ShiroRule shiroRule = new ShiroRule(); + + private final SCMContextProvider scmContextProvider = mock(SCMContextProvider.class); + private final IndexDtoGenerator indexDtoGenerator = new IndexDtoGenerator(ResourceLinksMock.createMock(URI.create("/")), scmContextProvider); + private final IndexResource indexResource = new IndexResource(indexDtoGenerator); + + @Test + public void shouldRenderLoginUrlsForUnauthenticatedRequest() { + IndexDto index = indexResource.getIndex(); + + Assertions.assertThat(index.getLinks().getLinkBy("login")).matches(Optional::isPresent); + } + + @Test + public void shouldRenderSelfLinkForUnauthenticatedRequest() { + IndexDto index = indexResource.getIndex(); + + Assertions.assertThat(index.getLinks().getLinkBy("self")).matches(Optional::isPresent); + } + + @Test + public void shouldRenderUiPluginsLinkForUnauthenticatedRequest() { + IndexDto index = indexResource.getIndex(); + + Assertions.assertThat(index.getLinks().getLinkBy("uiPlugins")).matches(Optional::isPresent); + } + + @Test + @SubjectAware(username = "trillian", password = "secret") + public void shouldRenderSelfLinkForAuthenticatedRequest() { + IndexDto index = indexResource.getIndex(); + + Assertions.assertThat(index.getLinks().getLinkBy("self")).matches(Optional::isPresent); + } + + @Test + @SubjectAware(username = "trillian", password = "secret") + public void shouldRenderUiPluginsLinkForAuthenticatedRequest() { + IndexDto index = indexResource.getIndex(); + + Assertions.assertThat(index.getLinks().getLinkBy("uiPlugins")).matches(Optional::isPresent); + } + + @Test + @SubjectAware(username = "trillian", password = "secret") + public void shouldRenderMeUrlForAuthenticatedRequest() { + IndexDto index = indexResource.getIndex(); + + Assertions.assertThat(index.getLinks().getLinkBy("me")).matches(Optional::isPresent); + } + + @Test + @SubjectAware(username = "trillian", password = "secret") + public void shouldRenderLogoutUrlForAuthenticatedRequest() { + IndexDto index = indexResource.getIndex(); + + Assertions.assertThat(index.getLinks().getLinkBy("logout")).matches(Optional::isPresent); + } + + @Test + @SubjectAware(username = "trillian", password = "secret") + public void shouldRenderRepositoriesForAuthenticatedRequest() { + IndexDto index = indexResource.getIndex(); + + Assertions.assertThat(index.getLinks().getLinkBy("repositories")).matches(Optional::isPresent); + } + + @Test + @SubjectAware(username = "trillian", password = "secret") + public void shouldNotRenderAdminLinksIfNotAuthorized() { + IndexDto index = indexResource.getIndex(); + + Assertions.assertThat(index.getLinks().getLinkBy("users")).matches(o -> !o.isPresent()); + Assertions.assertThat(index.getLinks().getLinkBy("groups")).matches(o -> !o.isPresent()); + Assertions.assertThat(index.getLinks().getLinkBy("config")).matches(o -> !o.isPresent()); + } + + @Test + @SubjectAware(username = "dent", password = "secret") + public void shouldRenderAdminLinksIfAuthorized() { + IndexDto index = indexResource.getIndex(); + + Assertions.assertThat(index.getLinks().getLinkBy("users")).matches(Optional::isPresent); + Assertions.assertThat(index.getLinks().getLinkBy("groups")).matches(Optional::isPresent); + Assertions.assertThat(index.getLinks().getLinkBy("config")).matches(Optional::isPresent); + } + + @Test + public void shouldGenerateVersion() { + when(scmContextProvider.getVersion()).thenReturn("v1"); + + IndexDto index = indexResource.getIndex(); + + Assertions.assertThat(index.getVersion()).isEqualTo("v1"); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/LinkBuilderTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/LinkBuilderTest.java index 37c584dfdf..c84e1a21a7 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/LinkBuilderTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/LinkBuilderTest.java @@ -4,7 +4,6 @@ import org.junit.Before; import org.junit.Test; import javax.ws.rs.Path; -import javax.ws.rs.core.UriInfo; import java.net.URI; import java.net.URISyntaxException; @@ -37,7 +36,7 @@ public class LinkBuilderTest { } } - private UriInfo uriInfo = mock(UriInfo.class); + private ScmPathInfo uriInfo = mock(ScmPathInfo.class); @Test public void shouldBuildSimplePath() { @@ -94,6 +93,6 @@ public class LinkBuilderTest { @Before public void setBaseUri() throws URISyntaxException { - when(uriInfo.getBaseUri()).thenReturn(new URI("http://example.com/")); + when(uriInfo.getApiRestUri()).thenReturn(new URI("http://example.com/")); } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeResourceTest.java index 09dd545eb5..c7b040172e 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeResourceTest.java @@ -2,11 +2,12 @@ package sonia.scm.api.v2.resources; import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.SubjectAware; +import org.apache.shiro.authc.credential.PasswordService; import org.jboss.resteasy.core.Dispatcher; -import org.jboss.resteasy.mock.MockDispatcherFactory; import org.jboss.resteasy.mock.MockHttpRequest; import org.jboss.resteasy.mock.MockHttpResponse; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.mockito.ArgumentCaptor; @@ -17,17 +18,22 @@ import sonia.scm.user.UserManager; import sonia.scm.web.VndMediaType; import javax.servlet.http.HttpServletResponse; -import javax.ws.rs.core.UriInfo; import java.net.URI; import java.net.URISyntaxException; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; +import static sonia.scm.api.v2.resources.DispatcherMock.createDispatcher; @SubjectAware( + username = "trillian", + password = "secret", configuration = "classpath:sonia/scm/repository/shiro.ini" ) public class MeResourceTest { @@ -35,35 +41,40 @@ public class MeResourceTest { @Rule public ShiroRule shiro = new ShiroRule(); - private Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); - + private Dispatcher dispatcher; private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(URI.create("/")); @Mock - private UriInfo uriInfo; + private ScmPathInfo uriInfo; @Mock - private UriInfoStore uriInfoStore; + private ScmPathInfoStore scmPathInfoStore; @Mock private UserManager userManager; @InjectMocks - private UserToUserDtoMapperImpl userToDtoMapper; + private MeToUserDtoMapperImpl userToDtoMapper; private ArgumentCaptor userCaptor = ArgumentCaptor.forClass(User.class); + @Mock + private PasswordService passwordService; + private User originalUser; + @Before public void prepareEnvironment() throws Exception { initMocks(this); - createDummyUser("trillian"); + originalUser = createDummyUser("trillian"); when(userManager.create(userCaptor.capture())).thenAnswer(invocation -> invocation.getArguments()[0]); doNothing().when(userManager).modify(userCaptor.capture()); doNothing().when(userManager).delete(userCaptor.capture()); - userToDtoMapper.setResourceLinks(resourceLinks); - MeResource meResource = new MeResource(userToDtoMapper, userManager); - dispatcher.getRegistry().addSingletonResource(meResource); - when(uriInfo.getBaseUri()).thenReturn(URI.create("/")); - when(uriInfoStore.get()).thenReturn(uriInfo); + when(userManager.isTypeDefault(userCaptor.capture())).thenCallRealMethod(); + when(userManager.getUserTypeChecker()).thenCallRealMethod(); + when(userManager.getDefaultType()).thenReturn("xml"); + MeResource meResource = new MeResource(userToDtoMapper, userManager, passwordService); + when(uriInfo.getApiRestUri()).thenReturn(URI.create("/")); + when(scmPathInfoStore.get()).thenReturn(uriInfo); + dispatcher = createDispatcher(meResource); } @Test @@ -77,14 +88,77 @@ public class MeResourceTest { assertEquals(HttpServletResponse.SC_OK, response.getStatus()); assertTrue(response.getContentAsString().contains("\"name\":\"trillian\"")); - assertTrue(response.getContentAsString().contains("\"password\":\"__dummypassword__\"")); - assertTrue(response.getContentAsString().contains("\"self\":{\"href\":\"/v2/users/trillian\"}")); + assertTrue(response.getContentAsString().contains("\"self\":{\"href\":\"/v2/me/\"}")); assertTrue(response.getContentAsString().contains("\"delete\":{\"href\":\"/v2/users/trillian\"}")); } + @Test + @SubjectAware(username = "trillian", password = "secret") + public void shouldEncryptPasswordBeforeChanging() throws Exception { + String newPassword = "pwd123"; + String encryptedNewPassword = "encrypted123"; + String oldPassword = "notEncriptedSecret"; + String content = String.format("{ \"oldPassword\": \"%s\" , \"newPassword\": \"%s\" }", oldPassword, newPassword); + MockHttpRequest request = MockHttpRequest + .put("/" + MeResource.ME_PATH_V2 + "password") + .contentType(VndMediaType.PASSWORD_CHANGE) + .content(content.getBytes()); + MockHttpResponse response = new MockHttpResponse(); + when(passwordService.encryptPassword(eq(newPassword))).thenReturn(encryptedNewPassword); + when(passwordService.encryptPassword(eq(oldPassword))).thenReturn("secret"); + + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus()); + verify(userManager).modify(any(User.class)); + User updatedUser = userCaptor.getValue(); + assertEquals(encryptedNewPassword, updatedUser.getPassword()); + } + + @Test + @SubjectAware(username = "trillian", password = "secret") + public void shouldGet400OnChangePasswordOfUserWithNonDefaultType() throws Exception { + originalUser.setType("not an xml type"); + String newPassword = "pwd123"; + String oldPassword = "notEncriptedSecret"; + String content = String.format("{ \"oldPassword\": \"%s\" , \"newPassword\": \"%s\" }", oldPassword, newPassword); + MockHttpRequest request = MockHttpRequest + .put("/" + MeResource.ME_PATH_V2 + "password") + .contentType(VndMediaType.PASSWORD_CHANGE) + .content(content.getBytes()); + MockHttpResponse response = new MockHttpResponse(); + when(passwordService.encryptPassword(newPassword)).thenReturn("encrypted123"); + when(passwordService.encryptPassword(eq(oldPassword))).thenReturn("secret"); + + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_BAD_REQUEST, response.getStatus()); + } + + @Test + @SubjectAware(username = "trillian", password = "secret") + public void shouldGet400OnChangePasswordIfOldPasswordDoesNotMatchOriginalPassword() throws Exception { + String newPassword = "pwd123"; + String oldPassword = "notEncriptedSecret"; + String content = String.format("{ \"oldPassword\": \"%s\" , \"newPassword\": \"%s\" }", oldPassword, newPassword); + MockHttpRequest request = MockHttpRequest + .put("/" + MeResource.ME_PATH_V2 + "password") + .contentType(VndMediaType.PASSWORD_CHANGE) + .content(content.getBytes()); + MockHttpResponse response = new MockHttpResponse(); + when(passwordService.encryptPassword(newPassword)).thenReturn("encrypted123"); + when(passwordService.encryptPassword(eq(oldPassword))).thenReturn("differentThanSecret"); + + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_BAD_REQUEST, response.getStatus()); + } + + private User createDummyUser(String name) { User user = new User(); user.setName(name); + user.setType("xml"); user.setPassword("secret"); user.setCreationDate(System.currentTimeMillis()); when(userManager.get(name)).thenReturn(user); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeToUserDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeToUserDtoMapperTest.java new file mode 100644 index 0000000000..4f40098da5 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MeToUserDtoMapperTest.java @@ -0,0 +1,135 @@ +package sonia.scm.api.v2.resources; + +import org.apache.shiro.subject.Subject; +import org.apache.shiro.subject.support.SubjectThreadState; +import org.apache.shiro.util.ThreadContext; +import org.apache.shiro.util.ThreadState; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import sonia.scm.user.User; +import sonia.scm.user.UserManager; + +import java.net.URI; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; + +public class MeToUserDtoMapperTest { + + private final URI baseUri = URI.create("http://example.com/base/"); + @SuppressWarnings("unused") // Is injected + private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); + + @Mock + private UserManager userManager; + + @InjectMocks + private MeToUserDtoMapperImpl mapper; + + private final Subject subject = mock(Subject.class); + private final ThreadState subjectThreadState = new SubjectThreadState(subject); + + private URI expectedBaseUri; + private URI expectedUserBaseUri; + + @Before + public void init() { + initMocks(this); + when(userManager.getDefaultType()).thenReturn("xml"); + expectedBaseUri = baseUri.resolve(MeResource.ME_PATH_V2 + "/"); + expectedUserBaseUri = baseUri.resolve(UserRootResource.USERS_PATH_V2 + "/"); + subjectThreadState.bind(); + ThreadContext.bind(subject); + } + + @After + public void unbindSubject() { + ThreadContext.unbindSubject(); + } + + @Test + public void shouldMapTheUpdateLink() { + User user = createDefaultUser(); + when(subject.isPermitted("user:modify:abc")).thenReturn(true); + + UserDto userDto = mapper.map(user); + assertEquals("expected update link", expectedUserBaseUri.resolve("abc").toString(), userDto.getLinks().getLinkBy("update").get().getHref()); + + when(subject.isPermitted("user:modify:abc")).thenReturn(false); + userDto = mapper.map(user); + assertFalse("expected no update link", userDto.getLinks().getLinkBy("update").isPresent()); + } + + @Test + public void shouldMapTheSelfLink() { + User user = createDefaultUser(); + when(subject.isPermitted("user:modify:abc")).thenReturn(true); + + UserDto userDto = mapper.map(user); + assertEquals("expected self link", expectedBaseUri.toString(), userDto.getLinks().getLinkBy("self").get().getHref()); + + } + + @Test + public void shouldMapTheDeleteLink() { + User user = createDefaultUser(); + when(subject.isPermitted("user:delete:abc")).thenReturn(true); + + UserDto userDto = mapper.map(user); + assertEquals("expected update link", expectedUserBaseUri.resolve("abc").toString(), userDto.getLinks().getLinkBy("delete").get().getHref()); + + when(subject.isPermitted("user:delete:abc")).thenReturn(false); + userDto = mapper.map(user); + assertFalse("expected no delete link", userDto.getLinks().getLinkBy("delete").isPresent()); + } + + @Test + public void shouldGetPasswordLinkOnlyForDefaultUserType() { + User user = createDefaultUser(); + when(subject.isPermitted("user:modify:abc")).thenReturn(true); + when(userManager.isTypeDefault(eq(user))).thenReturn(true); + + UserDto userDto = mapper.map(user); + + assertEquals("expected password link with modify permission", expectedBaseUri.resolve("password").toString(), userDto.getLinks().getLinkBy("password").get().getHref()); + + when(subject.isPermitted("user:modify:abc")).thenReturn(false); + userDto = mapper.map(user); + assertEquals("expected password link on mission modify permission", expectedBaseUri.resolve("password").toString(), userDto.getLinks().getLinkBy("password").get().getHref()); + + when(userManager.isTypeDefault(eq(user))).thenReturn(false); + + userDto = mapper.map(user); + + assertFalse("expected no password link", userDto.getLinks().getLinkBy("password").isPresent()); + } + + + @Test + public void shouldGetEmptyPasswordProperty() { + User user = createDefaultUser(); + user.setPassword("myHighSecurePassword"); + when(subject.isPermitted("user:modify:abc")).thenReturn(true); + + UserDto userDto = mapper.map(user); + + assertThat(userDto.getPassword()).as("hide password for the me resource").isBlank(); + } + + private User createDefaultUser() { + User user = new User(); + user.setName("abc"); + user.setCreationDate(1L); + return user; + } + + +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MockProvider.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MockProvider.java deleted file mode 100644 index bf84e4fe15..0000000000 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MockProvider.java +++ /dev/null @@ -1,22 +0,0 @@ -package sonia.scm.api.v2.resources; - -import javax.inject.Provider; - -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -/** - * A mockito implementation of CDI {@link javax.inject.Provider}. - */ -class MockProvider { - - private MockProvider() {} - - static Provider of(I instance) { - @SuppressWarnings("unchecked") // Can't make mockito return typed provider - Provider provider = mock(Provider.class); - when(provider.get()).thenReturn(instance); - return provider; - } - -} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MockScmProtocol.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MockScmProtocol.java new file mode 100644 index 0000000000..1ae5344849 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/MockScmProtocol.java @@ -0,0 +1,23 @@ +package sonia.scm.api.v2.resources; + +import sonia.scm.repository.api.ScmProtocol; + +class MockScmProtocol implements ScmProtocol { + private final String type; + private final String protocol; + + public MockScmProtocol(String type, String protocol) { + this.type = type; + this.protocol = protocol; + } + + @Override + public String getType() { + return type; + } + + @Override + public String getUrl() { + return protocol; + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ModificationsResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ModificationsResourceTest.java new file mode 100644 index 0000000000..64942fb84b --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ModificationsResourceTest.java @@ -0,0 +1,150 @@ +package sonia.scm.api.v2.resources; + +import com.google.inject.util.Providers; +import lombok.extern.slf4j.Slf4j; +import org.apache.shiro.subject.Subject; +import org.apache.shiro.subject.support.SubjectThreadState; +import org.apache.shiro.util.ThreadContext; +import org.apache.shiro.util.ThreadState; +import org.assertj.core.util.Lists; +import org.jboss.resteasy.core.Dispatcher; +import org.jboss.resteasy.mock.MockDispatcherFactory; +import org.jboss.resteasy.mock.MockHttpRequest; +import org.jboss.resteasy.mock.MockHttpResponse; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import sonia.scm.api.rest.AuthorizationExceptionMapper; +import sonia.scm.repository.InternalRepositoryException; +import sonia.scm.repository.Modifications; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Repository; +import sonia.scm.repository.api.ModificationsCommandBuilder; +import sonia.scm.repository.api.RepositoryService; +import sonia.scm.repository.api.RepositoryServiceFactory; +import sonia.scm.web.VndMediaType; + +import java.net.URI; +import java.text.MessageFormat; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@Slf4j +@RunWith(MockitoJUnitRunner.Silent.class) +public class ModificationsResourceTest extends RepositoryTestBase { + + + public static final String MODIFICATIONS_PATH = "space/repo/modifications/"; + public static final String MODIFICATIONS_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + MODIFICATIONS_PATH; + private final Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); + + private final URI baseUri = URI.create("/"); + private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); + + @Mock + private RepositoryServiceFactory serviceFactory; + + @Mock + private RepositoryService repositoryService; + + @Mock + private ModificationsCommandBuilder modificationsCommandBuilder; + + @InjectMocks + private ModificationsToDtoMapperImpl modificationsToDtoMapper; + + private ModificationsRootResource modificationsRootResource; + + + private final Subject subject = mock(Subject.class); + private final ThreadState subjectThreadState = new SubjectThreadState(subject); + + + @Before + public void prepareEnvironment() throws Exception { + modificationsRootResource = new ModificationsRootResource(serviceFactory, modificationsToDtoMapper); + super.modificationsRootResource = Providers.of(modificationsRootResource); + dispatcher.getRegistry().addSingletonResource(getRepositoryRootResource()); + when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(repositoryService); + when(serviceFactory.create(any(Repository.class))).thenReturn(repositoryService); + when(repositoryService.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo")); + dispatcher.getProviderFactory().registerProvider(NotFoundExceptionMapper.class); + dispatcher.getProviderFactory().registerProvider(AuthorizationExceptionMapper.class); + dispatcher.getProviderFactory().registerProvider(InternalRepositoryExceptionMapper.class); + when(repositoryService.getModificationsCommand()).thenReturn(modificationsCommandBuilder); + subjectThreadState.bind(); + ThreadContext.bind(subject); + when(subject.isPermitted(any(String.class))).thenReturn(true); + } + + @After + public void cleanupContext() { + ThreadContext.unbindSubject(); + } + + @Test + public void shouldGet404OnMissingModifications() throws Exception { + when(modificationsCommandBuilder.revision(any())).thenReturn(modificationsCommandBuilder); + when(modificationsCommandBuilder.getModifications()).thenReturn(null); + + MockHttpRequest request = MockHttpRequest + .get(MODIFICATIONS_URL + "not_existing_revision") + .accept(VndMediaType.MODIFICATIONS); + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + assertEquals(404, response.getStatus()); + } + + @Test + public void shouldGet500OnModificationsCommandError() throws Exception { + when(modificationsCommandBuilder.revision(any())).thenReturn(modificationsCommandBuilder); + when(modificationsCommandBuilder.getModifications()).thenThrow(InternalRepositoryException.class); + + MockHttpRequest request = MockHttpRequest + .get(MODIFICATIONS_URL + "revision") + .accept(VndMediaType.MODIFICATIONS); + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + assertEquals(500, response.getStatus()); + } + + + @Test + public void shouldGetModifications() throws Exception { + Modifications modifications = new Modifications(); + String revision = "revision"; + String addedFile_1 = "a.txt"; + String addedFile_2 = "b.txt"; + String modifiedFile_1 = "d.txt"; + String modifiedFile_2 = "c.txt"; + String removedFile_1 = "e.txt"; + String removedFile_2 = "f.txt"; + modifications.setRevision(revision); + modifications.setAdded(Lists.newArrayList(addedFile_1, addedFile_2)); + modifications.setModified(Lists.newArrayList(modifiedFile_1, modifiedFile_2)); + modifications.setRemoved(Lists.newArrayList(removedFile_1, removedFile_2)); + when(modificationsCommandBuilder.getModifications()).thenReturn(modifications); + when(modificationsCommandBuilder.revision(eq(revision))).thenReturn(modificationsCommandBuilder); + + MockHttpRequest request = MockHttpRequest + .get(MODIFICATIONS_URL + revision) + .accept(VndMediaType.MODIFICATIONS); + MockHttpResponse response = new MockHttpResponse(); + dispatcher.invoke(request, response); + assertEquals(200, response.getStatus()); + log.info("the content: ", response.getContentAsString()); + assertTrue(response.getContentAsString().contains(String.format("\"revision\":\"%s\"", revision))); + assertTrue(response.getContentAsString().contains(MessageFormat.format("\"added\":[\"{0}\",\"{1}\"]", addedFile_1, addedFile_2))); + assertTrue(response.getContentAsString().contains(MessageFormat.format("\"modified\":[\"{0}\",\"{1}\"]", modifiedFile_1, modifiedFile_2))); + assertTrue(response.getContentAsString().contains(MessageFormat.format("\"removed\":[\"{0}\",\"{1}\"]", removedFile_1, removedFile_2))); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java index 8d05c1f455..635d994763 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PermissionRootResourceTest.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.SubjectAware; import com.google.common.collect.ImmutableList; +import com.google.inject.util.Providers; import de.otto.edison.hal.HalRepresentation; import lombok.ToString; import lombok.extern.slf4j.Slf4j; @@ -47,6 +48,7 @@ import java.util.stream.Stream; import static de.otto.edison.hal.Link.link; import static de.otto.edison.hal.Links.linkingTo; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; import static org.junit.jupiter.api.DynamicTest.dynamicTest; import static org.mockito.Matchers.any; @@ -64,7 +66,7 @@ import static sonia.scm.api.v2.resources.PermissionDto.GROUP_PREFIX; password = "secret", configuration = "classpath:sonia/scm/repository/shiro.ini" ) -public class PermissionRootResourceTest { +public class PermissionRootResourceTest extends RepositoryTestBase { private static final String REPOSITORY_NAMESPACE = "repo_namespace"; private static final String REPOSITORY_NAME = "repo"; private static final String PERMISSION_WRITE = "repository:permissionWrite:" + REPOSITORY_NAME; @@ -137,9 +139,8 @@ public class PermissionRootResourceTest { initMocks(this); permissionCollectionToDtoMapper = new PermissionCollectionToDtoMapper(permissionToPermissionDtoMapper, resourceLinks); permissionRootResource = new PermissionRootResource(permissionDtoToPermissionMapper, permissionToPermissionDtoMapper, permissionCollectionToDtoMapper, resourceLinks, repositoryManager); - RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider - .of(new RepositoryResource(null, null, null, null, null, null, null, null, MockProvider.of(permissionRootResource), null)), null); - dispatcher = createDispatcher(repositoryRootResource); + super.permissionRootResource = Providers.of(permissionRootResource); + dispatcher = createDispatcher(getRepositoryRootResource()); subjectThreadState.bind(); ThreadContext.bind(subject); } @@ -233,6 +234,35 @@ public class PermissionRootResourceTest { ); } + + @Test + public void shouldGet400OnCreatingNewPermissionWithNotAllowedCharacters() throws URISyntaxException { + // the @ character at the begin of the name is not allowed + createUserWithRepository("user"); + String permissionJson = "{ \"name\": \"@permission\", \"type\": \"OWNER\" }"; + MockHttpRequest request = MockHttpRequest + .post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + PATH_OF_ALL_PERMISSIONS) + .content(permissionJson.getBytes()) + .contentType(VndMediaType.PERMISSION); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(400, response.getStatus()); + + // the whitespace at the begin opf the name is not allowed + permissionJson = "{ \"name\": \" permission\", \"type\": \"OWNER\" }"; + request = MockHttpRequest + .post("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + PATH_OF_ALL_PERMISSIONS) + .content(permissionJson.getBytes()) + .contentType(VndMediaType.PERMISSION); + response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(400, response.getStatus()); + } + @Test public void shouldGetCreatedPermissions() throws URISyntaxException { createUserWithRepositoryAndPermissions(TEST_PERMISSIONS, PERMISSION_WRITE); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java index 539d469445..0b2e50b6d9 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryRootResourceTest.java @@ -3,13 +3,13 @@ package sonia.scm.api.v2.resources; import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.SubjectAware; import com.google.common.io.Resources; +import com.google.inject.util.Providers; import org.jboss.resteasy.core.Dispatcher; import org.jboss.resteasy.mock.MockHttpRequest; import org.jboss.resteasy.mock.MockHttpResponse; import org.junit.Before; import org.junit.Rule; import org.junit.Test; -import org.mockito.Answers; import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; @@ -20,6 +20,7 @@ import sonia.scm.repository.PermissionType; import sonia.scm.repository.Repository; import sonia.scm.repository.RepositoryIsNotArchivedException; import sonia.scm.repository.RepositoryManager; +import sonia.scm.repository.api.RepositoryService; import sonia.scm.repository.api.RepositoryServiceFactory; import sonia.scm.web.VndMediaType; @@ -30,6 +31,7 @@ import java.net.URISyntaxException; import java.net.URL; import static java.util.Collections.singletonList; +import static java.util.stream.Stream.of; import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; import static javax.servlet.http.HttpServletResponse.SC_NO_CONTENT; @@ -55,7 +57,7 @@ import static sonia.scm.api.v2.resources.DispatcherMock.createDispatcher; password = "secret", configuration = "classpath:sonia/scm/repository/shiro.ini" ) -public class RepositoryRootResourceTest { +public class RepositoryRootResourceTest extends RepositoryTestBase { private Dispatcher dispatcher; @@ -64,8 +66,14 @@ public class RepositoryRootResourceTest { @Mock private RepositoryManager repositoryManager; - @Mock(answer = Answers.RETURNS_DEEP_STUBS) + @Mock private RepositoryServiceFactory serviceFactory; + @Mock + private RepositoryService service; + @Mock + private ScmPathInfoStore scmPathInfoStore; + @Mock + private ScmPathInfo uriInfo; private final URI baseUri = URI.create("/"); @@ -79,11 +87,15 @@ public class RepositoryRootResourceTest { @Before public void prepareEnvironment() { initMocks(this); - RepositoryResource repositoryResource = new RepositoryResource(repositoryToDtoMapper, dtoToRepositoryMapper, repositoryManager, null, null, null, null, null, null, null); + super.repositoryToDtoMapper = repositoryToDtoMapper; + super.dtoToRepositoryMapper = dtoToRepositoryMapper; + super.manager = repositoryManager; RepositoryCollectionToDtoMapper repositoryCollectionToDtoMapper = new RepositoryCollectionToDtoMapper(repositoryToDtoMapper, resourceLinks); - RepositoryCollectionResource repositoryCollectionResource = new RepositoryCollectionResource(repositoryManager, repositoryCollectionToDtoMapper, dtoToRepositoryMapper, resourceLinks); - RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider.of(repositoryResource), MockProvider.of(repositoryCollectionResource)); - dispatcher = createDispatcher(repositoryRootResource); + super.repositoryCollectionResource = Providers.of(new RepositoryCollectionResource(repositoryManager, repositoryCollectionToDtoMapper, dtoToRepositoryMapper, resourceLinks)); + dispatcher = createDispatcher(getRepositoryRootResource()); + when(serviceFactory.create(any(Repository.class))).thenReturn(service); + when(scmPathInfoStore.get()).thenReturn(uriInfo); + when(uriInfo.getApiRestUri()).thenReturn(URI.create("/x/y")); } @Test @@ -267,6 +279,20 @@ public class RepositoryRootResourceTest { assertFalse(modifiedRepositoryCaptor.getValue().getPermissions().isEmpty()); } + @Test + public void shouldCreateArrayOfProtocolUrls() throws Exception { + mockRepository("space", "repo"); + when(service.getSupportedProtocols()).thenReturn(of(new MockScmProtocol("http", "http://"), new MockScmProtocol("ssh", "ssh://"))); + + MockHttpRequest request = MockHttpRequest.get("/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + "space/repo"); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(SC_OK, response.getStatus()); + assertTrue(response.getContentAsString().contains("\"protocol\":[{\"href\":\"http://\",\"name\":\"http\"},{\"href\":\"ssh://\",\"name\":\"ssh\"}]")); + } + private PageResult createSingletonPageResult(Repository repository) { return new PageResult<>(singletonList(repository), 0); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryTestBase.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryTestBase.java new file mode 100644 index 0000000000..3d3b28ae51 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryTestBase.java @@ -0,0 +1,43 @@ +package sonia.scm.api.v2.resources; + +import com.google.inject.util.Providers; +import sonia.scm.repository.RepositoryManager; + +import javax.inject.Provider; + +public abstract class RepositoryTestBase { + + + protected RepositoryToRepositoryDtoMapper repositoryToDtoMapper; + protected RepositoryDtoToRepositoryMapper dtoToRepositoryMapper; + protected RepositoryManager manager; + protected Provider tagRootResource; + protected Provider branchRootResource; + protected Provider changesetRootResource; + protected Provider sourceRootResource; + protected Provider contentResource; + protected Provider permissionRootResource; + protected Provider diffRootResource; + protected Provider modificationsRootResource; + protected Provider fileHistoryRootResource; + protected Provider repositoryCollectionResource; + + + RepositoryRootResource getRepositoryRootResource() { + return new RepositoryRootResource(Providers.of(new RepositoryResource( + repositoryToDtoMapper, + dtoToRepositoryMapper, + manager, + tagRootResource, + branchRootResource, + changesetRootResource, + sourceRootResource, + contentResource, + permissionRootResource, + diffRootResource, + modificationsRootResource, + fileHistoryRootResource)), repositoryCollectionResource); + } + + +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java index 0c77d40023..2e6048d6b8 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryToRepositoryDtoMapperTest.java @@ -7,7 +7,6 @@ import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; -import org.mockito.Answers; import org.mockito.InjectMocks; import org.mockito.Mock; import sonia.scm.repository.HealthCheckFailure; @@ -15,13 +14,17 @@ import sonia.scm.repository.Permission; import sonia.scm.repository.PermissionType; import sonia.scm.repository.Repository; import sonia.scm.repository.api.Command; +import sonia.scm.repository.api.RepositoryService; import sonia.scm.repository.api.RepositoryServiceFactory; +import sonia.scm.repository.api.ScmProtocol; import java.net.URI; import static java.util.Collections.singletonList; +import static java.util.stream.Stream.of; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.any; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; @@ -39,8 +42,14 @@ public class RepositoryToRepositoryDtoMapperTest { private final URI baseUri = URI.create("http://example.com/base/"); @SuppressWarnings("unused") // Is injected private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); - @Mock(answer = Answers.RETURNS_DEEP_STUBS) + @Mock private RepositoryServiceFactory serviceFactory; + @Mock + private RepositoryService repositoryService; + @Mock + private ScmPathInfoStore scmPathInfoStore; + @Mock + private ScmPathInfo uriInfo; @InjectMocks private RepositoryToRepositoryDtoMapperImpl mapper; @@ -48,7 +57,11 @@ public class RepositoryToRepositoryDtoMapperTest { @Before public void init() { initMocks(this); - when(serviceFactory.create(any(Repository.class)).isSupported(any(Command.class))).thenReturn(true); + when(serviceFactory.create(any(Repository.class))).thenReturn(repositoryService); + when(repositoryService.isSupported(any(Command.class))).thenReturn(true); + when(repositoryService.getSupportedProtocols()).thenReturn(of()); + when(scmPathInfoStore.get()).thenReturn(uriInfo); + when(uriInfo.getApiRestUri()).thenReturn(URI.create("/x/y")); } @After @@ -129,14 +142,14 @@ public class RepositoryToRepositoryDtoMapperTest { @Test public void shouldNotCreateTagsLink_ifNotSupported() { - when(serviceFactory.create(any(Repository.class)).isSupported(Command.TAGS)).thenReturn(false); + when(repositoryService.isSupported(Command.TAGS)).thenReturn(false); RepositoryDto dto = mapper.map(createTestRepository()); assertFalse(dto.getLinks().getLinkBy("tags").isPresent()); } @Test public void shouldNotCreateBranchesLink_ifNotSupported() { - when(serviceFactory.create(any(Repository.class)).isSupported(Command.BRANCHES)).thenReturn(false); + when(repositoryService.isSupported(Command.BRANCHES)).thenReturn(false); RepositoryDto dto = mapper.map(createTestRepository()); assertFalse(dto.getLinks().getLinkBy("branches").isPresent()); } @@ -165,6 +178,43 @@ public class RepositoryToRepositoryDtoMapperTest { dto.getLinks().getLinkBy("permissions").get().getHref()); } + @Test + public void shouldCreateCorrectProtocolLinks() { + when(repositoryService.getSupportedProtocols()).thenReturn( + of(mockProtocol("http", "http://scm"), mockProtocol("other", "some://protocol")) + ); + + RepositoryDto dto = mapper.map(createTestRepository()); + assertTrue("should contain http link", dto.getLinks().stream().anyMatch(l -> l.getName().equals("http") && l.getHref().equals("http://scm"))); + assertTrue("should contain other link", dto.getLinks().stream().anyMatch(l -> l.getName().equals("other") && l.getHref().equals("some://protocol"))); + } + + @Test + @SubjectAware(username = "community") + public void shouldCreateProtocolLinksForPullPermission() { + when(repositoryService.getSupportedProtocols()).thenReturn( + of(mockProtocol("http", "http://scm"), mockProtocol("other", "some://protocol")) + ); + + RepositoryDto dto = mapper.map(createTestRepository()); + assertEquals(2, dto.getLinks().getLinksBy("protocol").size()); + } + + @Test + @SubjectAware(username = "unpriv") + public void shouldNotCreateProtocolLinksWithoutPullPermission() { + when(repositoryService.getSupportedProtocols()).thenReturn( + of(mockProtocol("http", "http://scm"), mockProtocol("other", "some://protocol")) + ); + + RepositoryDto dto = mapper.map(createTestRepository()); + assertTrue(dto.getLinks().getLinksBy("protocol").isEmpty()); + } + + private ScmProtocol mockProtocol(String type, String protocol) { + return new MockScmProtocol(type, protocol); + } + private Repository createTestRepository() { Repository repository = new Repository(); repository.setNamespace("testspace"); @@ -179,4 +229,5 @@ public class RepositoryToRepositoryDtoMapperTest { return repository; } + } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryTypeRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryTypeRootResourceTest.java index 9adca13225..2476785d70 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryTypeRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/RepositoryTypeRootResourceTest.java @@ -2,6 +2,7 @@ package sonia.scm.api.v2.resources; import com.google.common.collect.Lists; import com.google.common.collect.Sets; +import com.google.inject.util.Providers; import org.jboss.resteasy.core.Dispatcher; import org.jboss.resteasy.mock.MockDispatcherFactory; import org.jboss.resteasy.mock.MockHttpRequest; @@ -22,8 +23,10 @@ import java.util.List; import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; import static javax.servlet.http.HttpServletResponse.SC_OK; -import static org.junit.Assert.*; -import static org.hamcrest.Matchers.*; +import static org.hamcrest.Matchers.equalToIgnoringCase; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.when; @RunWith(MockitoJUnitRunner.Silent.class) @@ -52,7 +55,7 @@ public class RepositoryTypeRootResourceTest { RepositoryTypeCollectionToDtoMapper collectionMapper = new RepositoryTypeCollectionToDtoMapper(mapper, resourceLinks); RepositoryTypeCollectionResource collectionResource = new RepositoryTypeCollectionResource(repositoryManager, collectionMapper); RepositoryTypeResource resource = new RepositoryTypeResource(repositoryManager, mapper); - RepositoryTypeRootResource rootResource = new RepositoryTypeRootResource(MockProvider.of(collectionResource), MockProvider.of(resource)); + RepositoryTypeRootResource rootResource = new RepositoryTypeRootResource(Providers.of(collectionResource), Providers.of(resource)); dispatcher.getRegistry().addSingletonResource(rootResource); } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java index f9ace9f8f6..eed46a1f6e 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksMock.java @@ -1,6 +1,5 @@ package sonia.scm.api.v2.resources; -import javax.ws.rs.core.UriInfo; import java.net.URI; import static org.mockito.Mockito.mock; @@ -10,10 +9,12 @@ public class ResourceLinksMock { public static ResourceLinks createMock(URI baseUri) { ResourceLinks resourceLinks = mock(ResourceLinks.class); - UriInfo uriInfo = mock(UriInfo.class); - when(uriInfo.getBaseUri()).thenReturn(baseUri); + ScmPathInfo uriInfo = mock(ScmPathInfo.class); + when(uriInfo.getApiRestUri()).thenReturn(baseUri); - when(resourceLinks.user()).thenReturn(new ResourceLinks.UserLinks(uriInfo)); + ResourceLinks.UserLinks userLinks = new ResourceLinks.UserLinks(uriInfo); + when(resourceLinks.user()).thenReturn(userLinks); + when(resourceLinks.me()).thenReturn(new ResourceLinks.MeLinks(uriInfo,userLinks)); when(resourceLinks.userCollection()).thenReturn(new ResourceLinks.UserCollectionLinks(uriInfo)); when(resourceLinks.group()).thenReturn(new ResourceLinks.GroupLinks(uriInfo)); when(resourceLinks.groupCollection()).thenReturn(new ResourceLinks.GroupCollectionLinks(uriInfo)); @@ -22,15 +23,19 @@ public class ResourceLinksMock { when(resourceLinks.tag()).thenReturn(new ResourceLinks.TagCollectionLinks(uriInfo)); when(resourceLinks.branchCollection()).thenReturn(new ResourceLinks.BranchCollectionLinks(uriInfo)); when(resourceLinks.changeset()).thenReturn(new ResourceLinks.ChangesetLinks(uriInfo)); + when(resourceLinks.fileHistory()).thenReturn(new ResourceLinks.FileHistoryLinks(uriInfo)); when(resourceLinks.source()).thenReturn(new ResourceLinks.SourceLinks(uriInfo)); when(resourceLinks.permission()).thenReturn(new ResourceLinks.PermissionLinks(uriInfo)); when(resourceLinks.config()).thenReturn(new ResourceLinks.ConfigLinks(uriInfo)); when(resourceLinks.branch()).thenReturn(new ResourceLinks.BranchLinks(uriInfo)); when(resourceLinks.diff()).thenReturn(new ResourceLinks.DiffLinks(uriInfo)); + when(resourceLinks.modifications()).thenReturn(new ResourceLinks.ModificationsLinks(uriInfo)); when(resourceLinks.repositoryType()).thenReturn(new ResourceLinks.RepositoryTypeLinks(uriInfo)); when(resourceLinks.repositoryTypeCollection()).thenReturn(new ResourceLinks.RepositoryTypeCollectionLinks(uriInfo)); when(resourceLinks.uiPluginCollection()).thenReturn(new ResourceLinks.UIPluginCollectionLinks(uriInfo)); when(resourceLinks.uiPlugin()).thenReturn(new ResourceLinks.UIPluginLinks(uriInfo)); + when(resourceLinks.authentication()).thenReturn(new ResourceLinks.AuthenticationLinks(uriInfo)); + when(resourceLinks.index()).thenReturn(new ResourceLinks.IndexLinks(uriInfo)); return resourceLinks; } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java index a9209828c3..0544bf6a0d 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ResourceLinksTest.java @@ -6,7 +6,6 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import sonia.scm.repository.NamespaceAndName; -import javax.ws.rs.core.UriInfo; import java.net.URI; import static org.junit.Assert.assertEquals; @@ -18,9 +17,9 @@ public class ResourceLinksTest { private static final String BASE_URL = "http://example.com/"; @Mock - private UriInfoStore uriInfoStore; + private ScmPathInfoStore scmPathInfoStore; @Mock - private UriInfo uriInfo; + private ScmPathInfo uriInfo; @InjectMocks private ResourceLinks resourceLinks; @@ -177,7 +176,7 @@ public class ResourceLinksTest { @Before public void initUriInfo() { initMocks(this); - when(uriInfoStore.get()).thenReturn(uriInfo); - when(uriInfo.getBaseUri()).thenReturn(URI.create(BASE_URL)); + when(scmPathInfoStore.get()).thenReturn(uriInfo); + when(uriInfo.getApiRestUri()).thenReturn(URI.create(BASE_URL)); } } diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ScmPathInfoStoreTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ScmPathInfoStoreTest.java new file mode 100644 index 0000000000..544a918b8b --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/ScmPathInfoStoreTest.java @@ -0,0 +1,35 @@ +package sonia.scm.api.v2.resources; + +import org.junit.Test; + +import java.net.URI; + +import static org.junit.Assert.assertSame; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class ScmPathInfoStoreTest { + + @Test + public void shouldReturnSetInfo() { + URI someUri = URI.create("/anything"); + + ScmPathInfo uriInfo = mock(ScmPathInfo.class); + ScmPathInfoStore scmPathInfoStore = new ScmPathInfoStore(); + + when(uriInfo.getApiRestUri()).thenReturn(someUri); + + scmPathInfoStore.set(uriInfo); + + assertSame(someUri, scmPathInfoStore.get().getApiRestUri()); + } + + @Test(expected = IllegalStateException.class) + public void shouldFailIfSetTwice() { + ScmPathInfo uriInfo = mock(ScmPathInfo.class); + ScmPathInfoStore scmPathInfoStore = new ScmPathInfoStore(); + + scmPathInfoStore.set(uriInfo); + scmPathInfoStore.set(uriInfo); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/SourceRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/SourceRootResourceTest.java index 2c4893b362..c84a74bc92 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/SourceRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/SourceRootResourceTest.java @@ -1,5 +1,6 @@ package sonia.scm.api.v2.resources; +import com.google.inject.util.Providers; import org.jboss.resteasy.core.Dispatcher; import org.jboss.resteasy.mock.MockHttpRequest; import org.jboss.resteasy.mock.MockHttpResponse; @@ -32,7 +33,7 @@ import static sonia.scm.api.v2.resources.DispatcherMock.createDispatcher; @RunWith(MockitoJUnitRunner.Silent.class) -public class SourceRootResourceTest { +public class SourceRootResourceTest extends RepositoryTestBase { private Dispatcher dispatcher; private final URI baseUri = URI.create("/"); @@ -63,19 +64,8 @@ public class SourceRootResourceTest { when(fileObjectToFileObjectDtoMapper.map(any(FileObject.class), any(NamespaceAndName.class), anyString())).thenReturn(dto); SourceRootResource sourceRootResource = new SourceRootResource(serviceFactory, browserResultToBrowserResultDtoMapper); - RepositoryRootResource repositoryRootResource = - new RepositoryRootResource(MockProvider.of(new RepositoryResource(null, - null, - null, - null, - null, - null, - MockProvider.of(sourceRootResource), - null, - null, - null)), - null); - dispatcher = createDispatcher(repositoryRootResource); + super.sourceRootResource = Providers.of(sourceRootResource); + dispatcher = createDispatcher(getRepositoryRootResource()); } @Test diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/TagRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/TagRootResourceTest.java index 92d11b3895..5f49f31183 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/TagRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/TagRootResourceTest.java @@ -1,5 +1,6 @@ package sonia.scm.api.v2.resources; +import com.google.inject.util.Providers; import lombok.extern.slf4j.Slf4j; import org.apache.shiro.subject.Subject; import org.apache.shiro.subject.support.SubjectThreadState; @@ -7,7 +8,6 @@ import org.apache.shiro.util.ThreadContext; import org.apache.shiro.util.ThreadState; import org.assertj.core.util.Lists; import org.jboss.resteasy.core.Dispatcher; -import org.jboss.resteasy.mock.MockDispatcherFactory; import org.jboss.resteasy.mock.MockHttpRequest; import org.jboss.resteasy.mock.MockHttpResponse; import org.junit.After; @@ -35,14 +35,15 @@ import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import static sonia.scm.api.v2.resources.DispatcherMock.createDispatcher; @Slf4j @RunWith(MockitoJUnitRunner.Silent.class) -public class TagRootResourceTest { +public class TagRootResourceTest extends RepositoryTestBase { public static final String TAG_PATH = "space/repo/tags/"; public static final String TAG_URL = "/" + RepositoryRootResource.REPOSITORIES_PATH_V2 + TAG_PATH; - private final Dispatcher dispatcher = MockDispatcherFactory.createDispatcher(); + private Dispatcher dispatcher ; private final URI baseUri = URI.create("/"); private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); @@ -55,7 +56,6 @@ public class TagRootResourceTest { @Mock private TagsCommandBuilder tagsCommandBuilder; - private TagCollectionToDtoMapper tagCollectionToDtoMapper; @InjectMocks @@ -72,10 +72,8 @@ public class TagRootResourceTest { public void prepareEnvironment() throws Exception { tagCollectionToDtoMapper = new TagCollectionToDtoMapper(resourceLinks, tagToTagDtoMapper); tagRootResource = new TagRootResource(serviceFactory, tagCollectionToDtoMapper, tagToTagDtoMapper); - RepositoryRootResource repositoryRootResource = new RepositoryRootResource(MockProvider - .of(new RepositoryResource(null, null, null, MockProvider.of(tagRootResource), null, - null, null, null, null, null)), null); - dispatcher.getRegistry().addSingletonResource(repositoryRootResource); + super.tagRootResource = Providers.of(tagRootResource); + dispatcher = createDispatcher(getRepositoryRootResource()); when(serviceFactory.create(new NamespaceAndName("space", "repo"))).thenReturn(repositoryService); when(serviceFactory.create(any(Repository.class))).thenReturn(repositoryService); when(repositoryService.getRepository()).thenReturn(new Repository("repoId", "git", "space", "repo")); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UriInfoStoreTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UriInfoStoreTest.java deleted file mode 100644 index 559e701ae3..0000000000 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UriInfoStoreTest.java +++ /dev/null @@ -1,30 +0,0 @@ -package sonia.scm.api.v2.resources; - -import org.junit.Test; - -import javax.ws.rs.core.UriInfo; - -import static org.junit.Assert.assertSame; -import static org.mockito.Mockito.mock; - -public class UriInfoStoreTest { - - @Test - public void shouldReturnSetInfo() { - UriInfo uriInfo = mock(UriInfo.class); - UriInfoStore uriInfoStore = new UriInfoStore(); - - uriInfoStore.set(uriInfo); - - assertSame(uriInfo, uriInfoStore.get()); - } - - @Test(expected = IllegalStateException.class) - public void shouldFailIfSetTwice() { - UriInfo uriInfo = mock(UriInfo.class); - UriInfoStore uriInfoStore = new UriInfoStore(); - - uriInfoStore.set(uriInfo); - uriInfoStore.set(uriInfo); - } -} diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserDtoToUserMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserDtoToUserMapperTest.java index f97fd91c3b..19f247b3b2 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserDtoToUserMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserDtoToUserMapperTest.java @@ -23,18 +23,9 @@ public class UserDtoToUserMapperTest { @Test public void shouldMapFields() { UserDto dto = createDefaultDto(); - User user = mapper.map(dto, "original password"); + User user = mapper.map(dto, "used password"); assertEquals("abc" , user.getName()); - } - - @Test - public void shouldEncodePassword() { - when(passwordService.encryptPassword("unencrypted")).thenReturn("encrypted"); - - UserDto dto = createDefaultDto(); - dto.setPassword("unencrypted"); - User user = mapper.map(dto, "original password"); - assertEquals("encrypted" , user.getPassword()); + assertEquals("used password" , user.getPassword()); } @Before diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java index 5004eb7665..6ab1dc6aeb 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserRootResourceTest.java @@ -3,6 +3,7 @@ package sonia.scm.api.v2.resources; import com.github.sdorra.shiro.ShiroRule; import com.github.sdorra.shiro.SubjectAware; import com.google.common.io.Resources; +import com.google.inject.util.Providers; import org.apache.shiro.authc.credential.PasswordService; import org.jboss.resteasy.core.Dispatcher; import org.jboss.resteasy.mock.MockHttpRequest; @@ -22,6 +23,7 @@ import javax.servlet.http.HttpServletResponse; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; +import java.text.MessageFormat; import static java.util.Collections.singletonList; import static org.junit.Assert.assertEquals; @@ -60,21 +62,25 @@ public class UserRootResourceTest { private UserToUserDtoMapperImpl userToDtoMapper; private ArgumentCaptor userCaptor = ArgumentCaptor.forClass(User.class); + private User originalUser; @Before public void prepareEnvironment() throws Exception { initMocks(this); - User dummyUser = createDummyUser("Neo"); + originalUser = createDummyUser("Neo"); when(userManager.create(userCaptor.capture())).thenAnswer(invocation -> invocation.getArguments()[0]); + when(userManager.isTypeDefault(userCaptor.capture())).thenCallRealMethod(); + when(userManager.getUserTypeChecker()).thenCallRealMethod(); doNothing().when(userManager).modify(userCaptor.capture()); doNothing().when(userManager).delete(userCaptor.capture()); + when(userManager.getDefaultType()).thenReturn("xml"); UserCollectionToDtoMapper userCollectionToDtoMapper = new UserCollectionToDtoMapper(userToDtoMapper, resourceLinks); UserCollectionResource userCollectionResource = new UserCollectionResource(userManager, dtoToUserMapper, - userCollectionToDtoMapper, resourceLinks); - UserResource userResource = new UserResource(dtoToUserMapper, userToDtoMapper, userManager); - UserRootResource userRootResource = new UserRootResource(MockProvider.of(userCollectionResource), - MockProvider.of(userResource)); + userCollectionToDtoMapper, resourceLinks, passwordService); + UserResource userResource = new UserResource(dtoToUserMapper, userToDtoMapper, userManager, passwordService); + UserRootResource userRootResource = new UserRootResource(Providers.of(userCollectionResource), + Providers.of(userResource)); dispatcher = createDispatcher(userRootResource); } @@ -88,11 +94,36 @@ public class UserRootResourceTest { assertEquals(HttpServletResponse.SC_OK, response.getStatus()); assertTrue(response.getContentAsString().contains("\"name\":\"Neo\"")); - assertTrue(response.getContentAsString().contains("\"password\":\"__dummypassword__\"")); assertTrue(response.getContentAsString().contains("\"self\":{\"href\":\"/v2/users/Neo\"}")); assertTrue(response.getContentAsString().contains("\"delete\":{\"href\":\"/v2/users/Neo\"}")); } + @Test + public void shouldGet400OnCreatingNewUserWithNotAllowedCharacters() throws URISyntaxException { + // the @ character at the begin of the name is not allowed + String userJson = "{ \"name\": \"@user\", \"type\": \"db\" }"; + MockHttpRequest request = MockHttpRequest + .post("/" + UserRootResource.USERS_PATH_V2) + .contentType(VndMediaType.USER) + .content(userJson.getBytes()); + MockHttpResponse response = new MockHttpResponse(); + + dispatcher.invoke(request, response); + + assertEquals(400, response.getStatus()); + + // the whitespace at the begin opf the name is not allowed + userJson = "{ \"name\": \" user\", \"type\": \"db\" }"; + request = MockHttpRequest + .post("/" + UserRootResource.USERS_PATH_V2) + .contentType(VndMediaType.USER) + .content(userJson.getBytes()); + + dispatcher.invoke(request, response); + + assertEquals(400, response.getStatus()); + } + @Test @SubjectAware(username = "unpriv") public void shouldCreateLimitedResponseForSimpleUser() throws URISyntaxException { @@ -103,13 +134,48 @@ public class UserRootResourceTest { assertEquals(HttpServletResponse.SC_OK, response.getStatus()); assertTrue(response.getContentAsString().contains("\"name\":\"Neo\"")); - assertTrue(response.getContentAsString().contains("\"password\":\"__dummypassword__\"")); assertTrue(response.getContentAsString().contains("\"self\":{\"href\":\"/v2/users/Neo\"}")); assertFalse(response.getContentAsString().contains("\"delete\":{\"href\":\"/v2/users/Neo\"}")); } @Test - public void shouldCreateNewUserWithEncryptedPassword() throws Exception { + public void shouldEncryptPasswordBeforeChanging() throws Exception { + String newPassword = "pwd123"; + String content = String.format("{\"newPassword\": \"%s\"}", newPassword); + MockHttpRequest request = MockHttpRequest + .put("/" + UserRootResource.USERS_PATH_V2 + "Neo/password") + .contentType(VndMediaType.PASSWORD_CHANGE) + .content(content.getBytes()); + MockHttpResponse response = new MockHttpResponse(); + when(passwordService.encryptPassword(newPassword)).thenReturn("encrypted123"); + + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus()); + verify(userManager).modify(any(User.class)); + User updatedUser = userCaptor.getValue(); + assertEquals("encrypted123", updatedUser.getPassword()); + } + + @Test + public void shouldGet400OnChangePasswordOfUserWithNonDefaultType() throws Exception { + originalUser.setType("not an xml type"); + String newPassword = "pwd123"; + String content = String.format("{\"newPassword\": \"%s\"}", newPassword); + MockHttpRequest request = MockHttpRequest + .put("/" + UserRootResource.USERS_PATH_V2 + "Neo/password") + .contentType(VndMediaType.PASSWORD_CHANGE) + .content(content.getBytes()); + MockHttpResponse response = new MockHttpResponse(); + when(passwordService.encryptPassword(newPassword)).thenReturn("encrypted123"); + + dispatcher.invoke(request, response); + + assertEquals(HttpServletResponse.SC_BAD_REQUEST, response.getStatus()); + } + + @Test + public void shouldEncryptPasswordBeforeCreatingUser() throws Exception { URL url = Resources.getResource("sonia/scm/api/v2/user-test-create.json"); byte[] userJson = Resources.toByteArray(url); @@ -129,7 +195,7 @@ public class UserRootResourceTest { } @Test - public void shouldUpdateChangedUserWithEncryptedPassword() throws Exception { + public void shouldIgnoreGivenPasswordOnUpdatingUser() throws Exception { URL url = Resources.getResource("sonia/scm/api/v2/user-test-update.json"); byte[] userJson = Resources.toByteArray(url); @@ -138,14 +204,13 @@ public class UserRootResourceTest { .contentType(VndMediaType.USER) .content(userJson); MockHttpResponse response = new MockHttpResponse(); - when(passwordService.encryptPassword("pwd123")).thenReturn("encrypted123"); dispatcher.invoke(request, response); assertEquals(HttpServletResponse.SC_NO_CONTENT, response.getStatus()); verify(userManager).modify(any(User.class)); User updatedUser = userCaptor.getValue(); - assertEquals("encrypted123", updatedUser.getPassword()); + assertEquals(originalUser.getPassword(), updatedUser.getPassword()); } @Test @@ -153,7 +218,7 @@ public class UserRootResourceTest { MockHttpRequest request = MockHttpRequest .post("/" + UserRootResource.USERS_PATH_V2) .contentType(VndMediaType.USER) - .content(new byte[] {}); + .content(new byte[]{}); MockHttpResponse response = new MockHttpResponse(); when(passwordService.encryptPassword("pwd123")).thenReturn("encrypted123"); @@ -264,6 +329,7 @@ public class UserRootResourceTest { private User createDummyUser(String name) { User user = new User(); user.setName(name); + user.setType("xml"); user.setPassword("redpill"); user.setCreationDate(System.currentTimeMillis()); when(userManager.get(name)).thenReturn(user); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserToUserDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserToUserDtoMapperTest.java index 330dd2a89e..7570a3f162 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserToUserDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/UserToUserDtoMapperTest.java @@ -8,14 +8,17 @@ import org.junit.After; import org.junit.Before; import org.junit.Test; import org.mockito.InjectMocks; -import sonia.scm.api.rest.resources.UserResource; +import org.mockito.Mock; import sonia.scm.user.User; +import sonia.scm.user.UserManager; import java.net.URI; import java.time.Instant; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.initMocks; @@ -26,6 +29,9 @@ public class UserToUserDtoMapperTest { @SuppressWarnings("unused") // Is injected private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(baseUri); + @Mock + private UserManager userManager; + @InjectMocks private UserToUserDtoMapperImpl mapper; @@ -37,6 +43,7 @@ public class UserToUserDtoMapperTest { @Before public void init() { initMocks(this); + when(userManager.getDefaultType()).thenReturn("xml"); expectedBaseUri = baseUri.resolve(UserRootResource.USERS_PATH_V2 + "/"); subjectThreadState.bind(); ThreadContext.bind(subject); @@ -53,11 +60,42 @@ public class UserToUserDtoMapperTest { when(subject.isPermitted("user:modify:abc")).thenReturn(true); UserDto userDto = mapper.map(user); - - assertEquals("expected self link", expectedBaseUri.resolve("abc").toString(), userDto.getLinks().getLinkBy("self").get().getHref()); + assertEquals("expected self link", expectedBaseUri.resolve("abc").toString(), userDto.getLinks().getLinkBy("self").get().getHref()); assertEquals("expected update link", expectedBaseUri.resolve("abc").toString(), userDto.getLinks().getLinkBy("update").get().getHref()); } + @Test + public void shouldGetPasswordLinkOnlyForDefaultUserType() { + User user = createDefaultUser(); + when(subject.isPermitted("user:modify:abc")).thenReturn(true); + when(userManager.isTypeDefault(eq(user))).thenReturn(true); + + UserDto userDto = mapper.map(user); + + assertEquals("expected password link with modify permission", expectedBaseUri.resolve("abc/password").toString(), userDto.getLinks().getLinkBy("password").get().getHref()); + + when(subject.isPermitted("user:modify:abc")).thenReturn(false); + userDto = mapper.map(user); + assertEquals("expected password link on mission modify permission", expectedBaseUri.resolve("abc/password").toString(), userDto.getLinks().getLinkBy("password").get().getHref()); + + when(userManager.isTypeDefault(eq(user))).thenReturn(false); + + userDto = mapper.map(user); + + assertFalse("expected no password link", userDto.getLinks().getLinkBy("password").isPresent()); + } + + @Test + public void shouldGetEmptyPasswordProperty() { + User user = createDefaultUser(); + user.setPassword("myHighSecurePassword"); + when(subject.isPermitted("user:modify:abc")).thenReturn(true); + + UserDto userDto = mapper.map(user); + + assertThat(userDto.getPassword()).isBlank(); + } + @Test public void shouldMapLinks_forDelete() { User user = createDefaultUser(); @@ -65,7 +103,7 @@ public class UserToUserDtoMapperTest { UserDto userDto = mapper.map(user); - assertEquals("expected self link", expectedBaseUri.resolve("abc").toString(), userDto.getLinks().getLinkBy("self").get().getHref()); + assertEquals("expected self link", expectedBaseUri.resolve("abc").toString(), userDto.getLinks().getLinkBy("self").get().getHref()); assertEquals("expected delete link", expectedBaseUri.resolve("abc").toString(), userDto.getLinks().getLinkBy("delete").get().getHref()); } @@ -97,16 +135,6 @@ public class UserToUserDtoMapperTest { assertEquals("abc", userDto.getName()); } - @Test - public void shouldRemovePassword() { - User user = createDefaultUser(); - user.setPassword("password"); - - UserDto userDto = mapper.map(user); - - assertEquals(UserResource.DUMMY_PASSWORT, userDto.getPassword()); - } - @Test public void shouldMapTimes() { User user = createDefaultUser(); diff --git a/scm-webapp/src/test/java/sonia/scm/it/GitLfsITCase.java b/scm-webapp/src/test/java/sonia/scm/it/GitLfsITCase.java index 2452afb909..c55a33c39a 100644 --- a/scm-webapp/src/test/java/sonia/scm/it/GitLfsITCase.java +++ b/scm-webapp/src/test/java/sonia/scm/it/GitLfsITCase.java @@ -36,33 +36,38 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.type.TypeFactory; import com.fasterxml.jackson.module.jaxb.JaxbAnnotationIntrospector; import com.google.common.base.Charsets; +import com.sun.jersey.api.client.ClientResponse; import com.sun.jersey.api.client.UniformInterfaceException; import org.apache.shiro.crypto.hash.Sha256Hash; import org.hamcrest.Matchers; import org.junit.After; import org.junit.Before; -import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; import org.junit.rules.TemporaryFolder; import sonia.scm.api.rest.ObjectMapperProvider; import sonia.scm.api.v2.resources.RepositoryDto; +import sonia.scm.api.v2.resources.UserDto; +import sonia.scm.api.v2.resources.UserToUserDtoMapperImpl; import sonia.scm.repository.PermissionType; -import sonia.scm.repository.Repository; import sonia.scm.user.User; import sonia.scm.user.UserTestData; +import sonia.scm.util.HttpUtil; +import sonia.scm.web.VndMediaType; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlRootElement; import java.io.IOException; +import java.net.URI; import java.util.UUID; import static org.junit.Assert.assertArrayEquals; import static sonia.scm.it.IntegrationTestUtil.BASE_URL; import static sonia.scm.it.IntegrationTestUtil.REST_BASE_URL; import static sonia.scm.it.IntegrationTestUtil.createAdminClient; +import static sonia.scm.it.IntegrationTestUtil.createResource; import static sonia.scm.it.IntegrationTestUtil.readJson; import static sonia.scm.it.RepositoryITUtil.createRepository; import static sonia.scm.it.RepositoryITUtil.deleteRepository; @@ -111,7 +116,6 @@ public class GitLfsITCase { } @Test - @Ignore("permissions not yet implemented") public void testLfsAPIWithOwnerPermissions() throws IOException { uploadAndDownloadAsUser(PermissionType.OWNER); } @@ -122,9 +126,11 @@ public class GitLfsITCase { createUser(trillian); try { - // TODO enable when permissions are implemented in v2 -// repository.getPermissions().add(new Permission(trillian.getId(), permissionType)); -// modifyRepository(repository); + String permissionsUrl = repository.getLinks().getLinkBy("permissions").get().getHref(); + IntegrationTestUtil.createResource(adminClient, URI.create(permissionsUrl)) + .accept("*/*") + .type(VndMediaType.PERMISSION) + .post(ClientResponse.class, "{\"name\": \""+ trillian.getId() +"\", \"type\":\"WRITE\"}"); ScmClient client = new ScmClient(trillian.getId(), "secret123"); @@ -135,25 +141,27 @@ public class GitLfsITCase { } @Test - @Ignore("permissions not yet implemented") public void testLfsAPIWithWritePermissions() throws IOException { uploadAndDownloadAsUser(PermissionType.WRITE); } private void createUser(User user) { - adminClient.resource(REST_BASE_URL + "users.json").post(user); - } - - private void modifyRepository(Repository repository) { - adminClient.resource(REST_BASE_URL + "repositories/" + repository.getId() + ".json").put(repository); + UserDto dto = new UserToUserDtoMapperImpl(){ + @Override + protected void appendLinks(User user, UserDto target) {} + }.map(user); + dto.setPassword(user.getPassword()); + createResource(adminClient, "users") + .accept("*/*") + .type(VndMediaType.USER) + .post(ClientResponse.class, dto); } private void removeUser(User user) { - adminClient.resource(REST_BASE_URL + "users/" + user.getId() + ".json").delete(); + adminClient.resource(REST_BASE_URL + "users/" + user.getId()).delete(); } @Test - @Ignore("permissions not yet implemented") public void testLfsAPIWithoutWritePermissions() throws IOException { User trillian = UserTestData.createTrillian(); trillian.setPassword("secret123"); @@ -164,9 +172,11 @@ public class GitLfsITCase { try { - // TODO enable when permissions are implemented in v2 -// repository.getPermissions().add(new Permission(trillian.getId(), PermissionType.READ)); -// modifyRepository(repository); + String permissionsUrl = repository.getLinks().getLinkBy("permissions").get().getHref(); + IntegrationTestUtil.createResource(adminClient, URI.create(permissionsUrl)) + .accept("*/*") + .type(VndMediaType.PERMISSION) + .post(ClientResponse.class, "{\"name\": \""+ trillian.getId() +"\", \"type\":\"READ\"}"); ScmClient client = new ScmClient(trillian.getId(), "secret123"); uploadAndDownload(client); @@ -176,7 +186,6 @@ public class GitLfsITCase { } @Test - @Ignore("permissions not yet implemented") public void testLfsDownloadWithReadPermissions() throws IOException { User trillian = UserTestData.createTrillian(); trillian.setPassword("secret123"); @@ -184,9 +193,11 @@ public class GitLfsITCase { try { - // TODO enable when permissions are implemented in v2 -// repository.getPermissions().add(new Permission(trillian.getId(), PermissionType.READ)); -// modifyRepository(repository); + String permissionsUrl = repository.getLinks().getLinkBy("permissions").get().getHref(); + IntegrationTestUtil.createResource(adminClient, URI.create(permissionsUrl)) + .accept("*/*") + .type(VndMediaType.PERMISSION) + .post(ClientResponse.class, "{\"name\": \""+ trillian.getId() +"\", \"type\":\"READ\"}"); // upload data as admin String data = UUID.randomUUID().toString(); @@ -221,7 +232,7 @@ public class GitLfsITCase { LfsResponseBody response = request(client, request); String uploadURL = response.objects[0].actions.upload.href; - client.resource(uploadURL).put(data); + client.resource(uploadURL).header(HttpUtil.HEADER_USERAGENT, "git-lfs/z").put(data); return lfsObject; } @@ -233,14 +244,14 @@ public class GitLfsITCase { String json = client .resource(batchUrl) .accept("application/vnd.git-lfs+json") + .header(HttpUtil.HEADER_USERAGENT, "git-lfs/z") .header("Content-Type", "application/vnd.git-lfs+json") .post(String.class, requestAsString); return new ObjectMapperProvider().get().readValue(json, LfsResponseBody.class); } private String createBatchUrl() { - String url = BASE_URL + "git/" + repository.getNamespace() + "/" + repository.getName(); - return url + "/info/lfs/objects/batch"; + return String.format("%srepo/%s/%s/info/lfs/objects/batch", BASE_URL, repository.getNamespace(), repository.getName()); } private byte[] download(ScmClient client, LfsObject lfsObject) throws IOException { @@ -248,7 +259,7 @@ public class GitLfsITCase { LfsResponseBody response = request(client, request); String downloadUrl = response.objects[0].actions.download.href; - return client.resource(downloadUrl).get(byte[].class); + return client.resource(downloadUrl).header(HttpUtil.HEADER_USERAGENT, "git-lfs/z").get(byte[].class); } private LfsObject createLfsObject(byte[] data) { diff --git a/scm-webapp/src/test/java/sonia/scm/it/GitRepositoryPathMatcherITCase.java b/scm-webapp/src/test/java/sonia/scm/it/GitRepositoryPathMatcherITCase.java index 13cae15907..9e75857f08 100644 --- a/scm-webapp/src/test/java/sonia/scm/it/GitRepositoryPathMatcherITCase.java +++ b/scm-webapp/src/test/java/sonia/scm/it/GitRepositoryPathMatcherITCase.java @@ -100,7 +100,7 @@ public class GitRepositoryPathMatcherITCase { // tests end private String createUrl() { - return BASE_URL + "git/" + repository.getNamespace() + "/" + repository.getName(); + return BASE_URL + "repo/" + repository.getNamespace() + "/" + repository.getName(); } private void cloneAndPush( String url ) throws IOException { diff --git a/scm-webapp/src/test/java/sonia/scm/it/RepositoryHookITCase.java b/scm-webapp/src/test/java/sonia/scm/it/RepositoryHookITCase.java index dae80cd00d..d74769bf39 100644 --- a/scm-webapp/src/test/java/sonia/scm/it/RepositoryHookITCase.java +++ b/scm-webapp/src/test/java/sonia/scm/it/RepositoryHookITCase.java @@ -172,7 +172,7 @@ public class RepositoryHookITCase extends AbstractAdminITCaseBase Thread.sleep(WAIT_TIME); // check debug servlet that only one commit is present - WebResource.Builder wr = createResource(client, "../debug/" + repository.getNamespace() + "/" + repository.getName() + "/post-receive/last"); + WebResource.Builder wr = createResource(client, String.format("../debug/%s/%s/post-receive/last", repository.getNamespace(), repository.getName())); DebugHookData data = wr.get(DebugHookData.class); assertNotNull(data); assertThat(data.getChangesets(), allOf( @@ -195,8 +195,8 @@ public class RepositoryHookITCase extends AbstractAdminITCaseBase private RepositoryClient createRepositoryClient() throws IOException { - return REPOSITORY_CLIENT_FACTORY.create(repositoryType, - IntegrationTestUtil.BASE_URL + repositoryType + "/" + repository.getNamespace() + "/" + repository.getName(), + return REPOSITORY_CLIENT_FACTORY.create(repositoryType, + String.format("%srepo/%s/%s", IntegrationTestUtil.BASE_URL, repository.getNamespace(), repository.getName()), IntegrationTestUtil.ADMIN_USERNAME, IntegrationTestUtil.ADMIN_PASSWORD, workingCopy ); } diff --git a/scm-webapp/src/test/java/sonia/scm/it/UserPermissionITCase.java b/scm-webapp/src/test/java/sonia/scm/it/UserPermissionITCase.java index dc960c1b42..3c0e2eed52 100644 --- a/scm-webapp/src/test/java/sonia/scm/it/UserPermissionITCase.java +++ b/scm-webapp/src/test/java/sonia/scm/it/UserPermissionITCase.java @@ -37,6 +37,7 @@ package sonia.scm.it; import com.sun.jersey.api.client.ClientResponse; import de.otto.edison.hal.HalRepresentation; +import org.junit.Assume; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import sonia.scm.api.rest.ObjectMapperProvider; @@ -158,6 +159,7 @@ public class UserPermissionITCase extends AbstractPermissionITCaseBase @Override protected void checkGetAllResponse(ClientResponse response) { + Assume.assumeTrue(credentials.getUsername() == null); if (!credentials.isAnonymous()) { assertNotNull(response); diff --git a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerPerfTest.java b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerPerfTest.java index 5f4ea2fa72..448c2561f3 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerPerfTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerPerfTest.java @@ -54,7 +54,6 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import sonia.scm.SCMContextProvider; -import sonia.scm.Type; import sonia.scm.cache.GuavaCacheManager; import sonia.scm.config.ScmConfiguration; import sonia.scm.security.AuthorizationCollector; @@ -120,7 +119,6 @@ public class DefaultRepositoryManagerPerfTest { keyGenerator, repositoryDAO, handlerSet, - repositoryMatcher, namespaceStrategy ); diff --git a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java index efd3f673b0..a67c275bc0 100644 --- a/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/repository/DefaultRepositoryManagerTest.java @@ -38,6 +38,7 @@ import com.github.sdorra.shiro.SubjectAware; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; import org.apache.shiro.authz.UnauthorizedException; +import org.apache.shiro.util.ThreadContext; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -61,7 +62,6 @@ import sonia.scm.store.ConfigurationStoreFactory; import sonia.scm.store.JAXBConfigurationStoreFactory; import java.util.Collection; -import java.util.Collections; import java.util.HashSet; import java.util.Set; import java.util.Stack; @@ -95,6 +95,10 @@ import static org.mockito.Mockito.when; ) public class DefaultRepositoryManagerTest extends ManagerTestBase { + { + ThreadContext.unbindSubject(); + } + @Rule public ShiroRule shiro = new ShiroRule(); @@ -383,69 +387,6 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase { assertEquals("default_namespace", repository.getNamespace()); } - @Test - public void getRepositoryFromRequestUri_withoutLeadingSlash() throws AlreadyExistsException { - RepositoryManager m = createManager(); - m.init(contextProvider); - - createUriTestRepositories(m); - - assertEquals("scm-test", m.getFromUri("hg/namespace/scm-test").getName()); - assertEquals("namespace", m.getFromUri("hg/namespace/scm-test").getNamespace()); - } - - @Test - public void getRepositoryFromRequestUri_withLeadingSlash() throws AlreadyExistsException { - RepositoryManager m = createManager(); - m.init(contextProvider); - - createUriTestRepositories(m); - - assertEquals("scm-test", m.getFromUri("/hg/namespace/scm-test").getName()); - assertEquals("namespace", m.getFromUri("/hg/namespace/scm-test").getNamespace()); - } - - @Test - public void getRepositoryFromRequestUri_withPartialName() throws AlreadyExistsException { - RepositoryManager m = createManager(); - m.init(contextProvider); - - createUriTestRepositories(m); - - assertEquals("scm", m.getFromUri("hg/namespace/scm").getName()); - assertEquals("namespace", m.getFromUri("hg/namespace/scm").getNamespace()); - } - - @Test - public void getRepositoryFromRequestUri_withTrailingFilePath() throws AlreadyExistsException { - RepositoryManager m = createManager(); - m.init(contextProvider); - - createUriTestRepositories(m); - - assertEquals("test-1", m.getFromUri("/git/namespace/test-1/ka/some/path").getName()); - } - - @Test - public void getRepositoryFromRequestUri_forNotExistingRepositoryName() throws AlreadyExistsException { - RepositoryManager m = createManager(); - m.init(contextProvider); - - createUriTestRepositories(m); - - assertNull(m.getFromUri("/git/namespace/test-3/ka/some/path")); - } - - @Test - public void getRepositoryFromRequestUri_forWrongNamespace() throws AlreadyExistsException { - RepositoryManager m = createManager(); - m.init(contextProvider); - - createUriTestRepositories(m); - - assertNull(m.getFromUri("/git/other/other/test-2")); - } - @Test public void shouldSetNamespace() throws AlreadyExistsException { Repository repository = new Repository(null, "hg", null, "scm"); @@ -504,7 +445,7 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase { when(namespaceStrategy.createNamespace(Mockito.any(Repository.class))).thenAnswer(invocation -> mockedNamespace); return new DefaultRepositoryManager(configuration, contextProvider, - keyGenerator, repositoryDAO, handlerSet, createRepositoryMatcher(), namespaceStrategy); + keyGenerator, repositoryDAO, handlerSet, namespaceStrategy); } private void createRepository(RepositoryManager m, Repository repository) throws AlreadyExistsException { @@ -530,10 +471,6 @@ public class DefaultRepositoryManagerTest extends ManagerTestBase { assertEquals(repo.getLastModified(), other.getLastModified()); } - private RepositoryMatcher createRepositoryMatcher() { - return new RepositoryMatcher(Collections.emptySet()); - } - private Repository createRepository(Repository repository) throws AlreadyExistsException { manager.create(repository); assertNotNull(repository.getId()); diff --git a/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java b/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java index ed5689c9ce..5e7963ef1d 100644 --- a/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java +++ b/scm-webapp/src/test/java/sonia/scm/security/DefaultAuthorizationCollectorTest.java @@ -57,6 +57,7 @@ import sonia.scm.repository.RepositoryTestData; import sonia.scm.user.User; import sonia.scm.user.UserTestData; +import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.nullValue; @@ -160,7 +161,8 @@ public class DefaultAuthorizationCollectorTest { AuthorizationInfo authInfo = collector.collect(); assertThat(authInfo.getRoles(), Matchers.contains(Role.USER)); - assertThat(authInfo.getStringPermissions(), hasSize(0)); + assertThat(authInfo.getStringPermissions(), hasSize(1)); + assertThat(authInfo.getStringPermissions(), contains("user:read:trillian")); assertThat(authInfo.getObjectPermissions(), nullValue()); } @@ -207,7 +209,7 @@ public class DefaultAuthorizationCollectorTest { AuthorizationInfo authInfo = collector.collect(); assertThat(authInfo.getRoles(), Matchers.containsInAnyOrder(Role.USER)); assertThat(authInfo.getObjectPermissions(), nullValue()); - assertThat(authInfo.getStringPermissions(), containsInAnyOrder("repository:read,pull:one", "repository:read,pull,push:two")); + assertThat(authInfo.getStringPermissions(), containsInAnyOrder("repository:read,pull:one", "repository:read,pull,push:two", "user:read:trillian")); } /** @@ -228,7 +230,7 @@ public class DefaultAuthorizationCollectorTest { AuthorizationInfo authInfo = collector.collect(); assertThat(authInfo.getRoles(), Matchers.containsInAnyOrder(Role.USER)); assertThat(authInfo.getObjectPermissions(), nullValue()); - assertThat(authInfo.getStringPermissions(), containsInAnyOrder("one:one", "two:two")); + assertThat(authInfo.getStringPermissions(), containsInAnyOrder("one:one", "two:two", "user:read:trillian")); } private void authenticate(User user, String group, String... groups) { diff --git a/scm-webapp/src/test/java/sonia/scm/web/protocol/HttpProtocolServletTest.java b/scm-webapp/src/test/java/sonia/scm/web/protocol/HttpProtocolServletTest.java new file mode 100644 index 0000000000..077020f60c --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/web/protocol/HttpProtocolServletTest.java @@ -0,0 +1,114 @@ +package sonia.scm.web.protocol; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import sonia.scm.PushStateDispatcher; +import sonia.scm.repository.DefaultRepositoryProvider; +import sonia.scm.repository.NamespaceAndName; +import sonia.scm.repository.Repository; +import sonia.scm.repository.RepositoryNotFoundException; +import sonia.scm.repository.api.RepositoryService; +import sonia.scm.repository.api.RepositoryServiceFactory; +import sonia.scm.repository.spi.HttpScmProtocol; +import sonia.scm.web.UserAgent; +import sonia.scm.web.UserAgentParser; + +import javax.inject.Provider; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +import static org.mockito.AdditionalMatchers.not; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.MockitoAnnotations.initMocks; + +public class HttpProtocolServletTest { + + + @Mock + private RepositoryServiceFactory serviceFactory; + @Mock + private HttpServletRequest httpServletRequest; + @Mock + private PushStateDispatcher dispatcher; + @Mock + private UserAgentParser userAgentParser; + @Mock + private Provider requestProvider; + + @InjectMocks + private HttpProtocolServlet servlet; + + @Mock + private RepositoryService repositoryService; + @Mock + private UserAgent userAgent; + + @Mock + private HttpServletRequest request; + @Mock + private HttpServletResponse response; + @Mock + private HttpScmProtocol protocol; + + @Before + public void init() throws RepositoryNotFoundException { + initMocks(this); + when(userAgentParser.parse(request)).thenReturn(userAgent); + when(userAgent.isBrowser()).thenReturn(false); + NamespaceAndName existingRepo = new NamespaceAndName("space", "repo"); + when(serviceFactory.create(not(eq(existingRepo)))).thenThrow(RepositoryNotFoundException.class); + when(serviceFactory.create(existingRepo)).thenReturn(repositoryService); + when(requestProvider.get()).thenReturn(httpServletRequest); + } + + @Test + public void shouldDispatchBrowserRequests() throws ServletException, IOException { + when(userAgent.isBrowser()).thenReturn(true); + when(request.getRequestURI()).thenReturn("uri"); + + servlet.service(request, response); + + verify(dispatcher).dispatch(request, response, "uri"); + } + + @Test + public void shouldHandleBadPaths() throws IOException, ServletException { + when(request.getPathInfo()).thenReturn("/illegal"); + + servlet.service(request, response); + + verify(response).setStatus(400); + } + + @Test + public void shouldHandleNotExistingRepository() throws IOException, ServletException { + when(request.getPathInfo()).thenReturn("/not/exists"); + + servlet.service(request, response); + + verify(response).setStatus(404); + } + + @Test + public void shouldDelegateToProvider() throws RepositoryNotFoundException, IOException, ServletException { + when(request.getPathInfo()).thenReturn("/space/name"); + NamespaceAndName namespaceAndName = new NamespaceAndName("space", "name"); + doReturn(repositoryService).when(serviceFactory).create(namespaceAndName); + Repository repository = new Repository(); + when(repositoryService.getRepository()).thenReturn(repository); + when(repositoryService.getProtocol(HttpScmProtocol.class)).thenReturn(protocol); + + servlet.service(request, response); + + verify(httpServletRequest).setAttribute(DefaultRepositoryProvider.ATTRIBUTE_NAME, repository); + verify(protocol).serve(request, response, null); + verify(repositoryService).close(); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/web/protocol/NamespaceAndNameFromPathExtractorTest.java b/scm-webapp/src/test/java/sonia/scm/web/protocol/NamespaceAndNameFromPathExtractorTest.java new file mode 100644 index 0000000000..0998010069 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/web/protocol/NamespaceAndNameFromPathExtractorTest.java @@ -0,0 +1,68 @@ +package sonia.scm.web.protocol; + +import org.junit.jupiter.api.DynamicNode; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; +import sonia.scm.repository.NamespaceAndName; + +import java.util.Optional; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.DynamicTest.dynamicTest; + +public class NamespaceAndNameFromPathExtractorTest { + @TestFactory + Stream shouldExtractCorrectNamespaceAndName() { + return Stream.of( + "/space/repo", + "/space/repo/", + "/space/repo/here", + "/space/repo/here/there", + "space/repo", + "space/repo/", + "space/repo/here/there" + ).map(this::createCorrectTest); + } + + @TestFactory + Stream shouldHandleTrailingDotSomethings() { + return Stream.of( + "/space/repo.git", + "/space/repo.and.more", + "/space/repo." + ).map(this::createCorrectTest); + } + + private DynamicTest createCorrectTest(String path) { + return dynamicTest( + "should extract correct namespace and name for path " + path, + () -> { + Optional namespaceAndName = NamespaceAndNameFromPathExtractor.fromUri(path); + + assertThat(namespaceAndName.get()).isEqualTo(new NamespaceAndName("space", "repo")); + } + ); + } + + @TestFactory + Stream shouldHandleMissingParts() { + return Stream.of( + "", + "/", + "/space", + "/space/" + ).map(this::createFailureTest); + } + + private DynamicTest createFailureTest(String path) { + return dynamicTest( + "should not fail for wrong path " + path, + () -> { + Optional namespaceAndName = NamespaceAndNameFromPathExtractor.fromUri(path); + + assertThat(namespaceAndName.isPresent()).isFalse(); + } + ); + } +} diff --git a/scm-webapp/src/test/resources/sonia/scm/repository/shiro.ini b/scm-webapp/src/test/resources/sonia/scm/repository/shiro.ini index 5073bf398d..9a39a2d46c 100644 --- a/scm-webapp/src/test/resources/sonia/scm/repository/shiro.ini +++ b/scm-webapp/src/test/resources/sonia/scm/repository/shiro.ini @@ -3,9 +3,11 @@ trillian = secret, admin dent = secret, creator, heartOfGold, puzzle42 unpriv = secret crato = secret, creator +community = secret, oss [roles] admin = * creator = repository:create heartOfGold = "repository:read,modify,delete:hof" puzzle42 = "repository:read,write:p42" +oss = "repository:pull"