Skip to content

Commit

Permalink
generalize implementation of operations-listing endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
majewsky committed Aug 14, 2019
1 parent 9d3137f commit 00790fd
Show file tree
Hide file tree
Showing 4 changed files with 274 additions and 146 deletions.
93 changes: 62 additions & 31 deletions internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,23 @@ func (h *handler) BuildRouter() http.Handler {

router.Methods("GET").
Path(`/v1/projects/{project_id}/resources/{asset_type}/operations/pending`).
HandlerFunc(h.GetPendingOperationsForResource)
HandlerFunc(h.GetPendingOperations)
router.Methods("GET").
Path(`/v1/projects/{project_id}/resources/{asset_type}/operations/recently-failed`).
HandlerFunc(h.GetRecentlyFailedOperationsForResource)
HandlerFunc(h.GetRecentlyFailedOperations)
router.Methods("GET").
Path(`/v1/projects/{project_id}/resources/{asset_type}/operations/recently-succeeded`).
HandlerFunc(h.GetRecentlySucceededOperationsForResource)
HandlerFunc(h.GetRecentlySucceededOperations)

router.Methods("GET").
Path(`/v1/operations/pending`).
HandlerFunc(h.GetPendingOperations)
router.Methods("GET").
Path(`/v1/operations/recently-failed`).
HandlerFunc(h.GetRecentlyFailedOperations)
router.Methods("GET").
Path(`/v1/operations/recently-succeeded`).
HandlerFunc(h.GetRecentlySucceededOperations)

return router
}
Expand All @@ -102,64 +112,85 @@ func RequireJSON(w http.ResponseWriter, r *http.Request, data interface{}) bool
return true
}

func respondWithForbidden(w http.ResponseWriter) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusForbidden)
w.Write([]byte("403 Forbidden"))
}

func respondWithNotFound(w http.ResponseWriter) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusNotFound)
w.Write([]byte("404 Not found"))
}

func (h handler) CheckToken(w http.ResponseWriter, r *http.Request) (string, *gopherpolicy.Token) {
//all endpoints include the `project_id` variable, so it must definitely be there
projectUUID := mux.Vars(r)["project_id"]
if projectUUID == "" {
//for endpoints requiring the `project_id` variable, check that it's not empty
projectUUID, projectScoped := mux.Vars(r)["project_id"]
if projectScoped && projectUUID == "" {
respondWithNotFound(w)
return "", nil
}
//other endpoints might have a project ID in the `project` query argument instead
if !projectScoped {
if id := r.URL.Query().Get("project"); id != "" {
projectScoped = true
projectUUID = id
}
}

//collect object attributes for policy check
token := h.Validator.CheckToken(r)
token.Context.Logger = logg.Debug
logg.Debug("token has auth = %v", token.Context.Auth)
logg.Debug("token has roles = %v", token.Context.Roles)
//all project-scoped endpoints require the user to have access to the
//selected project
if projectScoped {
projectExists, err := h.SetTokenToProjectScope(token, projectUUID)
if respondwith.ErrorText(w, err) || !token.Require(w, "project:access") {
return "", nil
}

//only report 404 after having checked access rules, otherwise we might leak
//information about which projects exist to unauthorized users
if !projectExists {
respondWithNotFound(w)
return "", nil
}
}

return projectUUID, token
}

func (h handler) SetTokenToProjectScope(token *gopherpolicy.Token, projectUUID string) (projectExists bool, err error) {
objectAttrs := map[string]string{
"project_id": projectUUID,
"target.project.id": projectUUID,
}

project, err := h.Provider.GetProject(projectUUID)
if respondwith.ErrorText(w, err) {
return "", nil
if err != nil {
return false, err
}
scopeNotFound := project == nil
projectExists = project != nil
if project != nil {
objectAttrs["target.project.name"] = project.Name
objectAttrs["target.project.domain.id"] = project.DomainID

domain, err := h.Provider.GetDomain(project.DomainID)
if respondwith.ErrorText(w, err) {
return "", nil
if err != nil {
return false, err
}
if domain == nil {
scopeNotFound = true
projectExists = false
} else {
objectAttrs["target.project.domain.name"] = domain.Name
}
}

//all endpoints are project-scoped, so we require the user to have access to
//the selected project
token := h.Validator.CheckToken(r)
token.Context.Logger = logg.Debug
token.Context.Request = objectAttrs
logg.Debug("token has auth = %v", token.Context.Auth)
logg.Debug("token has roles = %v", token.Context.Roles)
logg.Debug("token has object attributes = %v", token.Context.Request)
if !token.Require(w, "project:access") {
return "", nil
}

//only report 404 after having checked access rules, otherwise we might leak
//information about which projects exist to unauthorized users
if scopeNotFound {
respondWithNotFound(w)
return "", nil
}

return projectUUID, token
return projectExists, nil
}

func (h handler) LoadResource(w http.ResponseWriter, r *http.Request, projectUUID string, token *gopherpolicy.Token, createIfMissing bool) *db.Resource {
Expand Down
22 changes: 16 additions & 6 deletions internal/api/assets.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,10 @@ type AssetChecked struct {
//Operation is how a db.PendingOperation or db.FinishedOperation looks like in
//the API.
type Operation struct {
AssetID string `json:"asset_id,omitempty"`
//^ AssetID is left empty when Operation appears inside type Asset.
ProjectUUID string `json:"project_id,omitempty"`
AssetType db.AssetType `json:"asset_type,omitempty"`
AssetID string `json:"asset_id,omitempty"`
//^ These fields are left empty when Operation appears inside type Asset.
State db.OperationState `json:"state"`
Reason db.OperationReason `json:"reason"`
OldSize uint64 `json:"old_size"`
Expand Down Expand Up @@ -109,7 +111,7 @@ func AssetFromDB(asset db.Asset) Asset {
}

//PendingOperationFromDB converts a db.PendingOperation into an api.Operation.
func PendingOperationFromDB(dbOp db.PendingOperation, assetID string) Operation {
func PendingOperationFromDB(dbOp db.PendingOperation, assetID string, res *db.Resource) Operation {
op := Operation{
AssetID: assetID,
State: dbOp.State(),
Expand All @@ -122,6 +124,10 @@ func PendingOperationFromDB(dbOp db.PendingOperation, assetID string) Operation
},
Finished: nil,
}
if res != nil {
op.ProjectUUID = res.ScopeUUID
op.AssetType = res.AssetType
}
if dbOp.ConfirmedAt != nil {
op.Confirmed = &OperationConfirmation{
AtUnix: dbOp.ConfirmedAt.Unix(),
Expand All @@ -137,7 +143,7 @@ func PendingOperationFromDB(dbOp db.PendingOperation, assetID string) Operation
}

//FinishedOperationFromDB converts a db.FinishedOperation into an api.Operation.
func FinishedOperationFromDB(dbOp db.FinishedOperation, assetID string) Operation {
func FinishedOperationFromDB(dbOp db.FinishedOperation, assetID string, res *db.Resource) Operation {
op := Operation{
AssetID: assetID,
State: dbOp.State(),
Expand All @@ -153,6 +159,10 @@ func FinishedOperationFromDB(dbOp db.FinishedOperation, assetID string) Operatio
ErrorMessage: dbOp.ErrorMessage,
},
}
if res != nil {
op.ProjectUUID = res.ScopeUUID
op.AssetType = res.AssetType
}
if dbOp.ConfirmedAt != nil {
op.Confirmed = &OperationConfirmation{
AtUnix: dbOp.ConfirmedAt.Unix(),
Expand Down Expand Up @@ -231,7 +241,7 @@ func (h handler) GetAsset(w http.ResponseWriter, r *http.Request) {
} else if respondwith.ErrorText(w, err) {
return
} else {
op := PendingOperationFromDB(dbPendingOp, "")
op := PendingOperationFromDB(dbPendingOp, "", nil)
asset.PendingOperation = &op
}

Expand All @@ -246,7 +256,7 @@ func (h handler) GetAsset(w http.ResponseWriter, r *http.Request) {
}
finishedOps := make([]Operation, len(dbFinishedOps))
for idx, op := range dbFinishedOps {
finishedOps[idx] = FinishedOperationFromDB(op, "")
finishedOps[idx] = FinishedOperationFromDB(op, "", nil)
}
asset.FinishedOperations = &finishedOps
}
Expand Down
Loading

0 comments on commit 00790fd

Please sign in to comment.