gophish/controllers/api/group.go

290 lines
11 KiB
Go

package api
import (
"encoding/json"
"net/http"
"strconv"
"strings"
"time"
ctx "github.com/gophish/gophish/context"
log "github.com/gophish/gophish/logger"
"github.com/gophish/gophish/models"
"github.com/gophish/gophish/util"
"github.com/gorilla/mux"
"github.com/jinzhu/gorm"
)
// Groups returns a list of groups if requested via GET.
// If requested via POST, APIGroups creates a new group and returns a reference to it.
func (as *Server) Groups(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == "GET":
gs, err := models.GetGroups(ctx.Get(r, "user_id").(int64))
if err != nil {
JSONResponse(w, models.Response{Success: false, Message: "No groups found"}, http.StatusNotFound)
return
}
JSONResponse(w, gs, http.StatusOK)
// POST: Create a new group and return it as JSON
case r.Method == "POST":
g := models.Group{}
// Put the request into a group
// Check if content is CSV
// NB We can only upload one file at a time for new group creation, as three seperate POSTs are sent in quick succesion. We can't
// identify which group the 1+n files should be assigned to. Need to give this more thought, perhaps we can upload multiple files in one POST,
// or assign a temporary tracking token to link them together.
var csvmode = false
contentType := r.Header.Get("Content-Type")
if strings.HasPrefix(contentType, "multipart/form-data") {
csvmode = true
targets, groupname, err := util.ParseCSV(r)
if err != nil {
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
return
}
g.Name = groupname
g.Targets = targets
} else {
// else decode as JSON
err := json.NewDecoder(r.Body).Decode(&g)
if err != nil {
JSONResponse(w, models.Response{Success: false, Message: "Invalid JSON structure"}, http.StatusBadRequest)
return
}
}
_, err := models.GetGroupByName(g.Name, ctx.Get(r, "user_id").(int64))
if err != gorm.ErrRecordNotFound {
JSONResponse(w, models.Response{Success: false, Message: "Group name already in use"}, http.StatusConflict)
return
}
g.ModifiedDate = time.Now().UTC()
g.UserId = ctx.Get(r, "user_id").(int64)
err = models.PostGroup(&g)
if err != nil {
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
return
}
// With CSV we don't return the entire target list, in line with the new pagination server side processing. To maintain backwards API capabiltiy the JSON request
// will still return the full list.
if csvmode == true {
JSONResponse(w, models.GroupSummary{Id: g.Id, Name: g.Name, ModifiedDate: g.ModifiedDate, NumTargets: int64(len(g.Targets))}, http.StatusCreated)
return
}
JSONResponse(w, g, http.StatusCreated)
}
}
// GroupsSummary returns a summary of the groups owned by the current user.
func (as *Server) GroupsSummary(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == "GET":
gs, err := models.GetGroupSummaries(ctx.Get(r, "user_id").(int64))
if err != nil {
log.Error(err)
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
return
}
JSONResponse(w, gs, http.StatusOK)
}
}
// Group returns details about the requested group.
// If the group is not valid, Group returns null.
func (as *Server) Group(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, _ := strconv.ParseInt(vars["id"], 0, 64)
// Paramters passed by DataTables for pagination are handled below
v := r.URL.Query()
search := v.Get("search[value]")
sortcolumn := v.Get("order[0][column]")
sortdir := v.Get("order[0][dir]")
sortby := v.Get("columns[" + sortcolumn + "][data]")
order := sortby + " " + sortdir // e.g "first_name asc"
start, err := strconv.ParseInt(v.Get("start"), 0, 64)
if err != nil {
start = -1 // Default. gorm will ignore with this value.
}
length, err := strconv.ParseInt(v.Get("length"), 0, 64)
if err != nil {
length = -1 // Default. gorm will ignore with this value.
}
draw, err := strconv.ParseInt(v.Get("draw"), 0, 64)
if err != nil {
draw = -1 // If the draw value is missing we can assume this is not a DataTable request and return regular API result
}
g, err := models.GetGroup(id, ctx.Get(r, "user_id").(int64), start, length, search, order)
if err != nil {
JSONResponse(w, models.Response{Success: false, Message: "Group not found"}, http.StatusNotFound)
return
}
switch {
case r.Method == "GET":
// If draw paratmer is -1 return regular API response, otherwise return pagination response
if draw == -1 {
JSONResponse(w, g, http.StatusOK)
} else {
// Handle pagination for DataTable
gs, _ := models.GetGroupSummary(id, ctx.Get(r, "user_id").(int64)) // We need to get the total number of records of the group
dT := models.DataTable{Draw: draw, RecordsTotal: gs.NumTargets, RecordsFiltered: gs.NumTargets}
dT.Data = make([]interface{}, len(g.Targets)) // Pseudocode of 'dT.Data = g.Targets'. https://golang.org/doc/faq#convert_slice_of_interface
for i, v := range g.Targets {
dT.Data[i] = v
}
JSONResponse(w, dT, http.StatusOK)
}
case r.Method == "DELETE":
err = models.DeleteGroup(&g)
if err != nil {
JSONResponse(w, models.Response{Success: false, Message: "Error deleting group"}, http.StatusInternalServerError)
return
}
JSONResponse(w, models.Response{Success: true, Message: "Group deleted successfully!"}, http.StatusOK)
case r.Method == "PUT":
// Change this to get from URL and uid (don't bother with id in r.Body)
g = models.Group{}
//Check if content is CSV
var csvmode = false
contentType := r.Header.Get("Content-Type")
if strings.HasPrefix(contentType, "multipart/form-data") {
csvmode = true
targets, groupname, err := util.ParseCSV(r)
if err != nil {
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
return
}
// We need to fetch all the existing targets for this group, so as to not overwrite them below
et, _ := models.GetTargets(id, -1, -1, "", "")
g.Targets = append(targets, et...)
g.Name = groupname
g.Id = id // ID isn't supplied in the CSV file upload. Perhaps we could use the filename paramter for this? I'm not sure if this is necessary though.
} else { // else JSON
err = json.NewDecoder(r.Body).Decode(&g)
if err != nil {
log.Errorf("error decoding group: %v", err)
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
return
}
}
if g.Id != id {
JSONResponse(w, models.Response{Success: false, Message: "Error: /:id and group_id mismatch"}, http.StatusInternalServerError)
return
}
g.ModifiedDate = time.Now().UTC()
g.UserId = ctx.Get(r, "user_id").(int64)
err = models.PutGroup(&g)
if err != nil {
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
return
}
// With CSV we don't return the entire target list, in line with the new pagination server side processing. To maintain backwards API capabiltiy the JSON request
// will still return the full list.
if csvmode == true {
JSONResponse(w, models.GroupSummary{Id: g.Id, Name: g.Name, ModifiedDate: g.ModifiedDate, NumTargets: int64(len(g.Targets))}, http.StatusCreated)
return
}
JSONResponse(w, g, http.StatusOK)
}
}
// GroupSummary returns a summary of the groups owned by the current user.
func (as *Server) GroupSummary(w http.ResponseWriter, r *http.Request) {
switch {
case r.Method == "GET":
vars := mux.Vars(r)
id, _ := strconv.ParseInt(vars["id"], 0, 64)
g, err := models.GetGroupSummary(id, ctx.Get(r, "user_id").(int64))
if err != nil {
JSONResponse(w, models.Response{Success: false, Message: "Group not found"}, http.StatusNotFound)
return
}
JSONResponse(w, g, http.StatusOK)
}
}
// GroupTarget handles interactions with individual targets
func (as *Server) GroupTarget(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
gid, _ := strconv.ParseInt(vars["id"], 0, 64) // group id
// Ensure the group belongs to the user
_, err := models.GetGroup(gid, ctx.Get(r, "user_id").(int64), 0, 0, "", "")
if err != nil {
JSONResponse(w, models.Response{Success: false, Message: "Group not found"}, http.StatusNotFound)
return
}
t := models.Target{}
err = json.NewDecoder(r.Body).Decode(&t)
if err != nil {
JSONResponse(w, models.Response{Success: false, Message: "Unable to decode target"}, http.StatusInternalServerError)
return
}
switch {
case r.Method == "PUT":
// Add an individual target to a group
err = models.AddTargetToGroup(t, gid)
if err != nil {
JSONResponse(w, models.Response{Success: false, Message: "Unable to add target to group"}, http.StatusNotFound)
return
}
JSONResponse(w, models.Response{Success: true, Message: "Added target to group"}, http.StatusCreated)
case r.Method == "DELETE":
err := models.DeleteTarget(&t, gid, ctx.Get(r, "user_id").(int64)) // We pass the group id to update modified date, and userid to ensure user owner the target
if err != nil {
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
return
}
JSONResponse(w, models.Response{Success: true, Message: "Deleted target"}, http.StatusCreated)
}
}
// GroupRename handles renaming of a group (without supplying all the targets, as in Group() PUT above)
func (as *Server) GroupRename(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id, _ := strconv.ParseInt(vars["id"], 0, 64) // group id
g, err := models.GetGroup(id, ctx.Get(r, "user_id").(int64), 0, 0, "", "")
if err != nil {
JSONResponse(w, models.Response{Success: false, Message: "Group not found"}, http.StatusNotFound)
return
}
switch {
case r.Method == "PUT":
g = models.Group{}
err = json.NewDecoder(r.Body).Decode(&g)
if err != nil {
log.Errorf("error decoding group: %v", err)
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusInternalServerError)
return
}
if g.Id != id {
JSONResponse(w, models.Response{Success: false, Message: "Error: /:id and group_id mismatch"}, http.StatusInternalServerError)
return
}
_, err := models.GetGroupByName(g.Name, ctx.Get(r, "user_id").(int64))
if err != gorm.ErrRecordNotFound {
JSONResponse(w, models.Response{Success: false, Message: "Group name already in use"}, http.StatusConflict)
return
}
g.ModifiedDate = time.Now().UTC()
g.UserId = ctx.Get(r, "user_id").(int64)
err = models.UpdateGroup(&g)
if err != nil {
JSONResponse(w, models.Response{Success: false, Message: err.Error()}, http.StatusBadRequest)
return
}
JSONResponse(w, models.Response{Success: true, Message: "Group renamed"}, http.StatusCreated)
}
}