groupHttpController.go 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643
  1. package controller
  2. import (
  3. "gopkg.in/go-playground/validator.v9"
  4. "github.com/gravitl/netmaker/models"
  5. "encoding/base64"
  6. "github.com/gravitl/netmaker/functions"
  7. "github.com/gravitl/netmaker/mongoconn"
  8. "time"
  9. "strings"
  10. "fmt"
  11. "context"
  12. "encoding/json"
  13. "net/http"
  14. "github.com/gorilla/mux"
  15. "go.mongodb.org/mongo-driver/bson"
  16. "go.mongodb.org/mongo-driver/mongo/options"
  17. "github.com/gravitl/netmaker/config"
  18. )
  19. func groupHandlers(r *mux.Router) {
  20. r.HandleFunc("/api/groups", securityCheck(http.HandlerFunc(getGroups))).Methods("GET")
  21. r.HandleFunc("/api/groups", securityCheck(http.HandlerFunc(createGroup))).Methods("POST")
  22. r.HandleFunc("/api/groups/{groupname}/keyupdate", securityCheck(http.HandlerFunc(keyUpdate))).Methods("POST")
  23. r.HandleFunc("/api/groups/{groupname}", securityCheck(http.HandlerFunc(getGroup))).Methods("GET")
  24. r.HandleFunc("/api/groups/{groupname}/numnodes", securityCheck(http.HandlerFunc(getGroupNodeNumber))).Methods("GET")
  25. r.HandleFunc("/api/groups/{groupname}", securityCheck(http.HandlerFunc(updateGroup))).Methods("PUT")
  26. r.HandleFunc("/api/groups/{groupname}", securityCheck(http.HandlerFunc(deleteGroup))).Methods("DELETE")
  27. r.HandleFunc("/api/groups/{groupname}/keys", securityCheck(http.HandlerFunc(createAccessKey))).Methods("POST")
  28. r.HandleFunc("/api/groups/{groupname}/keys", securityCheck(http.HandlerFunc(getAccessKeys))).Methods("GET")
  29. r.HandleFunc("/api/groups/{groupname}/keys/{name}", securityCheck(http.HandlerFunc(deleteAccessKey))).Methods("DELETE")
  30. }
  31. //Security check is middleware for every function and just checks to make sure that its the master calling
  32. //Only admin should have access to all these group-level actions
  33. //or maybe some Users once implemented
  34. func securityCheck(next http.Handler) http.HandlerFunc {
  35. return func(w http.ResponseWriter, r *http.Request) {
  36. var errorResponse = models.ErrorResponse{
  37. Code: http.StatusInternalServerError, Message: "W1R3: It's not you it's me.",
  38. }
  39. var params = mux.Vars(r)
  40. hasgroup := params["groupname"] != ""
  41. groupexists, _ := functions.GroupExists(params["groupname"])
  42. if hasgroup && !groupexists {
  43. errorResponse = models.ErrorResponse{
  44. Code: http.StatusNotFound, Message: "W1R3: This group does not exist.",
  45. }
  46. returnErrorResponse(w, r, errorResponse)
  47. } else {
  48. bearerToken := r.Header.Get("Authorization")
  49. var hasBearer = true
  50. var tokenSplit = strings.Split(bearerToken, " ")
  51. var authToken = ""
  52. if len(tokenSplit) < 2 {
  53. hasBearer = false
  54. } else {
  55. authToken = tokenSplit[1]
  56. }
  57. //all endpoints here require master so not as complicated
  58. //still might not be a good way of doing this
  59. if !hasBearer || !authenticateMaster(authToken) {
  60. errorResponse = models.ErrorResponse{
  61. Code: http.StatusUnauthorized, Message: "W1R3: You are unauthorized to access this endpoint.",
  62. }
  63. returnErrorResponse(w, r, errorResponse)
  64. } else {
  65. next.ServeHTTP(w, r)
  66. }
  67. }
  68. }
  69. }
  70. //Consider a more secure way of setting master key
  71. func authenticateMaster(tokenString string) bool {
  72. if tokenString == config.Config.Server.MasterKey {
  73. return true
  74. }
  75. return false
  76. }
  77. //simple get all groups function
  78. func getGroups(w http.ResponseWriter, r *http.Request) {
  79. //depends on list groups function
  80. //TODO: This is perhaps a more efficient way of handling ALL http handlers
  81. //Take their primary logic and put in a separate function
  82. //May be better since most http handler functionality is needed internally cross-method
  83. //E.G. a method may need to check against all groups. But it cant call this function. That's why there's ListGroups
  84. groups := functions.ListGroups()
  85. json.NewEncoder(w).Encode(groups)
  86. }
  87. func validateGroup(operation string, group models.Group) error {
  88. v := validator.New()
  89. _ = v.RegisterValidation("addressrange_valid", func(fl validator.FieldLevel) bool {
  90. isvalid := functions.IsIpv4CIDR(fl.Field().String())
  91. return isvalid
  92. })
  93. _ = v.RegisterValidation("nameid_valid", func(fl validator.FieldLevel) bool {
  94. isFieldUnique := operation == "update" || functions.IsGroupNameUnique(fl.Field().String())
  95. inGroupCharSet := functions.NameInGroupCharSet(fl.Field().String())
  96. return isFieldUnique && inGroupCharSet
  97. })
  98. _ = v.RegisterValidation("displayname_unique", func(fl validator.FieldLevel) bool {
  99. isFieldUnique := functions.IsGroupDisplayNameUnique(fl.Field().String())
  100. return isFieldUnique || operation == "update"
  101. })
  102. err := v.Struct(group)
  103. if err != nil {
  104. for _, e := range err.(validator.ValidationErrors) {
  105. fmt.Println(e)
  106. }
  107. }
  108. return err
  109. }
  110. //Get number of nodes associated with a group
  111. //May not be necessary, but I think the front end needs it? This should be reviewed after iteration 1
  112. func getGroupNodeNumber(w http.ResponseWriter, r *http.Request) {
  113. var params = mux.Vars(r)
  114. count, err := GetGroupNodeNumber(params["groupname"])
  115. if err != nil {
  116. var errorResponse = models.ErrorResponse{
  117. Code: http.StatusInternalServerError, Message: "W1R3: Error retrieving nodes.",
  118. }
  119. returnErrorResponse(w, r, errorResponse)
  120. } else {
  121. json.NewEncoder(w).Encode(count)
  122. }
  123. }
  124. //This is haphazard
  125. //I need a better folder structure
  126. //maybe a functions/ folder and then a node.go, group.go, keys.go, misc.go
  127. func GetGroupNodeNumber(groupName string) (int, error){
  128. collection := mongoconn.Client.Database("netmaker").Collection("nodes")
  129. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  130. filter := bson.M{"group": groupName}
  131. count, err := collection.CountDocuments(ctx, filter)
  132. returncount := int(count)
  133. //not sure if this is the right way of handling this error...
  134. if err != nil {
  135. return 9999, err
  136. }
  137. defer cancel()
  138. return returncount, err
  139. }
  140. //Simple get group function
  141. func getGroup(w http.ResponseWriter, r *http.Request) {
  142. // set header.
  143. w.Header().Set("Content-Type", "application/json")
  144. var params = mux.Vars(r)
  145. var group models.Group
  146. collection := mongoconn.Client.Database("netmaker").Collection("groups")
  147. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  148. filter := bson.M{"nameid": params["groupname"]}
  149. err := collection.FindOne(ctx, filter, options.FindOne().SetProjection(bson.M{"_id": 0})).Decode(&group)
  150. defer cancel()
  151. if err != nil {
  152. mongoconn.GetError(err, w)
  153. return
  154. }
  155. json.NewEncoder(w).Encode(group)
  156. }
  157. func keyUpdate(w http.ResponseWriter, r *http.Request) {
  158. w.Header().Set("Content-Type", "application/json")
  159. var params = mux.Vars(r)
  160. var group models.Group
  161. group, err := functions.GetParentGroup(params["groupname"])
  162. if err != nil {
  163. return
  164. }
  165. group.KeyUpdateTimeStamp = time.Now().Unix()
  166. collection := mongoconn.Client.Database("netmaker").Collection("groups")
  167. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  168. filter := bson.M{"nameid": params["groupname"]}
  169. // prepare update model.
  170. update := bson.D{
  171. {"$set", bson.D{
  172. {"addressrange", group.AddressRange},
  173. {"displayname", group.DisplayName},
  174. {"defaultlistenport", group.DefaultListenPort},
  175. {"defaultpostup", group.DefaultPostUp},
  176. {"defaultpreup", group.DefaultPreUp},
  177. {"defaultkeepalive", group.DefaultKeepalive},
  178. {"keyupdatetimestamp", group.KeyUpdateTimeStamp},
  179. {"defaultsaveconfig", group.DefaultSaveConfig},
  180. {"defaultinterface", group.DefaultInterface},
  181. {"nodeslastmodified", group.NodesLastModified},
  182. {"grouplastmodified", group.GroupLastModified},
  183. {"allowmanualsignup", group.AllowManualSignUp},
  184. {"defaultcheckininterval", group.DefaultCheckInInterval},
  185. }},
  186. }
  187. errN := collection.FindOneAndUpdate(ctx, filter, update).Decode(&group)
  188. defer cancel()
  189. if errN != nil {
  190. mongoconn.GetError(errN, w)
  191. fmt.Println(errN)
  192. return
  193. }
  194. json.NewEncoder(w).Encode(group)
  195. }
  196. //Update a group
  197. func updateGroup(w http.ResponseWriter, r *http.Request) {
  198. w.Header().Set("Content-Type", "application/json")
  199. var params = mux.Vars(r)
  200. var group models.Group
  201. group, err := functions.GetParentGroup(params["groupname"])
  202. if err != nil {
  203. return
  204. }
  205. var groupChange models.Group
  206. haschange := false
  207. hasrangeupdate := false
  208. _ = json.NewDecoder(r.Body).Decode(&groupChange)
  209. if groupChange.AddressRange == "" {
  210. groupChange.AddressRange = group.AddressRange
  211. }
  212. if groupChange.NameID == "" {
  213. groupChange.NameID = group.NameID
  214. }
  215. err = validateGroup("update", groupChange)
  216. if err != nil {
  217. return
  218. }
  219. //TODO: group.Name is not update-able
  220. //group.Name acts as the ID for the group and keeps it unique and searchable by nodes
  221. //should consider renaming to group.ID
  222. //Too lazy for now.
  223. //DisplayName is the editable version and will not be used for node searches,
  224. //but will be used by front end.
  225. if groupChange.AddressRange != "" {
  226. group.AddressRange = groupChange.AddressRange
  227. var isAddressOK bool = functions.IsIpv4CIDR(groupChange.AddressRange)
  228. if !isAddressOK {
  229. return
  230. }
  231. haschange = true
  232. hasrangeupdate = true
  233. }
  234. if groupChange.DefaultListenPort != 0 {
  235. group.DefaultListenPort = groupChange.DefaultListenPort
  236. haschange = true
  237. }
  238. if groupChange.DefaultPreUp != "" {
  239. group.DefaultPreUp = groupChange.DefaultPreUp
  240. haschange = true
  241. }
  242. if groupChange.DefaultInterface != "" {
  243. group.DefaultInterface = groupChange.DefaultInterface
  244. haschange = true
  245. }
  246. if groupChange.DefaultPostUp != "" {
  247. group.DefaultPostUp = groupChange.DefaultPostUp
  248. haschange = true
  249. }
  250. if groupChange.DefaultKeepalive != 0 {
  251. group.DefaultKeepalive = groupChange.DefaultKeepalive
  252. haschange = true
  253. }
  254. if groupChange.DisplayName != "" {
  255. group.DisplayName = groupChange.DisplayName
  256. haschange = true
  257. }
  258. if groupChange.DefaultCheckInInterval != 0 {
  259. group.DefaultCheckInInterval = groupChange.DefaultCheckInInterval
  260. haschange = true
  261. }
  262. //TODO: Important. This doesn't work. This will create cases where we will
  263. //unintentionally go from allowing manual signup to disallowing
  264. //need to find a smarter way
  265. //maybe make into a text field
  266. if groupChange.AllowManualSignUp != group.AllowManualSignUp {
  267. group.AllowManualSignUp = groupChange.AllowManualSignUp
  268. haschange = true
  269. }
  270. collection := mongoconn.Client.Database("netmaker").Collection("groups")
  271. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  272. filter := bson.M{"nameid": params["groupname"]}
  273. if haschange {
  274. group.SetGroupLastModified()
  275. }
  276. // prepare update model.
  277. update := bson.D{
  278. {"$set", bson.D{
  279. {"addressrange", group.AddressRange},
  280. {"displayname", group.DisplayName},
  281. {"defaultlistenport", group.DefaultListenPort},
  282. {"defaultpostup", group.DefaultPostUp},
  283. {"defaultpreup", group.DefaultPreUp},
  284. {"defaultkeepalive", group.DefaultKeepalive},
  285. {"defaultsaveconfig", group.DefaultSaveConfig},
  286. {"defaultinterface", group.DefaultInterface},
  287. {"nodeslastmodified", group.NodesLastModified},
  288. {"grouplastmodified", group.GroupLastModified},
  289. {"allowmanualsignup", group.AllowManualSignUp},
  290. {"defaultcheckininterval", group.DefaultCheckInInterval},
  291. }},
  292. }
  293. errN := collection.FindOneAndUpdate(ctx, filter, update).Decode(&group)
  294. defer cancel()
  295. if errN != nil {
  296. mongoconn.GetError(errN, w)
  297. fmt.Println(errN)
  298. return
  299. }
  300. //Cycles through nodes and gives them new IP's based on the new range
  301. //Pretty cool, but also pretty inefficient currently
  302. if hasrangeupdate {
  303. _ = functions.UpdateGroupNodeAddresses(params["groupname"])
  304. //json.NewEncoder(w).Encode(errG)
  305. }
  306. json.NewEncoder(w).Encode(group)
  307. }
  308. //Delete a group
  309. //Will stop you if there's any nodes associated
  310. func deleteGroup(w http.ResponseWriter, r *http.Request) {
  311. // Set header
  312. w.Header().Set("Content-Type", "application/json")
  313. var params = mux.Vars(r)
  314. var errorResponse = models.ErrorResponse{
  315. Code: http.StatusInternalServerError, Message: "W1R3: It's not you it's me.",
  316. }
  317. nodecount, err := GetGroupNodeNumber(params["groupname"])
  318. //we dont wanna leave nodes hanging. They need a group!
  319. if nodecount > 0 || err != nil {
  320. errorResponse = models.ErrorResponse{
  321. Code: http.StatusForbidden, Message: "W1R3: Node check failed. All nodes must be deleted before deleting group.",
  322. }
  323. returnErrorResponse(w, r, errorResponse)
  324. return
  325. }
  326. collection := mongoconn.Client.Database("netmaker").Collection("groups")
  327. filter := bson.M{"nameid": params["groupname"]}
  328. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  329. deleteResult, err := collection.DeleteOne(ctx, filter)
  330. defer cancel()
  331. if err != nil {
  332. mongoconn.GetError(err, w)
  333. return
  334. }
  335. json.NewEncoder(w).Encode(deleteResult)
  336. }
  337. //Create a group
  338. //Pretty simple
  339. func createGroup(w http.ResponseWriter, r *http.Request) {
  340. w.Header().Set("Content-Type", "application/json")
  341. //TODO:
  342. //This may be needed to get error response. May be why some errors dont work
  343. //analyze different error responses and see what needs to be done
  344. //commenting out for now
  345. /*
  346. var errorResponse = models.ErrorResponse{
  347. Code: http.StatusInternalServerError, Message: "W1R3: It's not you it's me.",
  348. }
  349. */
  350. var group models.Group
  351. // we decode our body request params
  352. _ = json.NewDecoder(r.Body).Decode(&group)
  353. //TODO: Not really doing good validation here. Same as createNode, updateNode, and updateGroup
  354. //Need to implement some better validation across the board
  355. err := validateGroup("create", group)
  356. if err != nil {
  357. return
  358. }
  359. group.SetDefaults()
  360. group.SetNodesLastModified()
  361. group.SetGroupLastModified()
  362. group.KeyUpdateTimeStamp = time.Now().Unix()
  363. collection := mongoconn.Client.Database("netmaker").Collection("groups")
  364. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  365. // insert our group into the group table
  366. result, err := collection.InsertOne(ctx, group)
  367. _ = result
  368. defer cancel()
  369. if err != nil {
  370. mongoconn.GetError(err, w)
  371. return
  372. }
  373. }
  374. // BEGIN KEY MANAGEMENT SECTION
  375. // Consider a separate file for these controllers but I think same file is fine for now
  376. //TODO: Very little error handling
  377. //accesskey is created as a json string inside the Group collection item in mongo
  378. func createAccessKey(w http.ResponseWriter, r *http.Request) {
  379. w.Header().Set("Content-Type", "application/json")
  380. var params = mux.Vars(r)
  381. var group models.Group
  382. var accesskey models.AccessKey
  383. //start here
  384. group, err := functions.GetParentGroup(params["groupname"])
  385. if err != nil {
  386. return
  387. }
  388. _ = json.NewDecoder(r.Body).Decode(&accesskey)
  389. if accesskey.Name == "" {
  390. accesskey.Name = functions.GenKeyName()
  391. }
  392. if accesskey.Value == "" {
  393. accesskey.Value = functions.GenKey()
  394. }
  395. if accesskey.Uses == 0 {
  396. accesskey.Uses = 1
  397. }
  398. gconf, errG := functions.GetGlobalConfig()
  399. if errG != nil {
  400. mongoconn.GetError(errG, w)
  401. return
  402. }
  403. network := params["groupname"]
  404. address := gconf.ServerGRPC + gconf.PortGRPC
  405. accessstringdec := address + "." + network + "." + accesskey.Value
  406. accesskey.AccessString = base64.StdEncoding.EncodeToString([]byte(accessstringdec))
  407. group.AccessKeys = append(group.AccessKeys, accesskey)
  408. collection := mongoconn.Client.Database("netmaker").Collection("groups")
  409. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  410. // Create filter
  411. filter := bson.M{"nameid": params["groupname"]}
  412. // Read update model from body request
  413. fmt.Println("Adding key to " + group.NameID)
  414. // prepare update model.
  415. update := bson.D{
  416. {"$set", bson.D{
  417. {"accesskeys", group.AccessKeys},
  418. }},
  419. }
  420. errN := collection.FindOneAndUpdate(ctx, filter, update).Decode(&group)
  421. defer cancel()
  422. if errN != nil {
  423. mongoconn.GetError(errN, w)
  424. return
  425. }
  426. w.Write([]byte(accesskey.Value))
  427. }
  428. //pretty simple get
  429. func getAccessKeys(w http.ResponseWriter, r *http.Request) {
  430. // set header.
  431. w.Header().Set("Content-Type", "application/json")
  432. var params = mux.Vars(r)
  433. var group models.Group
  434. //var keys []models.DisplayKey
  435. var keys []models.AccessKey
  436. collection := mongoconn.Client.Database("netmaker").Collection("groups")
  437. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  438. filter := bson.M{"nameid": params["groupname"]}
  439. err := collection.FindOne(ctx, filter, options.FindOne().SetProjection(bson.M{"_id": 0})).Decode(&group)
  440. defer cancel()
  441. if err != nil {
  442. mongoconn.GetError(err, w)
  443. return
  444. }
  445. keydata, keyerr := json.Marshal(group.AccessKeys)
  446. if keyerr != nil {
  447. return
  448. }
  449. json.Unmarshal(keydata, &keys)
  450. //json.NewEncoder(w).Encode(group.AccessKeys)
  451. json.NewEncoder(w).Encode(keys)
  452. }
  453. //delete key. Has to do a little funky logic since it's not a collection item
  454. func deleteAccessKey(w http.ResponseWriter, r *http.Request) {
  455. w.Header().Set("Content-Type", "application/json")
  456. var params = mux.Vars(r)
  457. var group models.Group
  458. keyname := params["name"]
  459. //start here
  460. group, err := functions.GetParentGroup(params["groupname"])
  461. if err != nil {
  462. return
  463. }
  464. //basically, turn the list of access keys into the list of access keys before and after the item
  465. //have not done any error handling for if there's like...1 item. I think it works? need to test.
  466. for i := len(group.AccessKeys) - 1; i >= 0; i-- {
  467. currentkey:= group.AccessKeys[i]
  468. if currentkey.Name == keyname {
  469. group.AccessKeys = append(group.AccessKeys[:i],
  470. group.AccessKeys[i+1:]...)
  471. }
  472. }
  473. collection := mongoconn.Client.Database("netmaker").Collection("groups")
  474. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  475. // Create filter
  476. filter := bson.M{"nameid": params["groupname"]}
  477. // prepare update model.
  478. update := bson.D{
  479. {"$set", bson.D{
  480. {"accesskeys", group.AccessKeys},
  481. }},
  482. }
  483. errN := collection.FindOneAndUpdate(ctx, filter, update).Decode(&group)
  484. defer cancel()
  485. if errN != nil {
  486. mongoconn.GetError(errN, w)
  487. return
  488. }
  489. }