mirror of
https://github.com/goharbor/harbor.git
synced 2025-01-03 06:28:06 +01:00
Merge pull request #11436 from ywk253100/200404_repo_encode
Update APIs to only accept encoded repository name that contains slash
This commit is contained in:
commit
e1ba985b7c
@ -53,11 +53,7 @@ paths:
|
|||||||
$ref: '#/responses/404'
|
$ref: '#/responses/404'
|
||||||
'500':
|
'500':
|
||||||
$ref: '#/responses/500'
|
$ref: '#/responses/500'
|
||||||
# the _self suffix here is used to avoid the conflict of repository name and URL path
|
/projects/{project_name}/repositories/{repository_name}:
|
||||||
# e.g. the repository name can be "library/artifacts", we cannot distinguish the URL
|
|
||||||
# "GET /projects/{project_name}/repositories/library/artifacts" is getting repository
|
|
||||||
# or listing artifacts
|
|
||||||
/projects/{project_name}/repositories/{repository_name}/_self:
|
|
||||||
get:
|
get:
|
||||||
summary: Get repository
|
summary: Get repository
|
||||||
description: Get the repository specified by name
|
description: Get the repository specified by name
|
||||||
@ -671,7 +667,7 @@ parameters:
|
|||||||
repositoryName:
|
repositoryName:
|
||||||
name: repository_name
|
name: repository_name
|
||||||
in: path
|
in: path
|
||||||
description: The name of the repository
|
description: The name of the repository. If it contains slash, encode it with URL encoding. e.g. a/b -> a%252Fb
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
reference:
|
reference:
|
||||||
|
@ -16,12 +16,13 @@ package artifact
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/goharbor/harbor/src/server/v2.0/models"
|
"net/url"
|
||||||
|
|
||||||
cmodels "github.com/goharbor/harbor/src/common/models"
|
cmodels "github.com/goharbor/harbor/src/common/models"
|
||||||
"github.com/goharbor/harbor/src/common/utils"
|
"github.com/goharbor/harbor/src/common/utils"
|
||||||
"github.com/goharbor/harbor/src/controller/tag"
|
"github.com/goharbor/harbor/src/controller/tag"
|
||||||
"github.com/goharbor/harbor/src/pkg/artifact"
|
"github.com/goharbor/harbor/src/pkg/artifact"
|
||||||
|
"github.com/goharbor/harbor/src/server/v2.0/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Artifact is the overall view of artifact
|
// Artifact is the overall view of artifact
|
||||||
@ -39,6 +40,8 @@ func (artifact *Artifact) SetAdditionLink(addition, version string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
projectName, repo := utils.ParseRepository(artifact.RepositoryName)
|
projectName, repo := utils.ParseRepository(artifact.RepositoryName)
|
||||||
|
// encode slash as %252F
|
||||||
|
repo = url.PathEscape(url.PathEscape(repo))
|
||||||
href := fmt.Sprintf("/api/%s/projects/%s/repositories/%s/artifacts/%s/additions/%s", version, projectName, repo, artifact.Digest, addition)
|
href := fmt.Sprintf("/api/%s/projects/%s/repositories/%s/artifacts/%s/additions/%s", version, projectName, repo, artifact.Digest, addition)
|
||||||
|
|
||||||
artifact.AdditionLinks[addition] = &AdditionLink{HREF: href, Absolute: false}
|
artifact.AdditionLinks[addition] = &AdditionLink{HREF: href, Absolute: false}
|
||||||
|
@ -34,7 +34,7 @@ import {
|
|||||||
clone,
|
clone,
|
||||||
CustomComparator,
|
CustomComparator,
|
||||||
DEFAULT_PAGE_SIZE, DEFAULT_SUPPORTED_MIME_TYPE,
|
DEFAULT_PAGE_SIZE, DEFAULT_SUPPORTED_MIME_TYPE,
|
||||||
formatSize, VULNERABILITY_SCAN_STATUS
|
formatSize, VULNERABILITY_SCAN_STATUS, dbEncodeURIComponent
|
||||||
} from "../../../../../../lib/utils/utils";
|
} from "../../../../../../lib/utils/utils";
|
||||||
import {
|
import {
|
||||||
ConfirmationAcknowledgement,
|
ConfirmationAcknowledgement,
|
||||||
@ -335,7 +335,7 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
if (this.artifactDigest) {
|
if (this.artifactDigest) {
|
||||||
const artifactParam: NewArtifactService.GetArtifactParams = {
|
const artifactParam: NewArtifactService.GetArtifactParams = {
|
||||||
repositoryName: this.repoName,
|
repositoryName: dbEncodeURIComponent(this.repoName),
|
||||||
projectName: this.projectName,
|
projectName: this.projectName,
|
||||||
reference: this.artifactDigest,
|
reference: this.artifactDigest,
|
||||||
withImmutableStatus: true,
|
withImmutableStatus: true,
|
||||||
@ -351,7 +351,7 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
|||||||
res.references.forEach((child, index) => {
|
res.references.forEach((child, index) => {
|
||||||
if (index >= (pageNumber - 1) * this.pageSize && index < pageNumber * this.pageSize) {
|
if (index >= (pageNumber - 1) * this.pageSize && index < pageNumber * this.pageSize) {
|
||||||
let childParams: NewArtifactService.GetArtifactParams = {
|
let childParams: NewArtifactService.GetArtifactParams = {
|
||||||
repositoryName: this.repoName,
|
repositoryName: dbEncodeURIComponent(this.repoName),
|
||||||
projectName: this.projectName,
|
projectName: this.projectName,
|
||||||
reference: child.child_digest,
|
reference: child.child_digest,
|
||||||
withImmutableStatus: true,
|
withImmutableStatus: true,
|
||||||
@ -378,7 +378,7 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
|||||||
} else {
|
} else {
|
||||||
let listArtifactParams: NewArtifactService.ListArtifactsParams = {
|
let listArtifactParams: NewArtifactService.ListArtifactsParams = {
|
||||||
projectName: this.projectName,
|
projectName: this.projectName,
|
||||||
repositoryName: this.repoName,
|
repositoryName: dbEncodeURIComponent(this.repoName),
|
||||||
withLabel: true,
|
withLabel: true,
|
||||||
withScanOverview: true,
|
withScanOverview: true,
|
||||||
withTag: true
|
withTag: true
|
||||||
@ -480,7 +480,7 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
|||||||
this.selectedRow = this.selectedTag;
|
this.selectedRow = this.selectedTag;
|
||||||
let params: NewArtifactService.AddLabelParams = {
|
let params: NewArtifactService.AddLabelParams = {
|
||||||
projectName: this.projectName,
|
projectName: this.projectName,
|
||||||
repositoryName: this.repoName,
|
repositoryName: dbEncodeURIComponent(this.repoName),
|
||||||
reference: this.selectedRow[0].digest,
|
reference: this.selectedRow[0].digest,
|
||||||
label: labelInfo.label
|
label: labelInfo.label
|
||||||
};
|
};
|
||||||
@ -517,7 +517,7 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
|||||||
this.selectedRow = this.selectedTag;
|
this.selectedRow = this.selectedTag;
|
||||||
let params: NewArtifactService.RemoveLabelParams = {
|
let params: NewArtifactService.RemoveLabelParams = {
|
||||||
projectName: this.projectName,
|
projectName: this.projectName,
|
||||||
repositoryName: this.repoName,
|
repositoryName: dbEncodeURIComponent(this.repoName),
|
||||||
reference: this.selectedRow[0].digest,
|
reference: this.selectedRow[0].digest,
|
||||||
labelId: labelId
|
labelId: labelId
|
||||||
};
|
};
|
||||||
@ -675,18 +675,18 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
|||||||
onRetag() {
|
onRetag() {
|
||||||
let params: NewArtifactService.CopyArtifactParams = {
|
let params: NewArtifactService.CopyArtifactParams = {
|
||||||
projectName: this.imageNameInput.projectName.value,
|
projectName: this.imageNameInput.projectName.value,
|
||||||
repositoryName: this.imageNameInput.repoName.value,
|
repositoryName: dbEncodeURIComponent(this.imageNameInput.repoName.value),
|
||||||
from: `${this.projectName}/${this.repoName}@${this.selectedRow[0].digest}`,
|
from: `${this.projectName}/${this.repoName}@${this.selectedRow[0].digest}`,
|
||||||
};
|
};
|
||||||
this.newArtifactService.CopyArtifact(params)
|
this.newArtifactService.CopyArtifact(params)
|
||||||
.pipe(finalize(() => {
|
.pipe(finalize(() => {
|
||||||
this.retagDialogOpened = false;
|
|
||||||
this.imageNameInput.form.reset();
|
this.imageNameInput.form.reset();
|
||||||
}))
|
}))
|
||||||
.subscribe(response => {
|
.subscribe(response => {
|
||||||
this.translateService.get('RETAG.MSG_SUCCESS').subscribe((res: string) => {
|
this.translateService.get('RETAG.MSG_SUCCESS').subscribe((res: string) => {
|
||||||
this.errorHandlerService.info(res);
|
this.errorHandlerService.info(res);
|
||||||
});
|
});
|
||||||
|
this.retagDialogOpened = false;
|
||||||
}, error => {
|
}, error => {
|
||||||
this.errorHandlerService.error(error);
|
this.errorHandlerService.error(error);
|
||||||
});
|
});
|
||||||
@ -779,7 +779,7 @@ export class ArtifactListTabComponent implements OnInit, OnDestroy {
|
|||||||
// } else {
|
// } else {
|
||||||
let params: NewArtifactService.DeleteArtifactParams = {
|
let params: NewArtifactService.DeleteArtifactParams = {
|
||||||
projectName: this.projectName,
|
projectName: this.projectName,
|
||||||
repositoryName: this.repoName,
|
repositoryName: dbEncodeURIComponent(this.repoName),
|
||||||
reference: artifact.digest
|
reference: artifact.digest
|
||||||
};
|
};
|
||||||
return this.newArtifactService
|
return this.newArtifactService
|
||||||
|
@ -27,6 +27,7 @@ import { Project } from '../../../project';
|
|||||||
import {
|
import {
|
||||||
RepositoryService as NewRepositoryService
|
RepositoryService as NewRepositoryService
|
||||||
} from "../../../../../../ng-swagger-gen/services/repository.service";
|
} from "../../../../../../ng-swagger-gen/services/repository.service";
|
||||||
|
import { dbEncodeURIComponent } from '../../../../../lib/utils/utils';
|
||||||
const TabLinkContentMap: { [index: string]: string } = {
|
const TabLinkContentMap: { [index: string]: string } = {
|
||||||
'repo-info': 'info',
|
'repo-info': 'info',
|
||||||
'repo-image': 'image'
|
'repo-image': 'image'
|
||||||
@ -117,7 +118,7 @@ export class ArtifactListComponent implements OnInit {
|
|||||||
retrieve(state?: State) {
|
retrieve(state?: State) {
|
||||||
let params: NewRepositoryService.GetRepositoryParams = {
|
let params: NewRepositoryService.GetRepositoryParams = {
|
||||||
projectName: this.projectName,
|
projectName: this.projectName,
|
||||||
repositoryName: this.repoName
|
repositoryName: dbEncodeURIComponent(this.repoName),
|
||||||
};
|
};
|
||||||
this.newRepositoryService.getRepository(params)
|
this.newRepositoryService.getRepository(params)
|
||||||
.subscribe(response => {
|
.subscribe(response => {
|
||||||
@ -168,7 +169,7 @@ export class ArtifactListComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
this.onGoing = true;
|
this.onGoing = true;
|
||||||
let params: NewRepositoryService.UpdateRepositoryParams = {
|
let params: NewRepositoryService.UpdateRepositoryParams = {
|
||||||
repositoryName: this.repoName,
|
repositoryName: dbEncodeURIComponent(this.repoName),
|
||||||
repository: {description: this.imageInfo},
|
repository: {description: this.imageInfo},
|
||||||
projectName: this.projectName,
|
projectName: this.projectName,
|
||||||
};
|
};
|
||||||
|
@ -8,6 +8,7 @@ import { ActivatedRoute, Router } from "@angular/router";
|
|||||||
import { AppConfigService } from "../../../services/app-config.service";
|
import { AppConfigService } from "../../../services/app-config.service";
|
||||||
import { Project } from "../../project";
|
import { Project } from "../../project";
|
||||||
import { finalize } from "rxjs/operators";
|
import { finalize } from "rxjs/operators";
|
||||||
|
import { dbEncodeURIComponent } from "../../../../lib/utils/utils";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "artifact-summary",
|
selector: "artifact-summary",
|
||||||
@ -87,7 +88,7 @@ export class ArtifactSummaryComponent implements OnInit {
|
|||||||
getArtifactDetails(): void {
|
getArtifactDetails(): void {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
this.artifactService.getArtifact({
|
this.artifactService.getArtifact({
|
||||||
repositoryName: this.repositoryName,
|
repositoryName: dbEncodeURIComponent(this.repositoryName),
|
||||||
reference: this.artifactDigest,
|
reference: this.artifactDigest,
|
||||||
projectName: this.projectName,
|
projectName: this.projectName,
|
||||||
withLabel: true,
|
withLabel: true,
|
||||||
|
@ -21,7 +21,7 @@ import {
|
|||||||
UserPermissionService, USERSTATICPERMISSION
|
UserPermissionService, USERSTATICPERMISSION
|
||||||
} from "../../../../../lib/services";
|
} from "../../../../../lib/services";
|
||||||
import { ClrDatagridStateInterface } from '@clr/angular';
|
import { ClrDatagridStateInterface } from '@clr/angular';
|
||||||
import { DEFAULT_PAGE_SIZE, calculatePage } from '../../../../../lib/utils/utils';
|
import { DEFAULT_PAGE_SIZE, calculatePage, dbEncodeURIComponent } from '../../../../../lib/utils/utils';
|
||||||
|
|
||||||
class InitTag {
|
class InitTag {
|
||||||
name = "";
|
name = "";
|
||||||
@ -74,7 +74,7 @@ export class ArtifactTagComponent implements OnInit, OnDestroy {
|
|||||||
checkTagName(name) {
|
checkTagName(name) {
|
||||||
let listArtifactParams: ArtifactService.ListArtifactsParams = {
|
let listArtifactParams: ArtifactService.ListArtifactsParams = {
|
||||||
projectName: this.projectName,
|
projectName: this.projectName,
|
||||||
repositoryName: this.repositoryName,
|
repositoryName: dbEncodeURIComponent(this.repositoryName),
|
||||||
withLabel: true,
|
withLabel: true,
|
||||||
withScanOverview: true,
|
withScanOverview: true,
|
||||||
withTag: true,
|
withTag: true,
|
||||||
@ -111,7 +111,7 @@ export class ArtifactTagComponent implements OnInit, OnDestroy {
|
|||||||
if (pageNumber <= 0) { pageNumber = 1; }
|
if (pageNumber <= 0) { pageNumber = 1; }
|
||||||
let params: ArtifactService.ListTagsParams = {
|
let params: ArtifactService.ListTagsParams = {
|
||||||
projectName: this.projectName,
|
projectName: this.projectName,
|
||||||
repositoryName: this.repositoryName,
|
repositoryName: dbEncodeURIComponent(this.repositoryName),
|
||||||
reference: this.artifactDetails.digest,
|
reference: this.artifactDetails.digest,
|
||||||
page: pageNumber,
|
page: pageNumber,
|
||||||
withSignature: true,
|
withSignature: true,
|
||||||
@ -272,6 +272,8 @@ export class ArtifactTagComponent implements OnInit, OnDestroy {
|
|||||||
existValid(name) {
|
existValid(name) {
|
||||||
if (name) {
|
if (name) {
|
||||||
this.tagNameChecker.next(name);
|
this.tagNameChecker.next(name);
|
||||||
|
} else {
|
||||||
|
this.isTagNameExist = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
toggleTagListOpenOrClose() {
|
toggleTagListOpenOrClose() {
|
||||||
|
@ -24,7 +24,7 @@ import {
|
|||||||
UserPermissionService, USERSTATICPERMISSION
|
UserPermissionService, USERSTATICPERMISSION
|
||||||
} from "../../../lib/services";
|
} from "../../../lib/services";
|
||||||
import { FilterComponent } from "../../../lib/components/filter/filter.component";
|
import { FilterComponent } from "../../../lib/components/filter/filter.component";
|
||||||
import { calculatePage, clone, DEFAULT_PAGE_SIZE } from "../../../lib/utils/utils";
|
import { calculatePage, clone, DEFAULT_PAGE_SIZE, dbEncodeURIComponent } from "../../../lib/utils/utils";
|
||||||
import { IServiceConfig, SERVICE_CONFIG } from "../../../lib/entities/service.config";
|
import { IServiceConfig, SERVICE_CONFIG } from "../../../lib/entities/service.config";
|
||||||
import { ErrorHandler } from "../../../lib/utils/error-handler";
|
import { ErrorHandler } from "../../../lib/utils/error-handler";
|
||||||
import { ConfirmationButtons, ConfirmationState, ConfirmationTargets } from "../../../lib/entities/shared.const";
|
import { ConfirmationButtons, ConfirmationState, ConfirmationTargets } from "../../../lib/entities/shared.const";
|
||||||
@ -222,7 +222,7 @@ export class RepositoryGridviewComponent implements OnChanges, OnInit, OnDestroy
|
|||||||
this.operationService.publishInfo(operMessage);
|
this.operationService.publishInfo(operMessage);
|
||||||
return this.newRepoService
|
return this.newRepoService
|
||||||
.deleteRepository({
|
.deleteRepository({
|
||||||
repositoryName: repo.name,
|
repositoryName: dbEncodeURIComponent(repo.name),
|
||||||
projectName: this.projectName
|
projectName: this.projectName
|
||||||
})
|
})
|
||||||
.pipe(map(
|
.pipe(map(
|
||||||
|
@ -14,7 +14,8 @@ import {
|
|||||||
clone,
|
clone,
|
||||||
CURRENT_BASE_HREF,
|
CURRENT_BASE_HREF,
|
||||||
DEFAULT_SUPPORTED_MIME_TYPE,
|
DEFAULT_SUPPORTED_MIME_TYPE,
|
||||||
VULNERABILITY_SCAN_STATUS
|
VULNERABILITY_SCAN_STATUS,
|
||||||
|
dbEncodeURIComponent
|
||||||
} from "../../../../lib/utils/utils";
|
} from "../../../../lib/utils/utils";
|
||||||
import { ArtifactService } from "../../../../../ng-swagger-gen/services/artifact.service";
|
import { ArtifactService } from "../../../../../ng-swagger-gen/services/artifact.service";
|
||||||
import { Artifact } from "../../../../../ng-swagger-gen/models/artifact";
|
import { Artifact } from "../../../../../ng-swagger-gen/models/artifact";
|
||||||
@ -118,7 +119,7 @@ export class ResultBarChartComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
this.onSubmitting = true;
|
this.onSubmitting = true;
|
||||||
|
|
||||||
this.scanningService.startVulnerabilityScanning(this.projectName, this.repoName, this.artifactDigest)
|
this.scanningService.startVulnerabilityScanning(this.projectName, dbEncodeURIComponent(this.repoName), this.artifactDigest)
|
||||||
.pipe(finalize(() => this.submitFinish.emit(false)))
|
.pipe(finalize(() => this.submitFinish.emit(false)))
|
||||||
.subscribe(() => {
|
.subscribe(() => {
|
||||||
this.onSubmitting = false;
|
this.onSubmitting = false;
|
||||||
@ -149,7 +150,7 @@ export class ResultBarChartComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
this.artifactService.getArtifact({
|
this.artifactService.getArtifact({
|
||||||
projectName: this.projectName,
|
projectName: this.projectName,
|
||||||
repositoryName: this.repoName,
|
repositoryName: dbEncodeURIComponent(this.repoName),
|
||||||
reference: this.artifactDigest,
|
reference: this.artifactDigest,
|
||||||
withScanOverview: true
|
withScanOverview: true
|
||||||
})
|
})
|
||||||
|
@ -19,6 +19,7 @@ import { Artifact } from "../../../../ng-swagger-gen/models/artifact";
|
|||||||
import { ArtifactService } from "../../../../ng-swagger-gen/services/artifact.service";
|
import { ArtifactService } from "../../../../ng-swagger-gen/services/artifact.service";
|
||||||
import { Project } from "../../project/project";
|
import { Project } from "../../project/project";
|
||||||
import { ProjectService } from "../../../lib/services";
|
import { ProjectService } from "../../../lib/services";
|
||||||
|
import { dbEncodeURIComponent } from '../../../lib/utils/utils';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
@ -38,7 +39,7 @@ export class ArtifactDetailRoutingResolverService implements Resolve<Artifact> {
|
|||||||
.pipe(
|
.pipe(
|
||||||
mergeMap((project: Project) => {
|
mergeMap((project: Project) => {
|
||||||
return forkJoin([this.artifactService.getArtifact({
|
return forkJoin([this.artifactService.getArtifact({
|
||||||
repositoryName: repositoryName,
|
repositoryName: dbEncodeURIComponent(repositoryName),
|
||||||
reference: artifactDigest,
|
reference: artifactDigest,
|
||||||
projectName: project.name,
|
projectName: project.name,
|
||||||
withLabel: true,
|
withLabel: true,
|
||||||
|
@ -624,3 +624,9 @@ export function mergeDeep(target, ...sources) {
|
|||||||
}
|
}
|
||||||
return mergeDeep(target, ...sources);
|
return mergeDeep(target, ...sources);
|
||||||
}
|
}
|
||||||
|
export function dbEncodeURIComponent(url: string) {
|
||||||
|
if (typeof url === "string") {
|
||||||
|
return encodeURIComponent(encodeURIComponent(url));
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
@ -1,100 +0,0 @@
|
|||||||
// Copyright Project Harbor Authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package path
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"regexp"
|
|
||||||
|
|
||||||
"github.com/goharbor/harbor/src/common/api"
|
|
||||||
"github.com/goharbor/harbor/src/server/middleware"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
defaultRegexps = []*regexp.Regexp{
|
|
||||||
regexp.MustCompile(`^/api/` + api.APIVersion + `/projects/.*/repositories/(.*)/_self/?$`),
|
|
||||||
regexp.MustCompile(`^/api/` + api.APIVersion + `/projects/.*/repositories/(.*)/artifacts/?$`),
|
|
||||||
regexp.MustCompile(`^/api/` + api.APIVersion + `/projects/.*/repositories/(.*)/artifacts/.*$`),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// EscapeMiddleware middleware which escape path parameters for swagger APIs
|
|
||||||
func EscapeMiddleware() func(http.Handler) http.Handler {
|
|
||||||
return middleware.New(func(w http.ResponseWriter, r *http.Request, next http.Handler) {
|
|
||||||
for _, re := range defaultRegexps {
|
|
||||||
if re.MatchString(r.URL.Path) {
|
|
||||||
r.URL.Path = escape(re, r.URL.Path)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func escape(re *regexp.Regexp, path string) string {
|
|
||||||
return replaceAllSubmatchFunc(re, path, func(groups []string) []string {
|
|
||||||
var results []string
|
|
||||||
for _, group := range groups {
|
|
||||||
results = append(results, url.PathEscape(group))
|
|
||||||
}
|
|
||||||
return results
|
|
||||||
}, -1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func replaceAllSubmatchFunc(re *regexp.Regexp, src string, repl func([]string) []string, n int) string {
|
|
||||||
var result string
|
|
||||||
|
|
||||||
last := 0
|
|
||||||
for _, match := range re.FindAllSubmatchIndex([]byte(src), n) {
|
|
||||||
// Append string between our last match and this one (i.e. non-matched string).
|
|
||||||
matchStart := match[0]
|
|
||||||
matchEnd := match[1]
|
|
||||||
result = result + src[last:matchStart]
|
|
||||||
last = matchEnd
|
|
||||||
|
|
||||||
// Determine the groups / submatch string and indices.
|
|
||||||
groups := []string{}
|
|
||||||
indices := [][2]int{}
|
|
||||||
for i := 2; i < len(match); i += 2 {
|
|
||||||
start := match[i]
|
|
||||||
end := match[i+1]
|
|
||||||
groups = append(groups, src[start:end])
|
|
||||||
indices = append(indices, [2]int{start, end})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace the groups
|
|
||||||
groups = repl(groups)
|
|
||||||
|
|
||||||
// Append match data.
|
|
||||||
lastGroup := matchStart
|
|
||||||
for i, newValue := range groups {
|
|
||||||
// Append string between our last group match and this one (i.e. non-group-matched string)
|
|
||||||
groupStart := indices[i][0]
|
|
||||||
groupEnd := indices[i][1]
|
|
||||||
result = result + src[lastGroup:groupStart]
|
|
||||||
lastGroup = groupEnd
|
|
||||||
|
|
||||||
// Append the new group value.
|
|
||||||
result = result + newValue
|
|
||||||
}
|
|
||||||
result = result + src[lastGroup:matchEnd] // remaining
|
|
||||||
}
|
|
||||||
|
|
||||||
result = result + src[last:] // remaining
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
@ -1,84 +0,0 @@
|
|||||||
// Copyright Project Harbor Authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package path
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"regexp"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_escape(t *testing.T) {
|
|
||||||
re := regexp.MustCompile(`/api/v2.0/projects/.*/repositories/(.*)/artifacts`)
|
|
||||||
|
|
||||||
type args struct {
|
|
||||||
re *regexp.Regexp
|
|
||||||
path string
|
|
||||||
}
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
args args
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
"/api/v2.0/projects/library/repositories/photon/artifacts",
|
|
||||||
args{re, "/api/v2.0/projects/library/repositories/photon/artifacts"},
|
|
||||||
"/api/v2.0/projects/library/repositories/photon/artifacts",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"/api/v2.0/projects/library/repositories/photon/hello-world/artifacts",
|
|
||||||
args{re, "/api/v2.0/projects/library/repositories/photon/hello-world/artifacts"},
|
|
||||||
"/api/v2.0/projects/library/repositories/photon%2Fhello-world/artifacts",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"/api/v2.0/projects/library/repositories/photon/hello-world/artifacts/digest/scan",
|
|
||||||
args{re, "/api/v2.0/projects/library/repositories/photon/hello-world/artifacts/digest/scan"},
|
|
||||||
"/api/v2.0/projects/library/repositories/photon%2Fhello-world/artifacts/digest/scan",
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
"/api/v2.0/projects/library/repositories",
|
|
||||||
args{re, "/api/v2.0/projects/library/repositories"},
|
|
||||||
"/api/v2.0/projects/library/repositories",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"/api/v2.0/projects/library/repositories/hello/mariadb/_self",
|
|
||||||
args{regexp.MustCompile(`^/api/v2.0/projects/.*/repositories/(.*)/_self`), "/api/v2.0/projects/library/repositories/hello/mariadb/_self"},
|
|
||||||
"/api/v2.0/projects/library/repositories/hello%2Fmariadb/_self",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
if got := escape(tt.args.re, tt.args.path); got != tt.want {
|
|
||||||
t.Errorf("escape() = %v, want %v", got, tt.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEscapeMiddleware(t *testing.T) {
|
|
||||||
r := httptest.NewRequest(http.MethodGet, "/api/v2.0/projects/library/repositories/hello/mariadb/_self", nil)
|
|
||||||
w := httptest.NewRecorder()
|
|
||||||
|
|
||||||
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.URL.Path != "/api/v2.0/projects/library/repositories/hello%2Fmariadb/_self" {
|
|
||||||
t.Errorf("escape middleware failed")
|
|
||||||
}
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
})
|
|
||||||
|
|
||||||
EscapeMiddleware()(next).ServeHTTP(w, r)
|
|
||||||
}
|
|
@ -139,12 +139,6 @@ func (b *BaseAPI) Links(ctx context.Context, u *url.URL, total, pageNumber, page
|
|||||||
return links
|
return links
|
||||||
}
|
}
|
||||||
ul := *u
|
ul := *u
|
||||||
// try to unescape the repository name which contains escaped slashes
|
|
||||||
if escapedPath, err := url.PathUnescape(ul.Path); err == nil {
|
|
||||||
ul.Path = escapedPath
|
|
||||||
} else {
|
|
||||||
log.Errorf("failed to unescape the path %s: %v", ul.Path, err)
|
|
||||||
}
|
|
||||||
// prev
|
// prev
|
||||||
if pageNumber > 1 && (pageNumber-1)*pageSize < total {
|
if pageNumber > 1 && (pageNumber-1)*pageSize < total {
|
||||||
q := ul.Query()
|
q := ul.Query()
|
||||||
|
@ -100,9 +100,9 @@ func (b *baseHandlerTestSuite) TestLinks() {
|
|||||||
links = b.base.Links(nil, url, 3, 2, 1)
|
links = b.base.Links(nil, url, 3, 2, 1)
|
||||||
b.Require().Len(links, 2)
|
b.Require().Len(links, 2)
|
||||||
b.Equal("prev", links[0].Rel)
|
b.Equal("prev", links[0].Rel)
|
||||||
b.Equal("http://localhost/api/library/hello-world/artifacts?page=1&page_size=1&q=a=~b", links[0].URL)
|
b.Equal("http://localhost/api/library%252Fhello-world/artifacts?page=1&page_size=1&q=a=~b", links[0].URL)
|
||||||
b.Equal("next", links[1].Rel)
|
b.Equal("next", links[1].Rel)
|
||||||
b.Equal("http://localhost/api/library/hello-world/artifacts?page=3&page_size=1&q=a=~b", links[1].URL)
|
b.Equal("http://localhost/api/library%252Fhello-world/artifacts?page=3&page_size=1&q=a=~b", links[1].URL)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBaseHandler(t *testing.T) {
|
func TestBaseHandler(t *testing.T) {
|
||||||
|
@ -21,7 +21,6 @@ import (
|
|||||||
serror "github.com/goharbor/harbor/src/server/error"
|
serror "github.com/goharbor/harbor/src/server/error"
|
||||||
"github.com/goharbor/harbor/src/server/middleware"
|
"github.com/goharbor/harbor/src/server/middleware"
|
||||||
"github.com/goharbor/harbor/src/server/middleware/blob"
|
"github.com/goharbor/harbor/src/server/middleware/blob"
|
||||||
"github.com/goharbor/harbor/src/server/middleware/path"
|
|
||||||
"github.com/goharbor/harbor/src/server/middleware/quota"
|
"github.com/goharbor/harbor/src/server/middleware/quota"
|
||||||
"github.com/goharbor/harbor/src/server/v2.0/restapi"
|
"github.com/goharbor/harbor/src/server/v2.0/restapi"
|
||||||
)
|
)
|
||||||
@ -45,9 +44,7 @@ func New() http.Handler {
|
|||||||
|
|
||||||
api.ServeError = serveError
|
api.ServeError = serveError
|
||||||
|
|
||||||
// HACK: Use path.EscapeMiddleware to escape same patterns of the URL before the swagger handler
|
return h
|
||||||
// eg /api/v2.0/projects/library/repositories/hello/world/artifacts to /api/v2.0/projects/library/repositories/hello%2Fworld/artifacts
|
|
||||||
return path.EscapeMiddleware()(h)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Before executing operation handler, go-swagger will bind a parameters object to a request and validate the request,
|
// Before executing operation handler, go-swagger will bind a parameters object to a request and validate the request,
|
||||||
|
@ -59,7 +59,7 @@ class TestProjects(unittest.TestCase):
|
|||||||
self.assertEqual(quota[0].used["storage"], 2789002)
|
self.assertEqual(quota[0].used["storage"], 2789002)
|
||||||
|
|
||||||
#8. Delete repository(RA) by user(UA);
|
#8. Delete repository(RA) by user(UA);
|
||||||
self.repo.delete_repoitory(project_name, image, **ADMIN_CLIENT)
|
self.repo.delete_repoitory(project_name, "goharbor%2Falpine", **ADMIN_CLIENT)
|
||||||
|
|
||||||
#9. Quota should be 0
|
#9. Quota should be 0
|
||||||
quota = self.system.get_project_quota("project", project_id, **ADMIN_CLIENT)
|
quota = self.system.get_project_quota("project", project_id, **ADMIN_CLIENT)
|
||||||
|
Loading…
Reference in New Issue
Block a user