func cookbookHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") pathArray := splitPath(r.URL.Path) cookbookResponse := make(map[string]interface{}) opUser, oerr := actor.GetReqUser(r.Header.Get("X-OPS-USERID")) if oerr != nil { jsonErrorReport(w, r, oerr.Error(), oerr.Status()) return } var numResults string r.ParseForm() if nrs, found := r.Form["num_versions"]; found { if len(nrs) < 0 { jsonErrorReport(w, r, "invalid num_versions", http.StatusBadRequest) return } numResults = nrs[0] err := util.ValidateNumVersions(numResults) if err != nil { jsonErrorReport(w, r, err.Error(), err.Status()) return } } force := "" if f, fok := r.Form["force"]; fok { if len(f) > 0 { force = f[0] } } pathArrayLen := len(pathArray) /* 1 and 2 length path arrays only support GET */ if pathArrayLen < 3 && r.Method != "GET" { jsonErrorReport(w, r, "Bad request.", http.StatusMethodNotAllowed) return } else if pathArrayLen < 3 && opUser.IsValidator() { jsonErrorReport(w, r, "You are not allowed to perform this action", http.StatusForbidden) return } /* chef-pedant is happier when checking if a validator can do something * surprisingly late in the game. It wants the perm checks to be * checked after the method for the end point is checked out as * something it's going to handle, so, for instance, issuing a DELETE * against an endpoint where that isn't allowed should respond with a * 405, rather than a 403, which also makes sense in areas where * validators shouldn't be able to do anything. *shrugs* */ if pathArrayLen == 1 || (pathArrayLen == 2 && pathArray[1] == "") { /* list all cookbooks */ cookbookResponse = cookbook.CookbookLister(numResults) } else if pathArrayLen == 2 { /* info about a cookbook and all its versions */ cookbookName := pathArray[1] /* Undocumented behavior - a cookbook name of _latest gets a * list of the latest versions of all the cookbooks, and _recipe * gets the recipes of the latest cookbooks. */ if cookbookName == "_latest" { cookbookResponse = cookbook.CookbookLatest() } else if cookbookName == "_recipes" { rlist, nerr := cookbook.CookbookRecipes() if nerr != nil { jsonErrorReport(w, r, nerr.Error(), nerr.Status()) return } enc := json.NewEncoder(w) if err := enc.Encode(&rlist); err != nil { jsonErrorReport(w, r, err.Error(), http.StatusInternalServerError) } return } else { cb, err := cookbook.Get(cookbookName) if err != nil { jsonErrorReport(w, r, err.Error(), http.StatusNotFound) return } /* Strange thing here. The API docs say if num_versions * is not specified to return one cookbook, yet the * spec indicates that if it's not set that all * cookbooks should be returned. Most places *except * here* that's the case, so it can't be changed in * infoHashBase. Explicitly set numResults to all * here. */ if numResults == "" { numResults = "all" } cookbookResponse[cookbookName] = cb.InfoHash(numResults) } } else if pathArrayLen == 3 { /* get information about or manipulate a specific cookbook * version */ cookbookName := pathArray[1] var cookbookVersion string var vererr util.Gerror opUser, oerr := actor.GetReqUser(r.Header.Get("X-OPS-USERID")) if oerr != nil { jsonErrorReport(w, r, oerr.Error(), oerr.Status()) return } if r.Method == "GET" && pathArray[2] == "_latest" { // might be other special vers cookbookVersion = pathArray[2] } else { cookbookVersion, vererr = util.ValidateAsVersion(pathArray[2]) if vererr != nil { vererr := util.Errorf("Invalid cookbook version '%s'.", pathArray[2]) jsonErrorReport(w, r, vererr.Error(), vererr.Status()) return } } switch r.Method { case "DELETE", "GET": if opUser.IsValidator() { jsonErrorReport(w, r, "You are not allowed to perform this action", http.StatusForbidden) return } cb, err := cookbook.Get(cookbookName) if err != nil { if err.Status() == http.StatusNotFound { msg := fmt.Sprintf("Cannot find a cookbook named %s with version %s", cookbookName, cookbookVersion) jsonErrorReport(w, r, msg, err.Status()) } else { jsonErrorReport(w, r, err.Error(), err.Status()) } return } cbv, err := cb.GetVersion(cookbookVersion) if err != nil { jsonErrorReport(w, r, err.Error(), http.StatusNotFound) return } if r.Method == "DELETE" { if !opUser.IsAdmin() { jsonErrorReport(w, r, "You are not allowed to take this action.", http.StatusForbidden) return } err := cb.DeleteVersion(cookbookVersion) if err != nil { jsonErrorReport(w, r, err.Error(), err.Status()) return } if lerr := loginfo.LogEvent(opUser, cbv, "delete"); lerr != nil { jsonErrorReport(w, r, lerr.Error(), http.StatusInternalServerError) return } /* If all versions are gone, remove the * cookbook - seems to be the desired * behavior. */ if cb.NumVersions() == 0 { if cerr := cb.Delete(); cerr != nil { jsonErrorReport(w, r, cerr.Error(), http.StatusInternalServerError) return } } } else { /* Special JSON rendition of the * cookbook with some but not all of * the fields. */ cookbookResponse = cbv.ToJSON(r.Method) /* Sometimes, but not always, chef needs * empty slices of maps for these * values. Arrrgh. */ /* Doing it this way is absolutely * insane. However, webui really wants * this information, while chef-pedant * absolutely does NOT want it there. * knife seems happy without it as well. * Until such time that this gets * resolved in a non-crazy manner, for * this only send that info back if it's * a webui request. */ if rs := r.Header.Get("X-Ops-Request-Source"); rs == "web" { chkDiv := []string{"definitions", "libraries", "attributes", "providers", "resources", "templates", "root_files", "files"} for _, cd := range chkDiv { if cookbookResponse[cd] == nil { cookbookResponse[cd] = make([]map[string]interface{}, 0) } } } } case "PUT": if !opUser.IsAdmin() { jsonErrorReport(w, r, "You are not allowed to perform this action", http.StatusForbidden) return } cbvData, jerr := parseObjJSON(r.Body) if jerr != nil { jsonErrorReport(w, r, jerr.Error(), http.StatusBadRequest) return } /* First, see if the cookbook already exists, & * if not create it. Second, see if this * specific version of the cookbook exists. If * so, update it, otherwise, create it and set * the latest version as needed. */ cb, err := cookbook.Get(cookbookName) if err != nil { cb, err = cookbook.New(cookbookName) if err != nil { jsonErrorReport(w, r, err.Error(), err.Status()) return } /* save it so we get the id with mysql * for creating versions & such */ serr := cb.Save() if serr != nil { jsonErrorReport(w, r, serr.Error(), http.StatusInternalServerError) return } if lerr := loginfo.LogEvent(opUser, cb, "create"); lerr != nil { jsonErrorReport(w, r, lerr.Error(), http.StatusInternalServerError) return } } cbv, err := cb.GetVersion(cookbookVersion) /* Does the cookbook_name in the URL and what's * in the body match? */ switch t := cbvData["cookbook_name"].(type) { case string: /* Only send this particular * error if the cookbook version * hasn't been created yet. * Instead we want a slightly * different version later. */ if t != cookbookName && cbv == nil { terr := util.Errorf("Field 'name' invalid") jsonErrorReport(w, r, terr.Error(), terr.Status()) return } default: // rather unlikely, I think, to // be able to get here past the // cookbook get. Punk out and // don't do anything } if err != nil { var nerr util.Gerror cbv, nerr = cb.NewVersion(cookbookVersion, cbvData) if nerr != nil { // If the new version failed to // take, and there aren't any // other versions of the cookbook // it needs to be deleted. if cb.NumVersions() == 0 { cb.Delete() } jsonErrorReport(w, r, nerr.Error(), nerr.Status()) return } if lerr := loginfo.LogEvent(opUser, cbv, "create"); lerr != nil { jsonErrorReport(w, r, lerr.Error(), http.StatusInternalServerError) return } w.WriteHeader(http.StatusCreated) } else { err := cbv.UpdateVersion(cbvData, force) if err != nil { jsonErrorReport(w, r, err.Error(), err.Status()) return } gerr := cb.Save() if gerr != nil { jsonErrorReport(w, r, gerr.Error(), http.StatusInternalServerError) return } if lerr := loginfo.LogEvent(opUser, cbv, "modify"); lerr != nil { jsonErrorReport(w, r, lerr.Error(), http.StatusInternalServerError) return } } /* API docs are wrong. The docs claim that this * should have no response body, but in fact it * wants some (not all) of the cookbook version * data. */ cookbookResponse = cbv.ToJSON(r.Method) default: jsonErrorReport(w, r, "Unrecognized method", http.StatusMethodNotAllowed) return } } else { /* Say what? Bad request. */ jsonErrorReport(w, r, "Bad request", http.StatusBadRequest) return } enc := json.NewEncoder(w) if err := enc.Encode(&cookbookResponse); err != nil { jsonErrorReport(w, r, err.Error(), http.StatusInternalServerError) } }
func dataHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") pathArray := splitPath(r.URL.Path) dbResponse := make(map[string]interface{}) opUser, oerr := actor.GetReqUser(r.Header.Get("X-OPS-USERID")) if oerr != nil { jsonErrorReport(w, r, oerr.Error(), oerr.Status()) return } if len(pathArray) == 1 { /* Either a list of data bags, or a POST to create a new one */ switch r.Method { case "GET": if opUser.IsValidator() { jsonErrorReport(w, r, "You are not allowed to perform this action", http.StatusForbidden) return } /* The list */ dbList := databag.GetList() for _, k := range dbList { dbResponse[k] = util.CustomURL(fmt.Sprintf("/data/%s", k)) } case "POST": if !opUser.IsAdmin() { jsonErrorReport(w, r, "You are not allowed to perform this action", http.StatusForbidden) return } dbData, jerr := parseObjJSON(r.Body) if jerr != nil { jsonErrorReport(w, r, jerr.Error(), http.StatusBadRequest) return } /* check that the name exists */ switch t := dbData["name"].(type) { case string: if t == "" { jsonErrorReport(w, r, "Field 'name' missing", http.StatusBadRequest) return } default: jsonErrorReport(w, r, "Field 'name' missing", http.StatusBadRequest) return } chefDbag, _ := databag.Get(dbData["name"].(string)) if chefDbag != nil { httperr := fmt.Errorf("Data bag %s already exists.", dbData["name"].(string)) jsonErrorReport(w, r, httperr.Error(), http.StatusConflict) return } chefDbag, nerr := databag.New(dbData["name"].(string)) if nerr != nil { jsonErrorReport(w, r, nerr.Error(), nerr.Status()) return } serr := chefDbag.Save() if serr != nil { jsonErrorReport(w, r, serr.Error(), http.StatusInternalServerError) return } if lerr := loginfo.LogEvent(opUser, chefDbag, "create"); lerr != nil { jsonErrorReport(w, r, lerr.Error(), http.StatusInternalServerError) return } dbResponse["uri"] = util.ObjURL(chefDbag) w.WriteHeader(http.StatusCreated) default: /* The chef-pedant spec wants this response for * some reason. Mix it up, I guess. */ w.Header().Set("Allow", "GET, POST") jsonErrorReport(w, r, "GET, POST", http.StatusMethodNotAllowed) return } } else { dbName := pathArray[1] /* chef-pedant is unhappy about not reporting the HTTP status * as 404 by fetching the data bag before we see if the method * is allowed, so do a quick check for that here. */ if (len(pathArray) == 2 && r.Method == "PUT") || (len(pathArray) == 3 && r.Method == "POST") { var allowed string if len(pathArray) == 2 { allowed = "GET, POST, DELETE" } else { allowed = "GET, PUT, DELETE" } w.Header().Set("Allow", allowed) jsonErrorReport(w, r, "Method not allowed", http.StatusMethodNotAllowed) return } if opUser.IsValidator() || (!opUser.IsAdmin() && r.Method != "GET") { jsonErrorReport(w, r, "You are not allowed to perform this action", http.StatusForbidden) return } chefDbag, err := databag.Get(dbName) if err != nil { var errMsg string status := err.Status() if r.Method == "POST" { /* Posts get a special snowflake message */ errMsg = fmt.Sprintf("No data bag '%s' could be found. Please create this data bag before adding items to it.", dbName) } else { if len(pathArray) == 3 { /* This is nuts. */ if r.Method == "DELETE" { errMsg = fmt.Sprintf("Cannot load data bag %s item %s", dbName, pathArray[2]) } else { errMsg = fmt.Sprintf("Cannot load data bag item %s for data bag %s", pathArray[2], dbName) } } else { errMsg = err.Error() } } jsonErrorReport(w, r, errMsg, status) return } if len(pathArray) == 2 { /* getting list of data bag items and creating data bag * items. */ switch r.Method { case "GET": for _, k := range chefDbag.ListDBItems() { dbResponse[k] = util.CustomObjURL(chefDbag, k) } case "DELETE": /* The chef API docs don't say anything * about this existing, but it does, * and without it you can't delete data * bags at all. */ dbResponse["chef_type"] = "data_bag" dbResponse["json_class"] = "Chef::DataBag" dbResponse["name"] = chefDbag.Name err := chefDbag.Delete() if err != nil { jsonErrorReport(w, r, err.Error(), http.StatusInternalServerError) return } if lerr := loginfo.LogEvent(opUser, chefDbag, "delete"); lerr != nil { jsonErrorReport(w, r, lerr.Error(), http.StatusInternalServerError) return } case "POST": rawData := databag.RawDataBagJSON(r.Body) dbitem, nerr := chefDbag.NewDBItem(rawData) if nerr != nil { jsonErrorReport(w, r, nerr.Error(), nerr.Status()) return } if lerr := loginfo.LogEvent(opUser, dbitem, "create"); lerr != nil { jsonErrorReport(w, r, lerr.Error(), http.StatusInternalServerError) return } /* The data bag return values are all * kinds of weird. Sometimes it sends * just the raw data, sometimes it sends * the whole object, sometimes a special * snowflake version. Ugh. Have to loop * through to avoid updating the pointer * in the cache by just assigning * dbitem.RawData to dbResponse. Urk. */ for k, v := range dbitem.RawData { dbResponse[k] = v } dbResponse["data_bag"] = dbitem.DataBagName dbResponse["chef_type"] = dbitem.ChefType w.WriteHeader(http.StatusCreated) default: w.Header().Set("Allow", "GET, DELETE, POST") jsonErrorReport(w, r, "GET, DELETE, POST", http.StatusMethodNotAllowed) return } } else { /* getting, editing, and deleting existing data bag items. */ dbItemName := pathArray[2] if _, err := chefDbag.GetDBItem(dbItemName); err != nil { var httperr string if r.Method != "DELETE" { httperr = fmt.Sprintf("Cannot load data bag item %s for data bag %s", dbItemName, chefDbag.Name) } else { httperr = fmt.Sprintf("Cannot load data bag %s item %s", chefDbag.Name, dbItemName) } jsonErrorReport(w, r, httperr, http.StatusNotFound) return } switch r.Method { case "GET": dbi, err := chefDbag.GetDBItem(dbItemName) if err != nil { jsonErrorReport(w, r, err.Error(), http.StatusInternalServerError) return } dbResponse = dbi.RawData case "DELETE": dbi, err := chefDbag.GetDBItem(dbItemName) if err != nil { jsonErrorReport(w, r, err.Error(), http.StatusInternalServerError) return } /* Gotta short circuit this */ enc := json.NewEncoder(w) if err := enc.Encode(&dbi); err != nil { jsonErrorReport(w, r, err.Error(), http.StatusInternalServerError) return } err = chefDbag.DeleteDBItem(dbItemName) if err != nil { jsonErrorReport(w, r, err.Error(), http.StatusInternalServerError) return } if lerr := loginfo.LogEvent(opUser, dbi, "delete"); lerr != nil { jsonErrorReport(w, r, lerr.Error(), http.StatusInternalServerError) return } return case "PUT": rawData := databag.RawDataBagJSON(r.Body) if rawID, ok := rawData["id"]; ok { switch rawID := rawID.(type) { case string: if rawID != dbItemName { jsonErrorReport(w, r, "DataBagItem name mismatch.", http.StatusBadRequest) return } default: jsonErrorReport(w, r, "Bad request", http.StatusBadRequest) return } } dbitem, err := chefDbag.UpdateDBItem(dbItemName, rawData) if err != nil { jsonErrorReport(w, r, err.Error(), http.StatusInternalServerError) return } if lerr := loginfo.LogEvent(opUser, dbitem, "modify"); lerr != nil { jsonErrorReport(w, r, lerr.Error(), http.StatusInternalServerError) return } /* Another weird data bag item response * which isn't at all unusual. */ for k, v := range dbitem.RawData { dbResponse[k] = v } dbResponse["data_bag"] = dbitem.DataBagName dbResponse["chef_type"] = dbitem.ChefType dbResponse["id"] = dbItemName default: w.Header().Set("Allow", "GET, DELETE, PUT") jsonErrorReport(w, r, "GET, DELETE, PUT", http.StatusMethodNotAllowed) return } } } enc := json.NewEncoder(w) if err := enc.Encode(&dbResponse); err != nil { jsonErrorReport(w, r, err.Error(), http.StatusInternalServerError) } }
func nodeHandling(w http.ResponseWriter, r *http.Request) map[string]string { /* We're dealing with nodes, then. */ nodeResponse := make(map[string]string) opUser, oerr := actor.GetReqUser(r.Header.Get("X-OPS-USERID")) if oerr != nil { jsonErrorReport(w, r, oerr.Error(), oerr.Status()) return nil } switch r.Method { case "GET": if opUser.IsValidator() { jsonErrorReport(w, r, "You are not allowed to take this action.", http.StatusForbidden) return nil } nodeList := node.GetList() for _, k := range nodeList { itemURL := fmt.Sprintf("/nodes/%s", k) nodeResponse[k] = util.CustomURL(itemURL) } case "POST": if opUser.IsValidator() { jsonErrorReport(w, r, "You are not allowed to take this action.", http.StatusForbidden) return nil } nodeData, jerr := parseObjJSON(r.Body) if jerr != nil { jsonErrorReport(w, r, jerr.Error(), http.StatusBadRequest) return nil } nodeName, sterr := util.ValidateAsString(nodeData["name"]) if sterr != nil { jsonErrorReport(w, r, sterr.Error(), http.StatusBadRequest) return nil } chefNode, _ := node.Get(nodeName) if chefNode != nil { httperr := fmt.Errorf("Node already exists") jsonErrorReport(w, r, httperr.Error(), http.StatusConflict) return nil } var nerr util.Gerror chefNode, nerr = node.NewFromJSON(nodeData) if nerr != nil { jsonErrorReport(w, r, nerr.Error(), nerr.Status()) return nil } err := chefNode.Save() if err != nil { jsonErrorReport(w, r, err.Error(), http.StatusInternalServerError) return nil } err = chefNode.UpdateStatus("new") if err != nil { jsonErrorReport(w, r, err.Error(), http.StatusInternalServerError) return nil } if lerr := loginfo.LogEvent(opUser, chefNode, "create"); lerr != nil { jsonErrorReport(w, r, lerr.Error(), http.StatusInternalServerError) return nil } nodeResponse["uri"] = util.ObjURL(chefNode) w.WriteHeader(http.StatusCreated) default: jsonErrorReport(w, r, "Method not allowed for nodes", http.StatusMethodNotAllowed) return nil } return nodeResponse }
func environmentHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") accErr := checkAccept(w, r, "application/json") if accErr != nil { jsonErrorReport(w, r, accErr.Error(), http.StatusNotAcceptable) return } opUser, oerr := actor.GetReqUser(r.Header.Get("X-OPS-USERID")) if oerr != nil { jsonErrorReport(w, r, oerr.Error(), oerr.Status()) return } pathArray := splitPath(r.URL.Path) envResponse := make(map[string]interface{}) var numResults string r.ParseForm() if nrs, found := r.Form["num_versions"]; found { if len(nrs) < 0 { jsonErrorReport(w, r, "invalid num_versions", http.StatusBadRequest) return } numResults = nrs[0] err := util.ValidateNumVersions(numResults) if err != nil { jsonErrorReport(w, r, "You have requested an invalid number of versions (x >= 0 || 'all')", err.Status()) return } } pathArrayLen := len(pathArray) if pathArrayLen == 1 { switch r.Method { case "GET": if opUser.IsValidator() { jsonErrorReport(w, r, "You are not allowed to perform this action", http.StatusForbidden) return } envList := environment.GetList() for _, env := range envList { envResponse[env] = util.CustomURL(fmt.Sprintf("/environments/%s", env)) } case "POST": if !opUser.IsAdmin() { jsonErrorReport(w, r, "You are not allowed to perform this action", http.StatusForbidden) return } envData, jerr := parseObjJSON(r.Body) if jerr != nil { jsonErrorReport(w, r, jerr.Error(), http.StatusBadRequest) return } if _, ok := envData["name"].(string); !ok || envData["name"].(string) == "" { jsonErrorReport(w, r, "Environment name missing", http.StatusBadRequest) return } chefEnv, _ := environment.Get(envData["name"].(string)) if chefEnv != nil { httperr := fmt.Errorf("Environment already exists") jsonErrorReport(w, r, httperr.Error(), http.StatusConflict) return } var eerr util.Gerror chefEnv, eerr = environment.NewFromJSON(envData) if eerr != nil { jsonErrorReport(w, r, eerr.Error(), eerr.Status()) return } if err := chefEnv.Save(); err != nil { jsonErrorReport(w, r, err.Error(), http.StatusBadRequest) return } if lerr := loginfo.LogEvent(opUser, chefEnv, "create"); lerr != nil { jsonErrorReport(w, r, lerr.Error(), http.StatusInternalServerError) return } envResponse["uri"] = util.ObjURL(chefEnv) w.WriteHeader(http.StatusCreated) default: jsonErrorReport(w, r, "Unrecognized method", http.StatusMethodNotAllowed) return } } else if pathArrayLen == 2 { /* All of the 2 element operations return the environment * object, so we do the json encoding in this block and return * out. */ envName := pathArray[1] env, err := environment.Get(envName) delEnv := false /* Set this to delete the environment after * sending the json. */ if err != nil { jsonErrorReport(w, r, err.Error(), http.StatusNotFound) return } switch r.Method { case "GET", "DELETE": /* We don't actually have to do much here. */ if r.Method == "DELETE" { if !opUser.IsAdmin() { jsonErrorReport(w, r, "You are not allowed to perform this action", http.StatusForbidden) return } if envName == "_default" { jsonErrorReport(w, r, "The '_default' environment cannot be modified.", http.StatusMethodNotAllowed) return } delEnv = true } else { if opUser.IsValidator() { jsonErrorReport(w, r, "You are not allowed to perform this action", http.StatusForbidden) return } } case "PUT": if !opUser.IsAdmin() { jsonErrorReport(w, r, "You are not allowed to perform this action", http.StatusForbidden) return } envData, jerr := parseObjJSON(r.Body) if jerr != nil { jsonErrorReport(w, r, jerr.Error(), http.StatusBadRequest) return } if envData == nil { jsonErrorReport(w, r, "No environment data in body at all!", http.StatusBadRequest) return } if _, ok := envData["name"]; !ok { //envData["name"] = envName jsonErrorReport(w, r, "Environment name missing", http.StatusBadRequest) return } jsonName, sterr := util.ValidateAsString(envData["name"]) if sterr != nil { jsonErrorReport(w, r, sterr.Error(), sterr.Status()) return } else if jsonName == "" { jsonErrorReport(w, r, "Environment name missing", http.StatusBadRequest) return } if envName != envData["name"].(string) { env, err = environment.Get(envData["name"].(string)) if err == nil { jsonErrorReport(w, r, "Environment already exists", http.StatusConflict) return } var eerr util.Gerror env, eerr = environment.NewFromJSON(envData) if eerr != nil { jsonErrorReport(w, r, eerr.Error(), eerr.Status()) return } w.WriteHeader(http.StatusCreated) oldenv, olderr := environment.Get(envName) if olderr == nil { oldenv.Delete() } } else { if jsonName == "" { envData["name"] = envName } if err := env.UpdateFromJSON(envData); err != nil { jsonErrorReport(w, r, err.Error(), err.Status()) return } } if err := env.Save(); err != nil { jsonErrorReport(w, r, err.Error(), err.Status()) return } if lerr := loginfo.LogEvent(opUser, env, "modify"); lerr != nil { jsonErrorReport(w, r, lerr.Error(), http.StatusInternalServerError) return } default: jsonErrorReport(w, r, "Unrecognized method", http.StatusMethodNotAllowed) return } enc := json.NewEncoder(w) if err := enc.Encode(&env); err != nil { jsonErrorReport(w, r, err.Error(), http.StatusInternalServerError) return } if delEnv { err := env.Delete() if err != nil { jsonErrorReport(w, r, err.Error(), http.StatusInternalServerError) return } if lerr := loginfo.LogEvent(opUser, env, "delete"); lerr != nil { jsonErrorReport(w, r, lerr.Error(), http.StatusInternalServerError) return } } return } else if pathArrayLen == 3 { envName := pathArray[1] op := pathArray[2] if op == "cookbook_versions" && r.Method != "POST" || op != "cookbook_versions" && r.Method != "GET" { jsonErrorReport(w, r, "Unrecognized method", http.StatusMethodNotAllowed) return } if opUser.IsValidator() { jsonErrorReport(w, r, "You are not allowed to perform this action", http.StatusForbidden) return } env, err := environment.Get(envName) if err != nil { var errMsg string // bleh, stupid errors if err.Status() == http.StatusNotFound && (op != "recipes" && op != "cookbooks") { errMsg = fmt.Sprintf("environment '%s' not found", envName) } else { errMsg = err.Error() } jsonErrorReport(w, r, errMsg, err.Status()) return } switch op { case "cookbook_versions": /* Chef Server API docs aren't even remotely * right here. What it actually wants is the * usual hash of info for the latest or * constrained version. Weird. */ cbVer, jerr := parseObjJSON(r.Body) if jerr != nil { errmsg := jerr.Error() if !strings.Contains(errmsg, "Field") { errmsg = "invalid JSON" } else { errmsg = jerr.Error() } jsonErrorReport(w, r, errmsg, http.StatusBadRequest) return } if _, ok := cbVer["run_list"]; !ok { jsonErrorReport(w, r, "POSTed JSON badly formed.", http.StatusMethodNotAllowed) return } deps, derr := cookbook.DependsCookbooks(cbVer["run_list"].([]string), env.CookbookVersions) if derr != nil { switch derr := derr.(type) { case *cookbook.DependsError: // In 1.0.0-dev, there's a // JSONErrorMapReport function in util. // Use that when moving this forward errMap := make(map[string][]map[string]interface{}) errMap["error"] = make([]map[string]interface{}, 1) errMap["error"][0] = derr.ErrMap() w.WriteHeader(http.StatusPreconditionFailed) enc := json.NewEncoder(w) if jerr := enc.Encode(&errMap); jerr != nil { logger.Errorf(jerr.Error()) } default: jsonErrorReport(w, r, derr.Error(), http.StatusPreconditionFailed) } return } /* Need our own encoding here too. */ enc := json.NewEncoder(w) if err := enc.Encode(&deps); err != nil { jsonErrorReport(w, r, err.Error(), http.StatusInternalServerError) } return case "cookbooks": envResponse = env.AllCookbookHash(numResults) case "nodes": nodeList, err := node.GetFromEnv(envName) if err != nil { jsonErrorReport(w, r, err.Error(), http.StatusInternalServerError) return } for _, chefNode := range nodeList { envResponse[chefNode.Name] = util.ObjURL(chefNode) } case "recipes": envRecipes := env.RecipeList() /* And... we have to do our own json response * here. Hmph. */ /* TODO: make the JSON encoding stuff its own * function. Dunno why I never thought of that * before now for this. */ enc := json.NewEncoder(w) if err := enc.Encode(&envRecipes); err != nil { jsonErrorReport(w, r, err.Error(), http.StatusInternalServerError) } return default: jsonErrorReport(w, r, "Bad request", http.StatusBadRequest) return } } else if pathArrayLen == 4 { envName := pathArray[1] /* op is either "cookbooks" or "roles", and opName is the name * of the object op refers to. */ op := pathArray[2] opName := pathArray[3] if r.Method != "GET" { jsonErrorReport(w, r, "Method not allowed", http.StatusMethodNotAllowed) return } if opUser.IsValidator() { jsonErrorReport(w, r, "You are not allowed to perform this action", http.StatusForbidden) return } env, err := environment.Get(envName) if err != nil { jsonErrorReport(w, r, err.Error(), http.StatusNotFound) return } /* Biting the bullet and not redirecting this to * /roles/NAME/environments/NAME. The behavior is exactly the * same, but it makes clients and chef-pedant somewhat unhappy * to not have this way available. */ if op == "roles" { role, err := role.Get(opName) if err != nil { jsonErrorReport(w, r, err.Error(), http.StatusNotFound) return } var runList []string if envName == "_default" { runList = role.RunList } else { runList = role.EnvRunLists[envName] } envResponse["run_list"] = runList } else if op == "cookbooks" { cb, err := cookbook.Get(opName) if err != nil { jsonErrorReport(w, r, err.Error(), http.StatusNotFound) return } /* Here and, I think, here only, if num_versions isn't * set it's supposed to return ALL matching versions. * API docs are wrong here. */ if numResults == "" { numResults = "all" } envResponse[opName] = cb.ConstrainedInfoHash(numResults, env.CookbookVersions[opName]) } else { /* Not an op we know. */ jsonErrorReport(w, r, "Bad request - too many elements in path", http.StatusBadRequest) return } } else { /* Bad number of path elements. */ jsonErrorReport(w, r, "Bad request - too many elements in path", http.StatusBadRequest) return } enc := json.NewEncoder(w) if err := enc.Encode(&envResponse); err != nil { jsonErrorReport(w, r, err.Error(), http.StatusInternalServerError) } }
func roleHandling(w http.ResponseWriter, r *http.Request) map[string]string { roleResponse := make(map[string]string) opUser, oerr := actor.GetReqUser(r.Header.Get("X-OPS-USERID")) if oerr != nil { jsonErrorReport(w, r, oerr.Error(), oerr.Status()) return nil } switch r.Method { case "GET": if opUser.IsValidator() { jsonErrorReport(w, r, "You are not allowed to take this action.", http.StatusForbidden) return nil } roleList := role.GetList() for _, k := range roleList { itemURL := fmt.Sprintf("/roles/%s", k) roleResponse[k] = util.CustomURL(itemURL) } case "POST": if !opUser.IsAdmin() { jsonErrorReport(w, r, "You are not allowed to take this action.", http.StatusForbidden) return nil } roleData, jerr := parseObjJSON(r.Body) if jerr != nil { jsonErrorReport(w, r, jerr.Error(), http.StatusBadRequest) return nil } if _, ok := roleData["name"].(string); !ok { jsonErrorReport(w, r, "Role name missing", http.StatusBadRequest) return nil } chefRole, _ := role.Get(roleData["name"].(string)) if chefRole != nil { httperr := fmt.Errorf("Role already exists") jsonErrorReport(w, r, httperr.Error(), http.StatusConflict) return nil } var nerr util.Gerror chefRole, nerr = role.NewFromJSON(roleData) if nerr != nil { jsonErrorReport(w, r, nerr.Error(), nerr.Status()) return nil } err := chefRole.Save() if err != nil { jsonErrorReport(w, r, err.Error(), http.StatusInternalServerError) return nil } if lerr := loginfo.LogEvent(opUser, chefRole, "create"); lerr != nil { jsonErrorReport(w, r, lerr.Error(), http.StatusInternalServerError) return nil } roleResponse["uri"] = util.ObjURL(chefRole) w.WriteHeader(http.StatusCreated) default: jsonErrorReport(w, r, "Method not allowed for roles", http.StatusMethodNotAllowed) return nil } return roleResponse }
// user handling func userHandling(w http.ResponseWriter, r *http.Request) map[string]string { userResponse := make(map[string]string) opUser, oerr := actor.GetReqUser(r.Header.Get("X-OPS-USERID")) if oerr != nil { jsonErrorReport(w, r, oerr.Error(), oerr.Status()) return nil } switch r.Method { case "GET": userList := user.GetList() for _, k := range userList { /* Make sure it's a client and not a user. */ itemURL := fmt.Sprintf("/users/%s", k) userResponse[k] = util.CustomURL(itemURL) } case "POST": userData, jerr := parseObjJSON(r.Body) if jerr != nil { jsonErrorReport(w, r, jerr.Error(), http.StatusBadRequest) return nil } if averr := util.CheckAdminPlusValidator(userData); averr != nil { jsonErrorReport(w, r, averr.Error(), averr.Status()) return nil } if !opUser.IsAdmin() && !opUser.IsValidator() { jsonErrorReport(w, r, "You are not allowed to take this action.", http.StatusForbidden) return nil } else if !opUser.IsAdmin() && opUser.IsValidator() { if aerr := opUser.CheckPermEdit(userData, "admin"); aerr != nil { jsonErrorReport(w, r, aerr.Error(), aerr.Status()) return nil } if verr := opUser.CheckPermEdit(userData, "validator"); verr != nil { jsonErrorReport(w, r, verr.Error(), verr.Status()) return nil } } userName, sterr := util.ValidateAsString(userData["name"]) if sterr != nil || userName == "" { err := fmt.Errorf("Field 'name' missing") jsonErrorReport(w, r, err.Error(), http.StatusBadRequest) return nil } chefUser, err := user.NewFromJSON(userData) if err != nil { jsonErrorReport(w, r, err.Error(), err.Status()) return nil } if publicKey, pkok := userData["public_key"]; !pkok { var perr error if userResponse["private_key"], perr = chefUser.GenerateKeys(); perr != nil { jsonErrorReport(w, r, perr.Error(), http.StatusInternalServerError) return nil } } else { switch publicKey := publicKey.(type) { case string: if pkok, pkerr := user.ValidatePublicKey(publicKey); !pkok { jsonErrorReport(w, r, pkerr.Error(), pkerr.Status()) return nil } chefUser.SetPublicKey(publicKey) case nil: var perr error if userResponse["private_key"], perr = chefUser.GenerateKeys(); perr != nil { jsonErrorReport(w, r, perr.Error(), http.StatusInternalServerError) return nil } default: jsonErrorReport(w, r, "Bad public key", http.StatusBadRequest) return nil } } /* If we make it here, we want the public key in the * response. I think. */ userResponse["public_key"] = chefUser.PublicKey() chefUser.Save() if lerr := loginfo.LogEvent(opUser, chefUser, "create"); lerr != nil { jsonErrorReport(w, r, lerr.Error(), http.StatusInternalServerError) return nil } userResponse["uri"] = util.ObjURL(chefUser) w.WriteHeader(http.StatusCreated) default: jsonErrorReport(w, r, "Method not allowed for clients or users", http.StatusMethodNotAllowed) return nil } return userResponse }
func clientHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") path := splitPath(r.URL.Path) clientName := path[1] opUser, oerr := actor.GetReqUser(r.Header.Get("X-OPS-USERID")) if oerr != nil { jsonErrorReport(w, r, oerr.Error(), oerr.Status()) return } switch r.Method { case "DELETE": chefClient, gerr := client.Get(clientName) if gerr != nil { jsonErrorReport(w, r, gerr.Error(), gerr.Status()) return } if !opUser.IsAdmin() && !opUser.IsSelf(chefClient) { jsonErrorReport(w, r, "Deleting that client is forbidden", http.StatusForbidden) return } /* Docs were incorrect. It does want the body of the * deleted object. */ jsonClient := chefClient.ToJSON() /* Log the delete event before deleting the client, in * case the client is deleting itself. */ if lerr := loginfo.LogEvent(opUser, chefClient, "delete"); lerr != nil { jsonErrorReport(w, r, lerr.Error(), http.StatusInternalServerError) return } err := chefClient.Delete() if err != nil { jsonErrorReport(w, r, err.Error(), http.StatusForbidden) return } enc := json.NewEncoder(w) if err = enc.Encode(&jsonClient); err != nil { jsonErrorReport(w, r, err.Error(), http.StatusInternalServerError) return } case "GET": chefClient, gerr := client.Get(clientName) if gerr != nil { jsonErrorReport(w, r, gerr.Error(), gerr.Status()) return } if !opUser.IsAdmin() && !opUser.IsSelf(chefClient) { jsonErrorReport(w, r, "You are not allowed to perform that action.", http.StatusForbidden) return } /* API docs are wrong here re: public_key vs. * certificate. Also orgname (at least w/ open source) * and clientname, and it wants chef_type and * json_class */ jsonClient := chefClient.ToJSON() enc := json.NewEncoder(w) if err := enc.Encode(&jsonClient); err != nil { jsonErrorReport(w, r, err.Error(), http.StatusInternalServerError) return } case "PUT": clientData, jerr := parseObjJSON(r.Body) if jerr != nil { jsonErrorReport(w, r, jerr.Error(), http.StatusBadRequest) return } chefClient, err := client.Get(clientName) if err != nil { jsonErrorReport(w, r, err.Error(), http.StatusNotFound) return } /* Makes chef-pedant happy. I suppose it is, after all, * pedantic. */ if averr := util.CheckAdminPlusValidator(clientData); averr != nil { jsonErrorReport(w, r, averr.Error(), averr.Status()) return } if !opUser.IsAdmin() && !opUser.IsSelf(chefClient) { jsonErrorReport(w, r, "You are not allowed to perform that action.", http.StatusForbidden) return } if !opUser.IsAdmin() { var verr util.Gerror aerr := opUser.CheckPermEdit(clientData, "admin") if !opUser.IsValidator() { verr = opUser.CheckPermEdit(clientData, "validator") } if aerr != nil && verr != nil { jsonErrorReport(w, r, "Client can be either an admin or a validator, but not both.", http.StatusBadRequest) return } else if aerr != nil || verr != nil { if aerr == nil { aerr = verr } jsonErrorReport(w, r, aerr.Error(), aerr.Status()) return } } jsonName, sterr := util.ValidateAsString(clientData["name"]) if sterr != nil { jsonErrorReport(w, r, sterr.Error(), http.StatusBadRequest) return } /* If clientName and clientData["name"] aren't the * same, we're renaming. Check the new name doesn't * already exist. */ jsonClient := chefClient.ToJSON() if clientName != jsonName { if lerr := loginfo.LogEvent(opUser, chefClient, "modify"); lerr != nil { jsonErrorReport(w, r, lerr.Error(), http.StatusInternalServerError) return } err := chefClient.Rename(jsonName) if err != nil { jsonErrorReport(w, r, err.Error(), err.Status()) return } w.WriteHeader(http.StatusCreated) } if uerr := chefClient.UpdateFromJSON(clientData); uerr != nil { jsonErrorReport(w, r, uerr.Error(), uerr.Status()) return } if pk, pkfound := clientData["public_key"]; pkfound { switch pk := pk.(type) { case string: if pkok, pkerr := client.ValidatePublicKey(pk); !pkok { jsonErrorReport(w, r, pkerr.Error(), http.StatusBadRequest) return } chefClient.SetPublicKey(pk) jsonClient["public_key"] = pk case nil: //show_public_key = false default: jsonErrorReport(w, r, "Bad request", http.StatusBadRequest) return } } if p, pfound := clientData["private_key"]; pfound { switch p := p.(type) { case bool: if p { var cgerr error if jsonClient["private_key"], cgerr = chefClient.GenerateKeys(); cgerr != nil { jsonErrorReport(w, r, cgerr.Error(), http.StatusInternalServerError) return } // make sure the json // client gets the new // public key jsonClient["public_key"] = chefClient.PublicKey() } default: jsonErrorReport(w, r, "Bad request", http.StatusBadRequest) return } } chefClient.Save() if lerr := loginfo.LogEvent(opUser, chefClient, "modify"); lerr != nil { jsonErrorReport(w, r, lerr.Error(), http.StatusInternalServerError) return } enc := json.NewEncoder(w) if err := enc.Encode(&jsonClient); err != nil { jsonErrorReport(w, r, err.Error(), http.StatusInternalServerError) return } default: jsonErrorReport(w, r, "Unrecognized method for client!", http.StatusMethodNotAllowed) } }
func roleHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") opUser, oerr := actor.GetReqUser(r.Header.Get("X-OPS-USERID")) if oerr != nil { jsonErrorReport(w, r, oerr.Error(), oerr.Status()) return } /* Roles are bit weird in that there's /roles/NAME, but also * /roles/NAME/environments and /roles/NAME/environments/NAME, so we'll * split up the whole path to get those values. */ pathArray := splitPath(r.URL.Path) roleName := pathArray[1] chefRole, err := role.Get(roleName) if err != nil { jsonErrorReport(w, r, err.Error(), http.StatusNotFound) return } if len(pathArray) == 2 { /* Normal /roles/NAME case */ switch r.Method { case "GET", "DELETE": if opUser.IsValidator() || (!opUser.IsAdmin() && r.Method == "DELETE") { jsonErrorReport(w, r, "You are not allowed to perform this action", http.StatusForbidden) return } enc := json.NewEncoder(w) if err = enc.Encode(&chefRole); err != nil { jsonErrorReport(w, r, err.Error(), http.StatusInternalServerError) return } if r.Method == "DELETE" { err = chefRole.Delete() if err != nil { jsonErrorReport(w, r, err.Error(), http.StatusInternalServerError) return } if lerr := loginfo.LogEvent(opUser, chefRole, "delete"); lerr != nil { jsonErrorReport(w, r, lerr.Error(), http.StatusInternalServerError) return } } case "PUT": if !opUser.IsAdmin() { jsonErrorReport(w, r, "You are not allowed to perform this action", http.StatusForbidden) return } roleData, jerr := parseObjJSON(r.Body) if jerr != nil { jsonErrorReport(w, r, jerr.Error(), http.StatusBadRequest) return } if _, ok := roleData["name"]; !ok { roleData["name"] = roleName } jsonName, sterr := util.ValidateAsString(roleData["name"]) if sterr != nil { jsonErrorReport(w, r, sterr.Error(), sterr.Status()) return } if roleName != roleData["name"].(string) { jsonErrorReport(w, r, "Role name mismatch", http.StatusBadRequest) return } if jsonName == "" { roleData["name"] = roleName } nerr := chefRole.UpdateFromJSON(roleData) if nerr != nil { jsonErrorReport(w, r, nerr.Error(), nerr.Status()) return } err = chefRole.Save() if err != nil { jsonErrorReport(w, r, err.Error(), http.StatusInternalServerError) return } if lerr := loginfo.LogEvent(opUser, chefRole, "modify"); lerr != nil { jsonErrorReport(w, r, lerr.Error(), http.StatusInternalServerError) return } enc := json.NewEncoder(w) if err = enc.Encode(&chefRole); err != nil { jsonErrorReport(w, r, err.Error(), http.StatusInternalServerError) } default: jsonErrorReport(w, r, "Unrecognized method!", http.StatusMethodNotAllowed) } } else { var environmentName string if len(pathArray) == 4 { environmentName = pathArray[3] if _, err := environment.Get(environmentName); err != nil { jsonErrorReport(w, r, err.Error(), http.StatusNotFound) return } } /* only method for the /roles/NAME/environment stuff is GET */ switch r.Method { case "GET": /* If we have an environment name, return the * environment specific run_list. Otherwise, * return the environments we have run lists * for. Always at least return "_default", * which refers to run_list. */ if opUser.IsValidator() { jsonErrorReport(w, r, "You are not allowed to perform this action", http.StatusForbidden) return } enc := json.NewEncoder(w) if environmentName != "" { var runList []string if environmentName == "_default" { runList = chefRole.RunList } else { runList = chefRole.EnvRunLists[environmentName] } resp := make(map[string][]string, 1) resp["run_list"] = runList if err = enc.Encode(&resp); err != nil { jsonErrorReport(w, r, err.Error(), http.StatusBadRequest) } } else { roleEnvs := make([]string, len(chefRole.EnvRunLists)+1) roleEnvs[0] = "_default" i := 1 for k := range chefRole.EnvRunLists { roleEnvs[i] = k i++ } if err = enc.Encode(&roleEnvs); err != nil { jsonErrorReport(w, r, err.Error(), http.StatusBadRequest) } } default: jsonErrorReport(w, r, "Unrecognized method!", http.StatusMethodNotAllowed) } } }
func nodeHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") nodeName := r.URL.Path[7:] opUser, oerr := actor.GetReqUser(r.Header.Get("X-OPS-USERID")) if oerr != nil { jsonErrorReport(w, r, oerr.Error(), oerr.Status()) return } /* So, what are we doing? Depends on the HTTP method, of course */ switch r.Method { case "GET", "DELETE": if opUser.IsValidator() || !opUser.IsAdmin() && r.Method == "DELETE" && !(opUser.IsClient() && opUser.(*client.Client).NodeName == nodeName) { jsonErrorReport(w, r, "You are not allowed to perform this action", http.StatusForbidden) return } chefNode, nerr := node.Get(nodeName) if nerr != nil { jsonErrorReport(w, r, nerr.Error(), http.StatusNotFound) return } enc := json.NewEncoder(w) if err := enc.Encode(&chefNode); err != nil { jsonErrorReport(w, r, err.Error(), http.StatusInternalServerError) return } if r.Method == "DELETE" { err := chefNode.Delete() if err != nil { jsonErrorReport(w, r, err.Error(), http.StatusInternalServerError) return } if lerr := loginfo.LogEvent(opUser, chefNode, "delete"); lerr != nil { jsonErrorReport(w, r, lerr.Error(), http.StatusInternalServerError) return } } case "PUT": if !opUser.IsAdmin() && !(opUser.IsClient() && opUser.(*client.Client).NodeName == nodeName) { jsonErrorReport(w, r, "You are not allowed to perform this action", http.StatusForbidden) return } nodeData, jerr := parseObjJSON(r.Body) if jerr != nil { jsonErrorReport(w, r, jerr.Error(), http.StatusBadRequest) return } chefNode, kerr := node.Get(nodeName) if kerr != nil { jsonErrorReport(w, r, kerr.Error(), http.StatusNotFound) return } /* If nodeName and nodeData["name"] don't match, we * need to make a new node. Make sure that node doesn't * exist. */ if _, found := nodeData["name"]; !found { nodeData["name"] = nodeName } jsonName, sterr := util.ValidateAsString(nodeData["name"]) if sterr != nil { jsonErrorReport(w, r, sterr.Error(), http.StatusBadRequest) return } if nodeName != jsonName && jsonName != "" { jsonErrorReport(w, r, "Node name mismatch.", http.StatusBadRequest) return } if jsonName == "" { nodeData["name"] = nodeName } nerr := chefNode.UpdateFromJSON(nodeData) if nerr != nil { jsonErrorReport(w, r, nerr.Error(), nerr.Status()) return } err := chefNode.Save() if err != nil { jsonErrorReport(w, r, err.Error(), http.StatusInternalServerError) return } if lerr := loginfo.LogEvent(opUser, chefNode, "modify"); lerr != nil { jsonErrorReport(w, r, lerr.Error(), http.StatusInternalServerError) return } enc := json.NewEncoder(w) if err = enc.Encode(&chefNode); err != nil { jsonErrorReport(w, r, err.Error(), http.StatusInternalServerError) } default: jsonErrorReport(w, r, "Unrecognized method!", http.StatusMethodNotAllowed) } }