Skip to content

Commit

Permalink
feat(api,ui): allow to rollback an action from audit (#4036)
Browse files Browse the repository at this point in the history
  • Loading branch information
richardlt authored and bnjjj committed Mar 25, 2019
1 parent bfaf236 commit 6b7aaad
Show file tree
Hide file tree
Showing 16 changed files with 396 additions and 135 deletions.
163 changes: 147 additions & 16 deletions engine/api/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,137 @@ func (api *API) getActionAuditHandler() service.Handler {
}
}

func (api *API) postActionAuditRollbackHandler() service.Handler {
return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r)

groupName := vars["groupName"]
actionName := vars["permActionName"]

auditID, err := requestVarInt(r, "auditID")
if err != nil {
return err
}

grp, err := group.LoadGroup(api.mustDB(), groupName)
if err != nil {
return err
}

old, err := action.LoadTypeDefaultByNameAndGroupID(api.mustDB(), actionName, grp.ID,
action.LoadOptions.Default,
)
if err != nil {
return err
}
if old == nil {
return sdk.WithStack(sdk.ErrNoAction)
}

aa, err := action.GetAuditByActionIDAndID(api.mustDB(), old.ID, auditID)
if err != nil {
return err
}
if aa == nil {
return sdk.WithStack(sdk.ErrNotFound)
}

var before sdk.Action
if err := json.Unmarshal([]byte(aa.DataBefore), &before); err != nil {
return sdk.WrapError(err, "cannot parse action audit")
}

ea := exportentities.NewAction(before)

tx, err := api.mustDB().Begin()
if err != nil {
return sdk.WithStack(err)
}
defer tx.Rollback() // nolint

// set group id on given action, if no group given use shared.infra fo backward compatibility
// current user should be admin if the group
var newGrp *sdk.Group
if ea.Group == sdk.SharedInfraGroupName || ea.Group == "" {
newGrp = group.SharedInfraGroup
} else if ea.Group == grp.Name {
newGrp = grp
} else {
newGrp, err = group.LoadGroupByName(tx, ea.Group)
if err != nil {
return err
}
}

u := deprecatedGetUser(ctx)

if grp.ID != newGrp.ID || old.Name != ea.Name {
// check that the group exists and user is admin for group id
if err := group.CheckUserIsGroupAdmin(grp, u); err != nil {
return err
}

// check that no action already exists for same group/name
current, err := action.LoadTypeDefaultByNameAndGroupID(tx, ea.Name, newGrp.ID)
if err != nil {
return err
}
if current != nil {
return sdk.NewErrorFrom(sdk.ErrAlreadyExist, "an action already exists for given name on this group")
}
}

data, err := ea.Action()
if err != nil {
return err
}

data.GroupID = &newGrp.ID

// set action id for children based on action name and group name
// if no group name given for child, first search an action for shared.infra for backward compatibility
// else search a builtin or plugin action
for i := range data.Actions {
a, err := action.RetrieveForGroupAndName(tx, data.Actions[i].Group, data.Actions[i].Name)
if err != nil {
return err
}
data.Actions[i].ID = a.ID
}

// check data validity
if err := data.IsValidDefault(); err != nil {
return err
}

data.ID = old.ID

// check that given children exists and can be used, and no loop exists
if err := action.CheckChildrenForGroupIDsWithLoop(tx, &data, []int64{group.SharedInfraGroup.ID, newGrp.ID}); err != nil {
return err
}

if err = action.Update(tx, &data); err != nil {
return sdk.WrapError(err, "cannot update action")
}

new, err := action.LoadByID(tx, data.ID, action.LoadOptions.Default)
if err != nil {
return err
}

if err := tx.Commit(); err != nil {
return sdk.WithStack(err)
}

event.PublishActionUpdate(*old, *new, u)

new.Editable = true

return service.WriteJSON(w, new, http.StatusOK)
}
}

func (api *API) getActionUsageHandler() service.Handler {
return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r)
Expand Down Expand Up @@ -520,11 +651,6 @@ func (api *API) importActionHandler() service.Handler {
return sdk.NewError(sdk.ErrWrongRequest, err)
}

data, err := ea.Action()
if err != nil {
return sdk.NewError(sdk.ErrWrongRequest, err)
}

tx, err := api.mustDB().Begin()
if err != nil {
return sdk.WithStack(err)
Expand All @@ -534,10 +660,10 @@ func (api *API) importActionHandler() service.Handler {
// set group id on given action, if no group given use shared.infra fo backward compatibility
// current user should be admin if the group
var grp *sdk.Group
if data.Group.Name == sdk.SharedInfraGroupName {
if ea.Group == sdk.SharedInfraGroupName || ea.Group == "" {
grp = group.SharedInfraGroup
} else {
grp, err = group.LoadGroupByName(tx, data.Group.Name)
grp, err = group.LoadGroupByName(tx, ea.Group)
if err != nil {
return err
}
Expand All @@ -549,6 +675,11 @@ func (api *API) importActionHandler() service.Handler {
return err
}

data, err := ea.Action()
if err != nil {
return err
}

data.GroupID = &grp.ID

// set action id for children based on action name and group name
Expand All @@ -568,10 +699,8 @@ func (api *API) importActionHandler() service.Handler {
}

// check if action exists in database
old, err := action.LoadTypeDefaultByNameAndGroupID(api.mustDB(), data.Name, grp.ID,
action.LoadOptions.WithRequirements,
action.LoadOptions.WithParameters,
action.LoadOptions.WithGroup,
old, err := action.LoadTypeDefaultByNameAndGroupID(tx, data.Name, grp.ID,
action.LoadOptions.Default,
)
if err != nil {
return err
Expand Down Expand Up @@ -602,21 +731,23 @@ func (api *API) importActionHandler() service.Handler {
}
}

if err := tx.Commit(); err != nil {
return sdk.WithStack(err)
}

new, err := action.LoadByID(api.mustDB(), data.ID, action.LoadOptions.Default)
new, err := action.LoadByID(tx, data.ID, action.LoadOptions.Default)
if err != nil {
return err
}

if err := tx.Commit(); err != nil {
return sdk.WithStack(err)
}

if exists {
event.PublishActionUpdate(*old, *new, u)
} else {
event.PublishActionAdd(*new, u)
}

new.Editable = true

code := http.StatusCreated
if exists {
code = http.StatusOK
Expand Down
4 changes: 2 additions & 2 deletions engine/api/action/audit.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func (a addActionAudit) Compute(db gorp.SqlExecutor, e sdk.Event) error {
return sdk.WrapError(err, "unable to marshal action")
}

return insertAudit(db, &sdk.AuditAction{
return InsertAudit(db, &sdk.AuditAction{
AuditCommon: sdk.AuditCommon{
EventType: strings.Replace(e.EventType, "sdk.Event", "", -1),
Created: e.Timestamp,
Expand Down Expand Up @@ -91,7 +91,7 @@ func (a updateActionAudit) Compute(db gorp.SqlExecutor, e sdk.Event) error {
return sdk.WrapError(err, "unable to marshal action")
}

return insertAudit(db, &sdk.AuditAction{
return InsertAudit(db, &sdk.AuditAction{
AuditCommon: sdk.AuditCommon{
EventType: strings.Replace(e.EventType, "sdk.Event", "", -1),
Created: e.Timestamp,
Expand Down
24 changes: 21 additions & 3 deletions engine/api/action/dao.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,8 @@ func deleteEdgesByParentID(db gorp.SqlExecutor, parentID int64) error {
return sdk.WithStack(err)
}

func insertAudit(db gorp.SqlExecutor, aa *sdk.AuditAction) error {
// InsertAudit in database.
func InsertAudit(db gorp.SqlExecutor, aa *sdk.AuditAction) error {
return sdk.WrapError(gorpmapping.Insert(db, aa), "unable to insert audit for action %d", aa.ActionID)
}

Expand All @@ -299,14 +300,31 @@ func GetAuditsByActionID(db gorp.SqlExecutor, actionID int64) ([]sdk.AuditAction
return aas, nil
}

// GetAuditByActionIDAndID returns audit for given action id and audit id.
func GetAuditByActionIDAndID(db gorp.SqlExecutor, actionID, auditID int64) (*sdk.AuditAction, error) {
var aa sdk.AuditAction

query := gorpmapping.NewQuery(`SELECT * FROM action_audit WHERE action_id = $1 AND id = $2`).
Args(actionID, auditID)
found, err := gorpmapping.Get(db, query, &aa)
if err != nil {
return nil, sdk.WrapError(err, "cannot get action audit %d for action %d", auditID, actionID)
}
if !found {
return nil, nil
}

return &aa, nil
}

// GetAuditLatestByActionID returns action latest audit by action id.
func GetAuditLatestByActionID(db gorp.SqlExecutor, actionID int64) (*sdk.AuditAction, error) {
var aa sdk.AuditAction

query := gorpmapping.NewQuery(`SELECT * FROM action_audit WHERE action_id = $1 ORDER BY created DESC LIMIT 1`).Args(actionID)
found, err := gorpmapping.Get(db, query, &aa)
if err != nil {
return nil, sdk.WrapError(err, "cannot get action latest audit")
return nil, sdk.WrapError(err, "cannot get latest audit for action %d", actionID)
}
if !found {
return nil, nil
Expand All @@ -322,7 +340,7 @@ func GetAuditOldestByActionID(db gorp.SqlExecutor, actionID int64) (*sdk.AuditAc
query := gorpmapping.NewQuery(`SELECT * FROM action_audit WHERE action_id = $1 ORDER BY created ASC LIMIT 1`).Args(actionID)
found, err := gorpmapping.Get(db, query, &aa)
if err != nil {
return nil, sdk.WrapError(err, "cannot get action oldtest audit")
return nil, sdk.WrapError(err, "cannot get oldtest audit for action %d", actionID)
}
if !found {
return nil, nil
Expand Down
75 changes: 75 additions & 0 deletions engine/api/action_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ package api

import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http/httptest"
"testing"
"time"

yaml "gopkg.in/yaml.v2"

Expand Down Expand Up @@ -94,3 +97,75 @@ func Test_postActionImportHandler(t *testing.T) {
//Check result
t.Logf(">>%s", rec.Body.String())
}

func Test_postActionAuditRollbackHandler(t *testing.T) {
api, db, _, end := newTestAPI(t)
defer end()

u, pass := assets.InsertAdminUser(db)

grp := assets.InsertTestGroup(t, db, sdk.RandomString(10))

a := sdk.Action{
GroupID: &grp.ID,
Type: sdk.DefaultAction,
Name: "myAction",
Parameters: []sdk.Parameter{
{
Name: "my-string",
Type: sdk.StringParameter,
},
{
Name: "my-bool",
Type: sdk.BooleanParameter,
},
},
}
assert.NoError(t, action.Insert(db, &a))

before, err := json.Marshal(sdk.Action{
Type: sdk.DefaultAction,
Name: "myAction",
Parameters: []sdk.Parameter{
{
Name: "my-string",
Type: sdk.StringParameter,
},
},
Group: &sdk.Group{Name: grp.Name},
})
assert.NoError(t, err)

after, err := json.Marshal(a)
assert.NoError(t, err)

aa := sdk.AuditAction{
AuditCommon: sdk.AuditCommon{
EventType: "ActionAdd",
Created: time.Now(),
},
ActionID: a.ID,
DataType: "json",
DataBefore: string(before),
DataAfter: string(after),
}
assert.NoError(t, action.InsertAudit(db, &aa))

// prepare action rollback request
uri := api.Router.GetRoute("POST", api.postActionAuditRollbackHandler, map[string]string{
"groupName": grp.Name,
"permActionName": a.Name,
"auditID": fmt.Sprintf("%d", aa.ID),
})
test.NotEmpty(t, uri)
req := assets.NewAuthentifiedRequest(t, u, pass, "POST", uri, nil)

// send action rollback request
rec := httptest.NewRecorder()
api.Router.Mux.ServeHTTP(rec, req)
assert.Equal(t, 200, rec.Code)
var aRollback sdk.Action
assert.NoError(t, json.Unmarshal(rec.Body.Bytes(), &aRollback))

assert.Equal(t, 1, len(aRollback.Parameters))
}
1 change: 1 addition & 0 deletions engine/api/api_routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ func (api *API) InitRouter() {
r.Handle("/action/{groupName}/{permActionName}/usage", r.GET(api.getActionUsageHandler))
r.Handle("/action/{groupName}/{permActionName}/export", r.GET(api.getActionExportHandler))
r.Handle("/action/{groupName}/{permActionName}/audit", r.GET(api.getActionAuditHandler))
r.Handle("/action/{groupName}/{permActionName}/audit/{auditID}/rollback", r.POST(api.postActionAuditRollbackHandler))
r.Handle("/action/requirement", r.GET(api.getActionsRequirements, Auth(false))) // FIXME add auth used by hatcheries
r.Handle("/project/{permProjectKey}/action", r.GET(api.getActionsForProjectHandler))
r.Handle("/group/{groupID}/action", r.GET(api.getActionsForGroupHandler))
Expand Down

0 comments on commit 6b7aaad

Please sign in to comment.