2016-02-01 12:59:10 +01:00
|
|
|
/*
|
|
|
|
Copyright (c) 2016 VMware, Inc. All Rights Reserved.
|
|
|
|
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.
|
|
|
|
*/
|
2016-02-26 11:54:14 +01:00
|
|
|
|
2016-02-01 12:59:10 +01:00
|
|
|
package api
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
2016-06-06 11:31:47 +02:00
|
|
|
"fmt"
|
2016-02-01 12:59:10 +01:00
|
|
|
"net/http"
|
2016-06-29 12:52:24 +02:00
|
|
|
"os"
|
2016-06-13 10:49:46 +02:00
|
|
|
"strconv"
|
2016-02-01 12:59:10 +01:00
|
|
|
|
2016-06-06 11:31:47 +02:00
|
|
|
"github.com/astaxie/beego/validation"
|
2016-03-07 15:27:47 +01:00
|
|
|
"github.com/vmware/harbor/auth"
|
2016-02-01 12:59:10 +01:00
|
|
|
"github.com/vmware/harbor/dao"
|
|
|
|
"github.com/vmware/harbor/models"
|
2016-03-28 02:50:09 +02:00
|
|
|
"github.com/vmware/harbor/utils/log"
|
2016-03-28 03:17:56 +02:00
|
|
|
|
|
|
|
"github.com/astaxie/beego"
|
2016-02-01 12:59:10 +01:00
|
|
|
)
|
|
|
|
|
2016-07-31 13:58:33 +02:00
|
|
|
const (
|
|
|
|
defaultPageSize int64 = 10
|
|
|
|
maxPageSize int64 = 100
|
|
|
|
)
|
|
|
|
|
2016-02-26 11:35:55 +01:00
|
|
|
// BaseAPI wraps common methods for controllers to host API
|
2016-02-01 12:59:10 +01:00
|
|
|
type BaseAPI struct {
|
|
|
|
beego.Controller
|
|
|
|
}
|
|
|
|
|
2016-02-26 11:35:55 +01:00
|
|
|
// Render returns nil as it won't render template
|
2016-02-01 12:59:10 +01:00
|
|
|
func (b *BaseAPI) Render() error {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2016-02-26 11:35:55 +01:00
|
|
|
// RenderError provides shortcut to render http error
|
2016-02-01 12:59:10 +01:00
|
|
|
func (b *BaseAPI) RenderError(code int, text string) {
|
|
|
|
http.Error(b.Ctx.ResponseWriter, text, code)
|
|
|
|
}
|
|
|
|
|
2016-02-26 11:35:55 +01:00
|
|
|
// DecodeJSONReq decodes a json request
|
2016-02-25 06:40:08 +01:00
|
|
|
func (b *BaseAPI) DecodeJSONReq(v interface{}) {
|
2016-02-01 12:59:10 +01:00
|
|
|
err := json.Unmarshal(b.Ctx.Input.CopyBody(1<<32), v)
|
|
|
|
if err != nil {
|
2016-03-25 07:55:53 +01:00
|
|
|
log.Errorf("Error while decoding the json request, error: %v", err)
|
2016-02-24 07:31:52 +01:00
|
|
|
b.CustomAbort(http.StatusBadRequest, "Invalid json request")
|
2016-02-01 12:59:10 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-06-06 11:31:47 +02:00
|
|
|
// Validate validates v if it implements interface validation.ValidFormer
|
|
|
|
func (b *BaseAPI) Validate(v interface{}) {
|
|
|
|
validator := validation.Validation{}
|
|
|
|
isValid, err := validator.Valid(v)
|
|
|
|
if err != nil {
|
|
|
|
log.Errorf("failed to validate: %v", err)
|
|
|
|
b.CustomAbort(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError))
|
|
|
|
}
|
|
|
|
|
|
|
|
if !isValid {
|
|
|
|
message := ""
|
|
|
|
for _, e := range validator.Errors {
|
|
|
|
message += fmt.Sprintf("%s %s \n", e.Field, e.Message)
|
|
|
|
}
|
|
|
|
b.CustomAbort(http.StatusBadRequest, message)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-06-06 11:42:16 +02:00
|
|
|
// DecodeJSONReqAndValidate does both decoding and validation
|
|
|
|
func (b *BaseAPI) DecodeJSONReqAndValidate(v interface{}) {
|
2016-06-06 11:31:47 +02:00
|
|
|
b.DecodeJSONReq(v)
|
|
|
|
b.Validate(v)
|
|
|
|
}
|
|
|
|
|
2016-02-26 11:35:55 +01:00
|
|
|
// ValidateUser checks if the request triggered by a valid user
|
2016-02-01 12:59:10 +01:00
|
|
|
func (b *BaseAPI) ValidateUser() int {
|
2016-08-11 10:35:18 +02:00
|
|
|
userID, needsCheck, ok := b.GetUserIDForRequest()
|
|
|
|
if !ok {
|
|
|
|
log.Warning("No user id in session, canceling request")
|
|
|
|
b.CustomAbort(http.StatusUnauthorized, "")
|
|
|
|
}
|
|
|
|
if needsCheck {
|
|
|
|
u, err := dao.GetUser(models.User{UserID: userID})
|
|
|
|
if err != nil {
|
|
|
|
log.Errorf("Error occurred in GetUser, error: %v", err)
|
|
|
|
b.CustomAbort(http.StatusInternalServerError, "Internal error.")
|
|
|
|
}
|
|
|
|
if u == nil {
|
|
|
|
log.Warningf("User was deleted already, user id: %d, canceling request.", userID)
|
|
|
|
b.CustomAbort(http.StatusUnauthorized, "")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return userID
|
|
|
|
}
|
2016-02-01 12:59:10 +01:00
|
|
|
|
2016-08-11 10:35:18 +02:00
|
|
|
// GetUserIDForRequest tries to get user ID from basic auth header and session.
|
|
|
|
// It returns the user ID, whether need further verification(when the id is from session) and if the action is successful
|
|
|
|
func (b *BaseAPI) GetUserIDForRequest() (int, bool, bool) {
|
2016-03-08 04:53:13 +01:00
|
|
|
username, password, ok := b.Ctx.Request.BasicAuth()
|
|
|
|
if ok {
|
2016-03-25 07:55:53 +01:00
|
|
|
log.Infof("Requst with Basic Authentication header, username: %s", username)
|
2016-04-15 07:17:32 +02:00
|
|
|
user, err := auth.Login(models.AuthModel{
|
|
|
|
Principal: username,
|
|
|
|
Password: password,
|
|
|
|
})
|
2016-03-07 15:27:47 +01:00
|
|
|
if err != nil {
|
2016-03-25 07:55:53 +01:00
|
|
|
log.Errorf("Error while trying to login, username: %s, error: %v", username, err)
|
2016-03-07 15:27:47 +01:00
|
|
|
user = nil
|
|
|
|
}
|
|
|
|
if user != nil {
|
2016-08-11 10:35:18 +02:00
|
|
|
// User login successfully no further check required.
|
|
|
|
return user.UserID, false, true
|
2016-03-07 15:27:47 +01:00
|
|
|
}
|
|
|
|
}
|
2016-08-11 10:35:18 +02:00
|
|
|
sessionUserID, ok := b.GetSession("userId").(int)
|
|
|
|
if ok {
|
|
|
|
// The ID is from session
|
|
|
|
return sessionUserID, true, true
|
2016-02-01 12:59:10 +01:00
|
|
|
}
|
2016-08-11 10:35:18 +02:00
|
|
|
log.Debug("No valid user id in session.")
|
|
|
|
return 0, false, false
|
2016-02-01 12:59:10 +01:00
|
|
|
}
|
2016-04-22 11:57:39 +02:00
|
|
|
|
2016-04-25 06:10:15 +02:00
|
|
|
// Redirect does redirection to resource URI with http header status code.
|
|
|
|
func (b *BaseAPI) Redirect(statusCode int, resouceID string) {
|
|
|
|
requestURI := b.Ctx.Request.RequestURI
|
|
|
|
resoucreURI := requestURI + "/" + resouceID
|
|
|
|
|
|
|
|
b.Ctx.Redirect(statusCode, resoucreURI)
|
2016-04-22 11:57:39 +02:00
|
|
|
}
|
2016-06-13 10:49:46 +02:00
|
|
|
|
|
|
|
// GetIDFromURL checks the ID in request URL
|
|
|
|
func (b *BaseAPI) GetIDFromURL() int64 {
|
|
|
|
idStr := b.Ctx.Input.Param(":id")
|
|
|
|
if len(idStr) == 0 {
|
|
|
|
b.CustomAbort(http.StatusBadRequest, "invalid ID in URL")
|
|
|
|
}
|
|
|
|
|
|
|
|
id, err := strconv.ParseInt(idStr, 10, 64)
|
|
|
|
if err != nil || id <= 0 {
|
|
|
|
b.CustomAbort(http.StatusBadRequest, "invalid ID in URL")
|
|
|
|
}
|
|
|
|
|
|
|
|
return id
|
|
|
|
}
|
2016-06-29 12:52:24 +02:00
|
|
|
|
2016-07-31 13:58:33 +02:00
|
|
|
// set "Link" and "X-Total-Count" header for pagination request
|
|
|
|
func (b *BaseAPI) setPaginationHeader(total, page, pageSize int64) {
|
|
|
|
b.Ctx.ResponseWriter.Header().Set("X-Total-Count", strconv.FormatInt(total, 10))
|
|
|
|
|
|
|
|
link := ""
|
|
|
|
|
|
|
|
// set previous link
|
|
|
|
if page > 1 && (page-1)*pageSize <= total {
|
|
|
|
u := *(b.Ctx.Request.URL)
|
|
|
|
q := u.Query()
|
|
|
|
q.Set("page", strconv.FormatInt(page-1, 10))
|
|
|
|
u.RawQuery = q.Encode()
|
|
|
|
if len(link) != 0 {
|
|
|
|
link += ", "
|
|
|
|
}
|
|
|
|
link += fmt.Sprintf("<%s>; rel=\"prev\"", u.String())
|
|
|
|
}
|
|
|
|
|
|
|
|
// set next link
|
|
|
|
if pageSize*page < total {
|
|
|
|
u := *(b.Ctx.Request.URL)
|
|
|
|
q := u.Query()
|
|
|
|
q.Set("page", strconv.FormatInt(page+1, 10))
|
|
|
|
u.RawQuery = q.Encode()
|
|
|
|
if len(link) != 0 {
|
|
|
|
link += ", "
|
|
|
|
}
|
|
|
|
link += fmt.Sprintf("<%s>; rel=\"next\"", u.String())
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(link) != 0 {
|
|
|
|
b.Ctx.ResponseWriter.Header().Set("Link", link)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *BaseAPI) getPaginationParams() (page, pageSize int64) {
|
|
|
|
page, err := b.GetInt64("page", 1)
|
|
|
|
if err != nil || page <= 0 {
|
|
|
|
b.CustomAbort(http.StatusBadRequest, "invalid page")
|
|
|
|
}
|
|
|
|
|
|
|
|
pageSize, err = b.GetInt64("page_size", defaultPageSize)
|
|
|
|
if err != nil || pageSize <= 0 {
|
|
|
|
b.CustomAbort(http.StatusBadRequest, "invalid page_size")
|
|
|
|
}
|
|
|
|
|
|
|
|
if pageSize > maxPageSize {
|
|
|
|
pageSize = maxPageSize
|
|
|
|
log.Debugf("the parameter page_size %d exceeds the max %d, set it to max", pageSize, maxPageSize)
|
|
|
|
}
|
|
|
|
|
|
|
|
return page, pageSize
|
|
|
|
}
|
|
|
|
|
2016-06-29 12:52:24 +02:00
|
|
|
func getIsInsecure() bool {
|
|
|
|
insecure := false
|
|
|
|
|
|
|
|
verifyRemoteCert := os.Getenv("VERIFY_REMOTE_CERT")
|
|
|
|
if verifyRemoteCert == "off" {
|
|
|
|
insecure = true
|
|
|
|
}
|
|
|
|
|
|
|
|
return insecure
|
|
|
|
}
|