Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(api,ui): allow to rollback an action from audit #4036

Merged
merged 7 commits into from
Mar 25, 2019
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