diff --git a/src/chartserver/base_test.go b/src/chartserver/base_test.go index df57c7a41..a4fe65cd2 100644 --- a/src/chartserver/base_test.go +++ b/src/chartserver/base_test.go @@ -113,6 +113,7 @@ var frontServer = httptest.NewUnstartedServer(http.HandlerFunc(func(w http.Respo var getChartV = regexp.MustCompile(`^/api/\w+/charts/.+/.+$`) if r.Method == http.MethodGet && getChartV.MatchString(r.RequestURI) { + *r = *(r.WithContext(context.WithValue(r.Context(), NamespaceContextKey, "repo1"))) mockController.GetManipulationHandler().GetChartVersion(w, r) return } diff --git a/src/chartserver/manipulation_handler.go b/src/chartserver/manipulation_handler.go index 89a6c1e35..e246d4f6c 100644 --- a/src/chartserver/manipulation_handler.go +++ b/src/chartserver/manipulation_handler.go @@ -2,6 +2,7 @@ package chartserver import ( "encoding/json" + "errors" "fmt" "net/http" "net/url" @@ -12,6 +13,14 @@ import ( helm_repo "k8s.io/helm/pkg/repo" ) +const ( + //NamespaceContextKey is context key for the namespace + NamespaceContextKey ContextKey = ":repo" +) + +//ContextKey is defined for add value in the context of http request +type ContextKey string + //ManipulationHandler includes all the handler methods for the purpose of manipulating the //chart repository type ManipulationHandler struct { @@ -77,9 +86,20 @@ func (mh *ManipulationHandler) GetChartVersion(w http.ResponseWriter, req *http. chartDetails := mh.chartCache.GetChart(chartV.Digest) if chartDetails == nil { //NOT hit!! + var namespace string + + repoValue := req.Context().Value(NamespaceContextKey) + if repoValue != nil { + if ns, ok := repoValue.(string); ok { + namespace = ns + } + } + + if len(strings.TrimSpace(namespace)) == 0 { + writeInternalError(w, errors.New("failed to extract namespace from the request")) + return + } - //TODO: - namespace := "repo1" content, err := mh.getChartVersionContent(namespace, chartV.URLs[0]) if err != nil { writeInternalError(w, err) diff --git a/src/ui/api/chart_repository.go b/src/ui/api/chart_repository.go new file mode 100644 index 000000000..1d8d89634 --- /dev/null +++ b/src/ui/api/chart_repository.go @@ -0,0 +1,260 @@ +package api + +import ( + "context" + "fmt" + "net/http" + "net/url" + "os" + "strings" + + "github.com/vmware/harbor/src/chartserver" + hlog "github.com/vmware/harbor/src/common/utils/log" +) + +const ( + backendChartServerAddr = "BACKEND_CHART_SERVER" + namespaceParam = ":repo" + + accessLevelPublic = iota + accessLevelRead + accessLevelWrite + accessLevelAll + accessLevelSystem +) + +//chartController is a singleton instance +var chartController = initializeChartController() + +//ChartRepositoryAPI provides related API handlers for the chart repository APIs +type ChartRepositoryAPI struct { + //The base controller to provide common utilities + BaseController + + //Keep the namespace if existing + namespace string +} + +//Prepare something for the following actions +func (cra *ChartRepositoryAPI) Prepare() { + //Call super prepare method + cra.BaseController.Prepare() + + //Try to extract namespace for parameter of path + //It may not exist + cra.namespace = strings.TrimSpace(cra.GetStringFromPath(namespaceParam)) + + //Check the existence of namespace + //Exclude the following URI + // -/index.yaml + // -/api/chartserver/health + incomingURI := cra.Ctx.Request.RequestURI + if incomingURI != "/index.yaml" && incomingURI != "/api/chartserver/health" { + if !cra.requireNamespace(cra.namespace) { + return + } + } +} + +//GetHealthStatus handles GET /api/chartserver/health +func (cra *ChartRepositoryAPI) GetHealthStatus() { + //Check access + if !cra.requireAccess(cra.namespace, accessLevelSystem) { + return + } + + //Override the request path to '/health' + req := cra.Ctx.Request + req.URL.Path = "/health" + + chartController.GetBaseHandler().GetHealthStatus(cra.Ctx.ResponseWriter, req) +} + +//GetIndexByRepo handles GET /:repo/index.yaml +func (cra *ChartRepositoryAPI) GetIndexByRepo() { + //Check access + if !cra.requireAccess(cra.namespace, accessLevelRead) { + return + } + + chartController.GetRepositoryHandler().GetIndexFileWithNS(cra.Ctx.ResponseWriter, cra.Ctx.Request) +} + +//GetIndex handles GET /index.yaml +func (cra *ChartRepositoryAPI) GetIndex() { + //Check access + if !cra.requireAccess(cra.namespace, accessLevelSystem) { + return + } + + chartController.GetRepositoryHandler().GetIndexFile(cra.Ctx.ResponseWriter, cra.Ctx.Request) +} + +//DownloadChart handles GET /:repo/charts/:filename +func (cra *ChartRepositoryAPI) DownloadChart() { + //Check access + if !cra.requireAccess(cra.namespace, accessLevelRead) { + return + } + + chartController.GetRepositoryHandler().DownloadChartObject(cra.Ctx.ResponseWriter, cra.Ctx.Request) +} + +//ListCharts handles GET /api/:repo/charts +func (cra *ChartRepositoryAPI) ListCharts() { + //Check access + if !cra.requireAccess(cra.namespace, accessLevelRead) { + return + } + + chartController.GetManipulationHandler().ListCharts(cra.Ctx.ResponseWriter, cra.Ctx.Request) +} + +//ListChartVersions GET /api/:repo/charts/:name +func (cra *ChartRepositoryAPI) ListChartVersions() { + //Check access + if !cra.requireAccess(cra.namespace, accessLevelRead) { + return + } + + chartController.GetManipulationHandler().GetChart(cra.Ctx.ResponseWriter, cra.Ctx.Request) +} + +//GetChartVersion handles GET /api/:repo/charts/:name/:version +func (cra *ChartRepositoryAPI) GetChartVersion() { + //Check access + if !cra.requireAccess(cra.namespace, accessLevelRead) { + return + } + + //Let's pass the namespace via the context of request + req := cra.Ctx.Request + *req = *(req.WithContext(context.WithValue(req.Context(), chartserver.NamespaceContextKey, cra.namespace))) + + chartController.GetManipulationHandler().GetChartVersion(cra.Ctx.ResponseWriter, req) +} + +//DeleteChartVersion handles DELETE /api/:repo/charts/:name/:version +func (cra *ChartRepositoryAPI) DeleteChartVersion() { + //Check access + if !cra.requireAccess(cra.namespace, accessLevelAll) { + return + } + + chartController.GetManipulationHandler().DeleteChartVersion(cra.Ctx.ResponseWriter, cra.Ctx.Request) +} + +//UploadChartVersion handles POST /api/:repo/charts +func (cra *ChartRepositoryAPI) UploadChartVersion() { + //Check access + if !cra.requireAccess(cra.namespace, accessLevelWrite) { + return + } + + chartController.GetManipulationHandler().UploadChartVersion(cra.Ctx.ResponseWriter, cra.Ctx.Request) +} + +//UploadChartProvFile handles POST /api/:repo/prov +func (cra *ChartRepositoryAPI) UploadChartProvFile() { + //Check access + if !cra.requireAccess(cra.namespace, accessLevelWrite) { + return + } + + chartController.GetManipulationHandler().UploadProvenanceFile(cra.Ctx.ResponseWriter, cra.Ctx.Request) +} + +//Check if there exists a valid namespace +//Return true if it does +//Return false if it does not +func (cra *ChartRepositoryAPI) requireNamespace(namespace string) bool { + //Actually, never should be like this + if len(namespace) == 0 { + cra.HandleBadRequest(":repo should be in the request URL") + return false + } + + existsing, err := cra.ProjectMgr.Exists(namespace) + if err != nil { + //Check failed with error + cra.RenderError(http.StatusInternalServerError, fmt.Sprintf("failed to check existence of namespace %s with error: %s", namespace, err.Error())) + return false + } + + //Not existing + if !existsing { + cra.HandleBadRequest(fmt.Sprintf("namespace %s is not existing", namespace)) + return false + } + + return true +} + +//Check if the related access match the expected requirement +//If with right access, return true +//If without right access, return false +func (cra *ChartRepositoryAPI) requireAccess(namespace string, accessLevel uint) bool { + if accessLevel == accessLevelPublic { + return true //do nothing + } + + //At least, authentication is necessary when level > public + if !cra.SecurityCtx.IsAuthenticated() { + cra.HandleUnauthorized() + return false + } + + theLevel := accessLevel + //If repo is empty, system admin role must be required + if len(namespace) == 0 { + theLevel = accessLevelSystem + } + + switch theLevel { + //Should be system admin role + case accessLevelSystem: + if !cra.SecurityCtx.IsSysAdmin() { + cra.RenderError(http.StatusForbidden, fmt.Sprintf("system admin role is required but user '%s' is not", cra.SecurityCtx.GetUsername())) + return false + } + case accessLevelAll: + if !cra.SecurityCtx.HasAllPerm(namespace) { + cra.RenderError(http.StatusForbidden, fmt.Sprintf("project admin role is required but user '%s' does not have", cra.SecurityCtx.GetUsername())) + return false + } + case accessLevelWrite: + if !cra.SecurityCtx.HasWritePerm(namespace) { + cra.RenderError(http.StatusForbidden, fmt.Sprintf("developer role is required but user '%s' does not have", cra.SecurityCtx.GetUsername())) + return false + } + case accessLevelRead: + if !cra.SecurityCtx.HasReadPerm(namespace) { + cra.RenderError(http.StatusForbidden, fmt.Sprintf("at least a guest role is required for user '%s'", cra.SecurityCtx.GetUsername())) + return false + } + default: + //access rejected for invalid scope + cra.RenderError(http.StatusForbidden, "unrecognized access scope") + return false + } + + return true +} + +//Initialize the chart service controller +func initializeChartController() *chartserver.Controller { + addr := os.Getenv(backendChartServerAddr) + url, err := url.Parse(addr) + if err != nil { + hlog.Fatal("chart storage server is not correctly configured") + } + + controller, err := chartserver.NewController(url) + if err != nil { + hlog.Fatal("failed to initialize chart API controller") + } + + hlog.Info("API controller for chart repository server is successfully initialized") + + return controller +} diff --git a/src/ui/router.go b/src/ui/router.go index a080a659a..4445273b8 100644 --- a/src/ui/router.go +++ b/src/ui/router.go @@ -122,6 +122,21 @@ func initRouters() { beego.Router("/registryproxy/*", &controllers.RegistryProxy{}, "*:Handle") + //APIs for chart repository + chartRepositoryAPIType := &api.ChartRepositoryAPI{} + beego.Router("/api/chartserver/health", chartRepositoryAPIType, "get:GetHealthStatus") + beego.Router("/api/:repo/charts", chartRepositoryAPIType, "get:ListCharts") + beego.Router("/api/:repo/charts/:name", chartRepositoryAPIType, "get:ListChartVersions") + beego.Router("/api/:repo/charts/:name/:version", chartRepositoryAPIType, "get:GetChartVersion") + beego.Router("/api/:repo/charts/:name/:version", chartRepositoryAPIType, "delete:DeleteChartVersion") + beego.Router("/api/:repo/charts", chartRepositoryAPIType, "post:UploadChartVersion") + beego.Router("/api/:repo/prov", chartRepositoryAPIType, "post:UploadChartProvFile") + + //Repository services + beego.Router("/:repo/index.yaml", chartRepositoryAPIType, "get:GetIndexByRepo") + beego.Router("/index.yaml", chartRepositoryAPIType, "get:GetIndex") + beego.Router("/:repo/charts/:filename", chartRepositoryAPIType, "get:DownloadChart") + //Error pages beego.ErrorController(&controllers.ErrorController{})