jit.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607
  1. package controllers
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "net/http"
  6. "strconv"
  7. "time"
  8. "github.com/gorilla/mux"
  9. "github.com/gravitl/netmaker/db"
  10. "github.com/gravitl/netmaker/logger"
  11. "github.com/gravitl/netmaker/logic"
  12. "github.com/gravitl/netmaker/models"
  13. "github.com/gravitl/netmaker/pro/email"
  14. proLogic "github.com/gravitl/netmaker/pro/logic"
  15. "github.com/gravitl/netmaker/schema"
  16. "golang.org/x/exp/slog"
  17. )
  18. func JITHandlers(r *mux.Router) {
  19. r.HandleFunc("/api/v1/jit", logic.SecurityCheck(true,
  20. http.HandlerFunc(handleJIT))).Methods(http.MethodPost, http.MethodGet)
  21. r.HandleFunc("/api/v1/jit", logic.SecurityCheck(true,
  22. http.HandlerFunc(deleteJITGrant))).Methods(http.MethodDelete)
  23. r.HandleFunc("/api/v1/jit_user/networks", logic.SecurityCheck(false,
  24. http.HandlerFunc(getUserJITNetworks))).Methods(http.MethodGet)
  25. r.HandleFunc("/api/v1/jit_user/request", logic.SecurityCheck(false,
  26. http.HandlerFunc(requestJITAccess))).Methods(http.MethodPost)
  27. }
  28. // @Summary List JIT requests for a network
  29. // @Router /api/v1/jit [get]
  30. // @Tags JIT
  31. // @Security oauth
  32. // @Produce json
  33. // @Param network query string true "Network ID"
  34. // @Param status query string false "Filter by status (pending, approved, denied, expired)"
  35. // @Param page query int false "Page number"
  36. // @Param per_page query int false "Items per page"
  37. // @Success 200 {array} schema.JITRequest
  38. // @Failure 400 {object} models.ErrorResponse
  39. // @Failure 500 {object} models.ErrorResponse
  40. //
  41. // @Summary Handle JIT operations (enable, disable, approve, deny)
  42. // @Router /api/v1/jit [post]
  43. // @Tags JIT
  44. // @Security oauth
  45. // @Accept json
  46. // @Produce json
  47. // @Param network query string true "Network ID"
  48. // @Param body body models.JITOperationRequest true "JIT operation request"
  49. // @Success 200 {object} models.SuccessResponse
  50. // @Failure 400 {object} models.ErrorResponse
  51. // @Failure 500 {object} models.ErrorResponse
  52. func handleJIT(w http.ResponseWriter, r *http.Request) {
  53. // Check if JIT feature is enabled
  54. featureFlags := logic.GetFeatureFlags()
  55. if !featureFlags.EnableJIT {
  56. logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("JIT feature is not enabled"), "forbidden"))
  57. return
  58. }
  59. networkID := r.URL.Query().Get("network")
  60. if networkID == "" {
  61. logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("network is required"), "badrequest"))
  62. return
  63. }
  64. username := r.Header.Get("user")
  65. if username == "" {
  66. logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("user not found in request"), "unauthorized"))
  67. return
  68. }
  69. user, err := logic.GetUser(username)
  70. if err != nil {
  71. logic.ReturnErrorResponse(w, r, logic.FormatError(err, "unauthorized"))
  72. return
  73. }
  74. switch r.Method {
  75. case http.MethodGet:
  76. handleJITGet(w, r, networkID, user)
  77. case http.MethodPost:
  78. handleJITPost(w, r, networkID, user)
  79. default:
  80. logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("method not allowed"), "badrequest"))
  81. }
  82. }
  83. // handleJITGet - handles GET requests for JIT status/requests
  84. func handleJITGet(w http.ResponseWriter, r *http.Request, networkID string, user *models.User) {
  85. statusFilter := r.URL.Query().Get("status") // "pending", "approved", "denied", "expired", or empty for all
  86. // Parse pagination parameters (default to 0, db.SetPagination will apply defaults)
  87. page, _ := strconv.Atoi(r.URL.Query().Get("page"))
  88. pageSize, _ := strconv.Atoi(r.URL.Query().Get("per_page"))
  89. // Apply defaults if not provided (matching db.SetPagination logic)
  90. if page < 1 {
  91. page = 1
  92. }
  93. if pageSize < 1 || pageSize > 100 {
  94. pageSize = 10
  95. }
  96. ctx := db.WithContext(r.Context())
  97. requests, total, err := proLogic.GetNetworkJITRequestsPaginated(ctx, networkID, statusFilter, page, pageSize)
  98. if err != nil {
  99. logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
  100. return
  101. }
  102. // Calculate pagination metadata
  103. totalPages := (int(total) + pageSize - 1) / pageSize
  104. if totalPages == 0 {
  105. totalPages = 1
  106. }
  107. response := map[string]interface{}{
  108. "data": requests,
  109. "page": page,
  110. "per_page": pageSize,
  111. "total": total,
  112. "total_pages": totalPages,
  113. }
  114. logic.ReturnSuccessResponseWithJson(w, r, response, "fetched JIT requests")
  115. }
  116. // handleJITPost - handles POST requests for JIT operations
  117. func handleJITPost(w http.ResponseWriter, r *http.Request, networkID string, user *models.User) {
  118. var req models.JITOperationRequest
  119. err := json.NewDecoder(r.Body).Decode(&req)
  120. if err != nil {
  121. logger.Log(0, "error decoding request body:", err.Error())
  122. logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
  123. return
  124. }
  125. switch req.Action {
  126. case "enable":
  127. handleEnableJIT(w, r, networkID, user)
  128. case "disable":
  129. handleDisableJIT(w, r, networkID, user)
  130. case "approve":
  131. handleApproveRequest(w, r, networkID, user, req.RequestID, req.ExpiresAt)
  132. case "deny":
  133. handleDenyRequest(w, r, networkID, user, req.RequestID)
  134. default:
  135. logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("invalid action"), "badrequest"))
  136. }
  137. }
  138. // handleEnableJIT - enables JIT on a network
  139. func handleEnableJIT(w http.ResponseWriter, r *http.Request, networkID string, user *models.User) {
  140. // Check if user is admin
  141. if !proLogic.IsNetworkAdmin(user, networkID) {
  142. logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("only network admins can enable JIT"), "forbidden"))
  143. return
  144. }
  145. if err := proLogic.EnableJITOnNetwork(networkID); err != nil {
  146. logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
  147. return
  148. }
  149. logic.LogEvent(&models.Event{
  150. Action: models.Update,
  151. Source: models.Subject{
  152. ID: user.UserName,
  153. Name: user.UserName,
  154. Type: models.UserSub,
  155. },
  156. TriggeredBy: user.UserName,
  157. Target: models.Subject{
  158. ID: networkID,
  159. Name: networkID,
  160. Type: models.NetworkSub,
  161. },
  162. NetworkID: models.NetworkID(networkID),
  163. Origin: models.Dashboard,
  164. })
  165. logic.ReturnSuccessResponse(w, r, "JIT enabled on network")
  166. }
  167. // handleDisableJIT - disables JIT on a network
  168. func handleDisableJIT(w http.ResponseWriter, r *http.Request, networkID string, user *models.User) {
  169. // Check if user is admin
  170. if !proLogic.IsNetworkAdmin(user, networkID) {
  171. logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("only network admins can disable JIT"), "forbidden"))
  172. return
  173. }
  174. if err := proLogic.DisableJITOnNetwork(networkID); err != nil {
  175. logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
  176. return
  177. }
  178. logic.LogEvent(&models.Event{
  179. Action: models.Update,
  180. Source: models.Subject{
  181. ID: user.UserName,
  182. Name: user.UserName,
  183. Type: models.UserSub,
  184. },
  185. TriggeredBy: user.UserName,
  186. Target: models.Subject{
  187. ID: networkID,
  188. Name: networkID,
  189. Type: models.NetworkSub,
  190. },
  191. NetworkID: models.NetworkID(networkID),
  192. Origin: models.Dashboard,
  193. })
  194. logic.ReturnSuccessResponse(w, r, "JIT disabled on network")
  195. }
  196. // handleApproveRequest - approves a JIT request
  197. func handleApproveRequest(w http.ResponseWriter, r *http.Request, networkID string, user *models.User, requestID string, expiresAtEpoch int64) {
  198. // Check if user is admin
  199. if !proLogic.IsNetworkAdmin(user, networkID) {
  200. logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("only network admins can approve requests"), "forbidden"))
  201. return
  202. }
  203. if requestID == "" {
  204. logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("request_id is required"), "badrequest"))
  205. return
  206. }
  207. if expiresAtEpoch <= 0 {
  208. logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("expires_at is required and must be a valid Unix epoch timestamp"), "badrequest"))
  209. return
  210. }
  211. // Convert epoch to time.Time
  212. expiresAt := time.Unix(expiresAtEpoch, 0).UTC()
  213. now := time.Now().UTC()
  214. // Validate that expires_at is in the future
  215. if expiresAt.Before(now) || expiresAt.Equal(now) {
  216. logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("expires_at must be in the future"), "badrequest"))
  217. return
  218. }
  219. grant, req, err := proLogic.ApproveJITRequest(requestID, expiresAt, user.UserName)
  220. if err != nil {
  221. logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
  222. return
  223. }
  224. // Send approval email to user
  225. go func() {
  226. network, _ := logic.GetNetwork(networkID)
  227. if err := email.SendJITApprovalEmail(grant, req, network); err != nil {
  228. slog.Error("failed to send approval notification", "error", err)
  229. }
  230. }()
  231. logic.LogEvent(&models.Event{
  232. Action: models.Update,
  233. Source: models.Subject{
  234. ID: user.UserName,
  235. Name: user.UserName,
  236. Type: models.UserSub,
  237. },
  238. TriggeredBy: user.UserName,
  239. Target: models.Subject{
  240. ID: requestID,
  241. Name: networkID,
  242. Type: models.NetworkSub,
  243. },
  244. NetworkID: models.NetworkID(networkID),
  245. Origin: models.Dashboard,
  246. })
  247. logic.ReturnSuccessResponseWithJson(w, r, grant, "JIT request approved")
  248. }
  249. // handleDenyRequest - denies a JIT request
  250. func handleDenyRequest(w http.ResponseWriter, r *http.Request, networkID string, user *models.User, requestID string) {
  251. // Check if user is admin
  252. if !proLogic.IsNetworkAdmin(user, networkID) {
  253. logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("only network admins can deny requests"), "forbidden"))
  254. return
  255. }
  256. if requestID == "" {
  257. logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("request_id is required"), "badrequest"))
  258. return
  259. }
  260. request, err := proLogic.DenyJITRequest(requestID, user.UserName)
  261. if err != nil {
  262. logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
  263. return
  264. }
  265. // Send denial email to requester
  266. go func() {
  267. network, _ := logic.GetNetwork(networkID)
  268. if err := email.SendJITDeniedEmail(request, network); err != nil {
  269. slog.Error("failed to send JIT denied notification", "error", err)
  270. }
  271. }()
  272. logic.LogEvent(&models.Event{
  273. Action: models.Update,
  274. Source: models.Subject{
  275. ID: user.UserName,
  276. Name: user.UserName,
  277. Type: models.UserSub,
  278. },
  279. TriggeredBy: user.UserName,
  280. Target: models.Subject{
  281. ID: requestID,
  282. Name: networkID,
  283. Type: models.NetworkSub,
  284. },
  285. NetworkID: models.NetworkID(networkID),
  286. Origin: models.Dashboard,
  287. })
  288. logic.ReturnSuccessResponse(w, r, "JIT request denied")
  289. }
  290. // @Summary Delete/revoke a JIT grant
  291. // @Router /api/v1/jit [delete]
  292. // @Tags JIT
  293. // @Security oauth
  294. // @Produce json
  295. // @Param network query string true "Network ID"
  296. // @Param grant_id query string true "Grant ID to revoke"
  297. // @Success 200 {object} models.SuccessResponse
  298. // @Failure 400 {object} models.ErrorResponse
  299. // @Failure 500 {object} models.ErrorResponse
  300. func deleteJITGrant(w http.ResponseWriter, r *http.Request) {
  301. // Check if JIT feature is enabled
  302. featureFlags := logic.GetFeatureFlags()
  303. if !featureFlags.EnableJIT {
  304. logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("JIT feature is not enabled"), "forbidden"))
  305. return
  306. }
  307. networkID := r.URL.Query().Get("network")
  308. grantID := r.URL.Query().Get("grant_id")
  309. if networkID == "" || grantID == "" {
  310. logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("network and grant_id are required"), "badrequest"))
  311. return
  312. }
  313. username := r.Header.Get("user")
  314. if username == "" {
  315. logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("user not found in request"), "unauthorized"))
  316. return
  317. }
  318. user, err := logic.GetUser(username)
  319. if err != nil {
  320. logic.ReturnErrorResponse(w, r, logic.FormatError(err, "unauthorized"))
  321. return
  322. }
  323. // Check if user is admin
  324. if !proLogic.IsNetworkAdmin(user, networkID) {
  325. logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("only network admins can revoke grants"), "forbidden"))
  326. return
  327. }
  328. ctx := db.WithContext(r.Context())
  329. grant := schema.JITGrant{ID: grantID}
  330. if err := grant.Get(ctx); err != nil {
  331. logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
  332. return
  333. }
  334. if grant.NetworkID != networkID {
  335. logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("grant does not belong to this network"), "badrequest"))
  336. return
  337. }
  338. // Delete all grants for this user on this network (in case there are multiple)
  339. if err := proLogic.DeactivateUserGrantsOnNetwork(networkID, grant.UserID); err != nil {
  340. logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
  341. return
  342. }
  343. // Update associated request status to "expired" for all approved requests from this user
  344. request := schema.JITRequest{
  345. NetworkID: networkID,
  346. UserID: grant.UserID,
  347. }
  348. allRequests, err := request.ListByNetwork(ctx)
  349. var revokedRequest *schema.JITRequest
  350. if err == nil {
  351. for _, req := range allRequests {
  352. if req.UserID == grant.UserID && req.Status == "approved" {
  353. req.Status = "expired"
  354. req.RevokedAt = time.Now().UTC()
  355. if err := req.Update(ctx); err != nil {
  356. logger.Log(0, "failed to update request status when revoking grant:", err.Error())
  357. // Don't fail the operation, just log
  358. } else {
  359. // Use the first approved request for email notification
  360. if revokedRequest == nil {
  361. revokedRequest = &req
  362. }
  363. }
  364. }
  365. }
  366. }
  367. // Send email notification to user
  368. if revokedRequest != nil {
  369. network, err := logic.GetNetwork(networkID)
  370. if err == nil {
  371. if err := email.SendJITExpirationEmail(&grant, revokedRequest, network, true, user.UserName); err != nil {
  372. slog.Warn("failed to send revocation email", "grant_id", grantID, "user", revokedRequest.UserName, "error", err)
  373. }
  374. }
  375. }
  376. // Disconnect user's ext clients from the network
  377. if err := proLogic.DisconnectUserExtClientsFromNetwork(networkID, grant.UserID); err != nil {
  378. logger.Log(0, "failed to disconnect ext clients when revoking grant:", err.Error())
  379. }
  380. logic.LogEvent(&models.Event{
  381. Action: models.Delete,
  382. Source: models.Subject{
  383. ID: user.UserName,
  384. Name: user.UserName,
  385. Type: models.UserSub,
  386. },
  387. TriggeredBy: user.UserName,
  388. Target: models.Subject{
  389. ID: grantID,
  390. Name: networkID,
  391. Type: models.NetworkSub,
  392. },
  393. NetworkID: models.NetworkID(networkID),
  394. Origin: models.Dashboard,
  395. })
  396. logic.ReturnSuccessResponse(w, r, "JIT grant revoked")
  397. }
  398. // @Summary Get user JIT networks status
  399. // @Router /api/v1/jit_user/networks [get]
  400. // @Tags JIT
  401. // @Security oauth
  402. // @Produce json
  403. // @Success 200 {array} models.UserJITNetworkStatus
  404. // @Failure 400 {object} models.ErrorResponse
  405. // @Failure 500 {object} models.ErrorResponse
  406. func getUserJITNetworks(w http.ResponseWriter, r *http.Request) {
  407. // Check if JIT feature is enabled
  408. featureFlags := logic.GetFeatureFlags()
  409. if !featureFlags.EnableJIT {
  410. logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("JIT feature is not enabled"), "forbidden"))
  411. return
  412. }
  413. username := r.Header.Get("user")
  414. if username == "" {
  415. logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("user not found in request"), "unauthorized"))
  416. return
  417. }
  418. user, err := logic.GetUser(username)
  419. if err != nil {
  420. logic.ReturnErrorResponse(w, r, logic.FormatError(err, "unauthorized"))
  421. return
  422. }
  423. // Get all networks user has access to
  424. allNetworks, err := logic.GetNetworks()
  425. if err != nil {
  426. logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
  427. return
  428. }
  429. // Filter networks by user role
  430. userNetworks := logic.FilterNetworksByRole(allNetworks, *user)
  431. // Build response with JIT status for each network
  432. networksWithJITStatus, err := proLogic.GetUserJITNetworksStatus(userNetworks, user.UserName)
  433. if err != nil {
  434. logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
  435. return
  436. }
  437. logic.ReturnSuccessResponseWithJson(w, r, networksWithJITStatus, "fetched user JIT network status")
  438. }
  439. // @Summary Request JIT access to a network
  440. // @Router /api/v1/jit_user/request [post]
  441. // @Tags JIT
  442. // @Security oauth
  443. // @Accept json
  444. // @Produce json
  445. // @Param network query string true "Network ID"
  446. // @Param body body models.JITAccessRequest true "JIT access request"
  447. // @Success 200 {object} schema.JITRequest
  448. // @Failure 400 {object} models.ErrorResponse
  449. // @Failure 500 {object} models.ErrorResponse
  450. func requestJITAccess(w http.ResponseWriter, r *http.Request) {
  451. // Check if JIT feature is enabled
  452. featureFlags := logic.GetFeatureFlags()
  453. if !featureFlags.EnableJIT {
  454. logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("JIT feature is not enabled"), "forbidden"))
  455. return
  456. }
  457. username := r.Header.Get("user")
  458. if username == "" {
  459. logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("user not found in request"), "unauthorized"))
  460. return
  461. }
  462. network := r.URL.Query().Get("network")
  463. user, err := logic.GetUser(username)
  464. if err != nil {
  465. logic.ReturnErrorResponse(w, r, logic.FormatError(err, "unauthorized"))
  466. return
  467. }
  468. var req models.JITAccessRequest
  469. err = json.NewDecoder(r.Body).Decode(&req)
  470. if err != nil {
  471. logger.Log(0, "error decoding request body:", err.Error())
  472. logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
  473. return
  474. }
  475. req.NetworkID = network
  476. // Validate required fields
  477. if req.NetworkID == "" {
  478. logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("network_id is required"), "badrequest"))
  479. return
  480. }
  481. if req.Reason == "" {
  482. logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("reason is required"), "badrequest"))
  483. return
  484. }
  485. // Check if user has access to the network by role
  486. allNetworks, err := logic.GetNetworks()
  487. if err != nil {
  488. logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
  489. return
  490. }
  491. // Filter networks by user role
  492. userNetworks := logic.FilterNetworksByRole(allNetworks, *user)
  493. hasAccess := false
  494. for _, network := range userNetworks {
  495. if network.NetID == req.NetworkID {
  496. hasAccess = true
  497. break
  498. }
  499. }
  500. if !hasAccess {
  501. logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("user does not have access to this network"), "forbidden"))
  502. return
  503. }
  504. // Create the JIT request
  505. request, err := proLogic.CreateJITRequest(req.NetworkID, user.UserName, req.Reason)
  506. if err != nil {
  507. logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
  508. return
  509. }
  510. // Send email notifications to network admins
  511. go func() {
  512. network, _ := logic.GetNetwork(req.NetworkID)
  513. if err := email.SendJITRequestEmails(request, network); err != nil {
  514. slog.Error("failed to send JIT request notifications", "error", err)
  515. }
  516. }()
  517. logic.LogEvent(&models.Event{
  518. Action: models.Create,
  519. Source: models.Subject{
  520. ID: user.UserName,
  521. Name: user.UserName,
  522. Type: models.UserSub,
  523. },
  524. TriggeredBy: user.UserName,
  525. Target: models.Subject{
  526. ID: request.ID,
  527. Name: req.NetworkID,
  528. Type: models.NetworkSub,
  529. },
  530. NetworkID: models.NetworkID(req.NetworkID),
  531. Origin: models.ClientApp,
  532. })
  533. logic.ReturnSuccessResponseWithJson(w, r, request, "JIT access request created")
  534. }