mirror of
https://github.com/gravitl/netmaker.git
synced 2026-10-12 02:15:06 +00:00
* wip(go): return auth token with user on create org owner; * wip(go): prevent use of default tenant slug ; * wip(go): upsert settings for tenant in ctx; * wip(go): use passed ctx when updating user; remove password validation from ValidateUser (already validated in UpdateUser); * wip(go): qualify all tenant id usages with table name; * wip(go): use passed ctx * wip(go): return old enrollment key type on create; * wip(go): prevent null networks return; * wip(go): wire ctx in dns/nameserver functions; wire ctx in host registration functions; * wip(go): wire ctx in node functions; * wip(go): wire ctx in node functions; * wip(go): wire ctx in extclient functions; * wip(go): remove not needed field; * wip(go): use passed ctx; * wip(go): use unique index instead of primary key; * wip(go): wire ctx in extclient apis; * wip(go): wire ctx in extclient apis; * wip(go): remove org id check from global scoped get tenant api; * wip(go): add scope slug to discover response; * wip(go): wire ctx in get users; * wip(go): set tenant id in posture check violations; * wip(go): upsert membership instead of user; * wip(go): pass correct len; * wip(go): optimize (single loop); * wip(go): unify todos; * wip(go): add todo; * wip(go): wire context for network get; * wip(go): wire context; * wip(go): wire context; * wip(go): wire ctx when getting tenant superadmin; * wip(go): add network creator to network admin group; * wip(go): check for user membership on sync; * wip(go): add methods with strict membership check; * wip(go): remove unwanted feature flags; * wip(go): report per-tenant usage during license validation; * wip(go): move email setting from tenant to org; * wip(go): pass org scope to email init; * wip(go): remove old tenant email settings usages; use org settings to get email settings * wip(go): use get email settings from org settings; * wip(go): remove email settings migration; * wip(go): migrate email settings from server to org settings; * wip(go): assign global network admin group membership only for super-admin and admins; * wip(go): assign global network admin group membership only for super-admin and admins; * wip(go): add membership alias to filter; * wip(go): skip clearing local license validation response cache; * wip(go): extend license validation response for multi-tenancy; * wip(go): add functions to get sole tenant and org; * wip(go): use tenant-specific feature flags; * wip(go): add tenant status type; * wip(go): remove old hook comment; * wip(go): expect ctx during license validation; * wip(go): always create default org and tenant for ce; * wip(go): add tenant-specific check for license validation error; * wip(go): cache license validation response in memory; * wip(go): add todo to handle multiple-tenant for integration pull; * wip(go): pass ctx during license validation; * wip(go): add todo for making changes later; run hook separately for each tenant; * wip(go): return specific errors when license validation response not in cache; * wip(go): use retryable.httpclient for calling license validation api; * wip(go): process license validation; * wip(go): license validation function; * wip(go): migration on license validation; * wip(go): org settings not expected; * wip(go): remove expiry from license validation response; * wip(go): add limits and limits enforcement to license validation response; * wip(go): remove expiry usages; * wip(go): add arg to clear cache; * wip(go): add arg to clear cache; * wip(go): skip license validation if cached resp; * wip(go): allow org_id and tenant_id query params; * wip(go): upsert tenant membership on sync; * wip(go): pass ctx to delete network roles; * wip(go): create tenant membership on org owner creation on ce; * wip(go): add tenant orchestrator; * wip(go): use tenant orchestrator; * wip(go): add default settings for tenant; * wip(go): make org owner superadmin on all tenants; * wip(go): simplify SyncOrgAndTenants; * wip(go): retry license validation only 3 times * wip(go): store invalid err; * wip(go): add function to check if limits are to be enforced on a tenant; * wip(go): pass tenant scope if possible when checking for license validation error; * wip(go): remove merge errors; * wip(go): add functions for limit checks; * wip(go): wire host limit check; * wip(go): wire ingress limit check; * wip(go): wire egress limit check; * wip(go): wire client limit check; * wip(go): wire network limit check; * wip(go): wire user limit check; * wip(go): use correct errors; * wip(go): skip telemetry for msps; * wip(go): disable telemetry on msp tenants; * wip(go): err check; * wip(go): uncomment log event; * wip(go): get sso state to get scope for non-headless oauth login; * wip(go): inline delete pending user; * wip(go): allow both org and tenant scope for pending users table; * wip(go): add migration for pending user table during set tenant id and rekey org/tenant id; * wip(go): update queries to use new scope fields in pending users table; * wip(go): allow both org and tenant scope for user invites table; * wip(go): update queries to use new scope fields; * wip(go): add migration for rekey-ing org/tenant id and setting tenant id; * wip(go): inline delete user invite; * wip(go): set scope when creating invite; * wip(go): get user count with membership is ctx is tenant scoped; * wip(go): add methods to upsert and delete org memberships; * wip(go): add global valid tenant and org roles map; * wip(go): add scope query in user invite get by email; * wip(go): add support for upsert and delete of org and tenant membership both; * wip(go): infer scope for user invite apis; * wip(go): wire ctx in get user invite by email; * wip(go): allow no scope headers to be passed in infer scope apis; * wip(go): add validation and checks for org-scoped invite api handlers; * wip(go): wire ctx to is pending user check; * wip(go): infer scope for pending user apis; * wip(go): set default role based on ctx scope; * wip(go): move mfa to tenant membership table; move account status to membership tables; * wip(go): migrate account status and mfa settings; * wip(go): update account status on sync; * wip(go): use update mfa to update mfa setting; * wip(go): set new membership settings on user creation; * wip(go): use membership where required; * wip(go): wire ctx on key unlink refs; * wip(go): use r.Context directly without reinitializing db; * wip(go): wire ctx in acls; * wip(go): add tenant scoped primary key constructor; * wip(go): use tenant scoped primary key for acls, dns, extclients and tags; * wip(go): wire ctx for get groups in network; * wip(go): wire ctx for get groups; * wip(go): scope non-uuid group ids by tenant id; * wip(go): migrate non-uuid group ids to scoped ids; * wip(go): don't expect scope id in invite validation and signup apis; * wip(go): wire ctx for get matching user; allow org scope when querying by external idp id; * wip(go): add mfa to org; * wip(go): scope preauth token; * wip(go): add fallback for older auth tokens; * wip(go): wire ctx for preauth token creation; * wip(go): migrate mfa settings; * wip(go): resolve mfa settings for inherited auth; * wip(go): update mfa for org; * wip(go): infer scope for totp apis; * wip(go): use ctx when passed; * wip(go): infer scope for list user apis; * wip(go): ensure set primary keys don't cause errors during fetch; * wip(go): make primary key for integrations uuid and store provider in separate key; * wip(go): migrate integration ids; * wip(go): use provider instead of id everywhere; * wip(go): remove gorm column; * wip(go): wire ctx for igw egress creation; * wip(go): wire ctx for igw egress update; * wip(go): wire ctx for host registration; * wip(go): wire ctx for host registration; * wip(go): merge errors; * wip(go): merge errors; * wip(go): wire ctx; remove default scope usages; * wip(go): use get with membership; * wip(go): org scope license validation; * wip(go): remove debug log; * wip(go): remove scoping on list roles; * wip(go): ignore invite creation if already exists; * wip(go): add is msp func to check if deployment is msp deployment; * wip(go): ensure username is email for msp users; * wip(go): add email validated check on user table; * wip(go): mark email validated when oauth user; * wip(go): handle gorm error; * wip(go): add tenant id to siem grpc schema; * wip(go): read tenant from scoped ctx; * wip(go): pass scoped ctx to siem grpc client; * wip(go): remove unused publisher; * wip(go): publish all tenant exporter updates on pull; * wip(go): fix wg usage; * wip(go): tenant scope the ctx when getting server info; * wip(go): add tenant id to enrollment key token; * wip(go): make host register tenant scope; * wip(go): add default settings; * wip(go): use correct checks for is msp tenant; * wip(go): add email validation extension; * wip(go): add pages for validation check; * wip(go): use extension for validating user; * wip(go): add invite type; * wip(go): create only one validation invite per user email; * wip(go): add validate email handler; * wip(go): add email builder; * wip(go): add user invite type migration logic; * wip(go): only log email send failures; * wip(go): add tenant id to flows table; * wip(go): add func to check if connected to clickhouse; * wip(go): migrate clickhouse; * wip(go): add tenant id to mdm and edr state; * wip(go): add delete methods for tables; * wip(go): add tenant teardown method; * wip(go): handle tenant deletion; * wip(go): clean up flow logs on tenant deletion; * wip(go): add update org membership role id method; * wip(go): add transfer org ownership api; * wip(go): move audit logs retention period to org settings; * wip(go):get retention period from org settings; * wip(go): remove old audit logs usage; * wip(go): migrate audit logs retention period; * wip(go): unmarshal decodes nums as float64; * wip(go): run flow logs cleanup for all tenants in one go; * wip(go): add audit logs retention period and smtp settings to tenant settings table for non-msp tenants; * wip(go): resolve retention period and smtp settings correctly; * wip(go): handle retention period and smtp settings updates and fetches correctly; * wip(go): overlay retention period and smtp settings on server settings;; * wip(go): upsert overlayed settings; * wip(go): add azure tenant id to org settings; * wip(go): tenant scope list roles; * wip(go): add noop creds; * wip(go): use first instead of find; * wip(go): use correct roles when transfering ownership; * wip(go): delete access tokens only if tenant scoped; * wip(go): make delete user work for both org and tenant scope; * wip(go): delete extclients only if tenant scoped; * wip(go): check if caller is allowed to delete user; * wip(go): infer scope for update user; * wip(go): prevent org-owner elevation; * wip(go): permissions check for update; * wip(go): delete user access tokens only if tenant scope; * wip(go): delete extclients only if tenant scope; * wip(go): move upsert membership in upsert user; * wip(go): set api host when constructing server config; * wip(go): fix static errors; * wip(go): fix test cases; * wip(go): use correct get ns order; * wip(go): use correct get pending host order; * wip(go): use correct get access token order; * wip(go): use correct get event order; * wip(go): return error response on emqx user creation error; * wip(go): store correct value in cache; * wip(go): return better error response; * wip(go): return better error response; * wip(go): use cache when listing egress acls; * wip(go): wire ctx in UserHasNetworkGroupAccess; * wip(go): remove double scope wrapping; * wip(go): add security check for list and get org; * wip(go): ratelimit discover api; * wip(go): expect sole org id in get and create org owner; * wip(go): wrap create org owner in tx;
723 lines
23 KiB
Go
723 lines
23 KiB
Go
package controllers
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/gorilla/mux"
|
|
"github.com/gravitl/netmaker/db"
|
|
"github.com/gravitl/netmaker/logger"
|
|
"github.com/gravitl/netmaker/logic"
|
|
"github.com/gravitl/netmaker/middleware"
|
|
"github.com/gravitl/netmaker/models"
|
|
"github.com/gravitl/netmaker/pro/email"
|
|
proLogic "github.com/gravitl/netmaker/pro/logic"
|
|
"github.com/gravitl/netmaker/schema"
|
|
"github.com/gravitl/netmaker/scope"
|
|
"golang.org/x/exp/slog"
|
|
)
|
|
|
|
func JITHandlers(r *mux.Router) {
|
|
r.HandleFunc("/api/v1/jit", middleware.Scope(scope.TenantScope, logic.SecurityCheck(true,
|
|
http.HandlerFunc(handleJIT)))).Methods(http.MethodPost, http.MethodGet)
|
|
|
|
r.HandleFunc("/api/v1/jit", middleware.Scope(scope.TenantScope, logic.SecurityCheck(true,
|
|
http.HandlerFunc(deleteJITGrant)))).Methods(http.MethodDelete)
|
|
|
|
r.HandleFunc("/api/v1/jit_user/networks", middleware.Scope(scope.TenantScope, logic.SecurityCheck(false,
|
|
http.HandlerFunc(getUserJITNetworks)))).Methods(http.MethodGet)
|
|
|
|
r.HandleFunc("/api/v1/jit_user/request", middleware.Scope(scope.TenantScope, logic.SecurityCheck(false,
|
|
http.HandlerFunc(requestJITAccess)))).Methods(http.MethodPost)
|
|
}
|
|
|
|
// @Summary List JIT requests for a network
|
|
// @Router /api/v1/jit [get]
|
|
// @Tags JIT
|
|
// @Security oauth
|
|
// @Produce json
|
|
// @Param network query string true "Network ID"
|
|
// @Param status query string false "Filter by status (pending, approved, denied, expired)"
|
|
// @Param page query int false "Page number"
|
|
// @Param per_page query int false "Items per page"
|
|
// @Success 200 {array} schema.JITRequest
|
|
// @Failure 400 {object} models.ErrorResponse
|
|
// @Failure 500 {object} models.ErrorResponse
|
|
//
|
|
// @Summary Handle JIT operations (enable, disable, approve, deny)
|
|
// @Router /api/v1/jit [post]
|
|
// @Tags JIT
|
|
// @Security oauth
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param network query string true "Network ID"
|
|
// @Param body body models.JITOperationRequest true "JIT operation request (user_group_ids for enable / update_jit_user_groups)"
|
|
// @Success 200 {object} models.SuccessResponse
|
|
// @Failure 400 {object} models.ErrorResponse
|
|
// @Failure 500 {object} models.ErrorResponse
|
|
func handleJIT(w http.ResponseWriter, r *http.Request) {
|
|
// Check if JIT feature is enabled
|
|
featureFlags := logic.GetFeatureFlags(r.Context())
|
|
if !featureFlags.EnableJIT {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("JIT feature is not enabled"), "forbidden"))
|
|
return
|
|
}
|
|
|
|
networkID := r.URL.Query().Get("network")
|
|
if networkID == "" {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("network is required"), "badrequest"))
|
|
return
|
|
}
|
|
|
|
username := r.Header.Get("user")
|
|
if username == "" {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("user not found in request"), "unauthorized"))
|
|
return
|
|
}
|
|
|
|
user := &schema.User{Username: username}
|
|
err := user.GetWithMembership(r.Context())
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "unauthorized"))
|
|
return
|
|
}
|
|
|
|
switch r.Method {
|
|
case http.MethodGet:
|
|
handleJITGet(w, r, networkID, user)
|
|
case http.MethodPost:
|
|
handleJITPost(w, r, networkID, user)
|
|
default:
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("method not allowed"), "badrequest"))
|
|
}
|
|
}
|
|
|
|
// handleJITGet - handles GET requests for JIT status/requests
|
|
func handleJITGet(w http.ResponseWriter, r *http.Request, networkID string, user *schema.User) {
|
|
statusFilter := r.URL.Query().Get("status") // "pending", "approved", "denied", "expired", or empty for all
|
|
|
|
// Parse pagination parameters (default to 0, db.SetPagination will apply defaults)
|
|
page, _ := strconv.Atoi(r.URL.Query().Get("page"))
|
|
pageSize, _ := strconv.Atoi(r.URL.Query().Get("per_page"))
|
|
|
|
// Apply defaults if not provided (matching db.SetPagination logic)
|
|
if page < 1 {
|
|
page = 1
|
|
}
|
|
if pageSize < 1 || pageSize > 100 {
|
|
pageSize = 10
|
|
}
|
|
|
|
ctx := db.WithContext(r.Context())
|
|
requests, total, err := proLogic.GetNetworkJITRequestsPaginated(ctx, networkID, statusFilter, page, pageSize)
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
|
return
|
|
}
|
|
|
|
// Calculate pagination metadata
|
|
totalPages := (int(total) + pageSize - 1) / pageSize
|
|
if totalPages == 0 {
|
|
totalPages = 1
|
|
}
|
|
|
|
response := models.PaginatedResponse{
|
|
Data: requests,
|
|
Page: page,
|
|
PerPage: pageSize,
|
|
Total: int(total),
|
|
TotalPages: totalPages,
|
|
}
|
|
|
|
logic.ReturnSuccessResponseWithJson(w, r, response, "fetched JIT requests")
|
|
}
|
|
|
|
// handleJITPost - handles POST requests for JIT operations
|
|
func handleJITPost(w http.ResponseWriter, r *http.Request, networkID string, user *schema.User) {
|
|
var req models.JITOperationRequest
|
|
err := json.NewDecoder(r.Body).Decode(&req)
|
|
if err != nil {
|
|
logger.Log(0, "error decoding request body:", err.Error())
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
|
|
switch req.Action {
|
|
case "enable":
|
|
handleEnableJIT(w, r, networkID, user, req.UserGroupIDs)
|
|
case "disable":
|
|
handleDisableJIT(w, r, networkID, user)
|
|
case "update_jit_user_groups":
|
|
handleUpdateJITUserGroups(w, r, networkID, user, req.UserGroupIDs)
|
|
case "approve":
|
|
handleApproveRequest(w, r, networkID, user, req.RequestID, req.ExpiresAt)
|
|
case "deny":
|
|
handleDenyRequest(w, r, networkID, user, req.RequestID)
|
|
default:
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("invalid action"), "badrequest"))
|
|
}
|
|
}
|
|
|
|
// handleEnableJIT - enables JIT on a network
|
|
func handleEnableJIT(w http.ResponseWriter, r *http.Request, networkID string, user *schema.User, userGroupIDs []string) {
|
|
// Check if user is admin
|
|
if !proLogic.IsNetworkAdmin(r.Context(), user, networkID) {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("only network admins can enable JIT"), "forbidden"))
|
|
return
|
|
}
|
|
|
|
if err := proLogic.EnableJITOnNetwork(r.Context(), networkID, jitRequestToGroupIDs(userGroupIDs)); err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
|
return
|
|
}
|
|
|
|
logic.LogEvent(r.Context(), &models.Event{
|
|
Action: schema.JitEnable,
|
|
Source: models.Subject{
|
|
ID: user.Username,
|
|
Name: user.Username,
|
|
Type: schema.UserSub,
|
|
},
|
|
TriggeredBy: user.Username,
|
|
Target: models.Subject{
|
|
ID: networkID,
|
|
Name: networkID,
|
|
Type: schema.NetworkSub,
|
|
},
|
|
NetworkID: schema.NetworkID(networkID),
|
|
Origin: schema.Dashboard,
|
|
})
|
|
|
|
logic.ReturnSuccessResponse(w, r, "JIT enabled on network")
|
|
}
|
|
|
|
// handleDisableJIT - disables JIT on a network
|
|
func handleDisableJIT(w http.ResponseWriter, r *http.Request, networkID string, user *schema.User) {
|
|
// Check if user is admin
|
|
if !proLogic.IsNetworkAdmin(r.Context(), user, networkID) {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("only network admins can disable JIT"), "forbidden"))
|
|
return
|
|
}
|
|
|
|
if err := proLogic.DisableJITOnNetwork(r.Context(), networkID); err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
|
return
|
|
}
|
|
|
|
logic.LogEvent(r.Context(), &models.Event{
|
|
Action: schema.JitDisable,
|
|
Source: models.Subject{
|
|
ID: user.Username,
|
|
Name: user.Username,
|
|
Type: schema.UserSub,
|
|
},
|
|
TriggeredBy: user.Username,
|
|
Target: models.Subject{
|
|
ID: networkID,
|
|
Name: networkID,
|
|
Type: schema.NetworkSub,
|
|
},
|
|
NetworkID: schema.NetworkID(networkID),
|
|
Origin: schema.Dashboard,
|
|
})
|
|
|
|
logic.ReturnSuccessResponse(w, r, "JIT disabled on network")
|
|
}
|
|
|
|
// handleUpdateJITUserGroups - updates JIT user-group allowlist while JIT stays enabled
|
|
func handleUpdateJITUserGroups(w http.ResponseWriter, r *http.Request, networkID string, user *schema.User, userGroupIDs []string) {
|
|
if !proLogic.IsNetworkAdmin(r.Context(), user, networkID) {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("only network admins can update JIT user groups"), "forbidden"))
|
|
return
|
|
}
|
|
|
|
currNet := &schema.Network{Name: networkID}
|
|
err := (currNet).Get(r.Context())
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
|
return
|
|
}
|
|
|
|
// Snapshot the previous group names before we mutate the network, so the
|
|
// audit diff retains human-readable names even if any of the listed groups
|
|
// are deleted later.
|
|
oldSnapshot := newJITNetworkAuditSnapshot(r.Context(), currNet)
|
|
|
|
if err := proLogic.UpdateJITUserGroupsOnNetwork(r.Context(), networkID, jitRequestToGroupIDs(userGroupIDs)); err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
|
return
|
|
}
|
|
updatedNet := &schema.Network{Name: networkID}
|
|
err = (updatedNet).Get(r.Context())
|
|
|
|
if err != nil {
|
|
slog.Error("failed to fetch updated network for JIT user-group audit diff", "network", networkID, "error", err)
|
|
return
|
|
}
|
|
diff := models.Diff{
|
|
Old: oldSnapshot,
|
|
New: newJITNetworkAuditSnapshot(r.Context(), updatedNet),
|
|
}
|
|
logic.LogEvent(r.Context(), &models.Event{
|
|
Action: schema.JitGroupsUpdate,
|
|
Source: models.Subject{
|
|
ID: user.Username,
|
|
Name: user.Username,
|
|
Type: schema.JITSub,
|
|
},
|
|
TriggeredBy: user.Username,
|
|
Target: models.Subject{
|
|
ID: networkID,
|
|
Name: networkID,
|
|
Type: schema.JITSub,
|
|
},
|
|
NetworkID: schema.NetworkID(networkID),
|
|
Origin: schema.Dashboard,
|
|
Diff: diff,
|
|
})
|
|
|
|
logic.ReturnSuccessResponse(w, r, "JIT user groups updated")
|
|
}
|
|
|
|
// jitUserGroupRef pins a user-group ID alongside its display name at the moment
|
|
// of an audit event, so the event remains human-readable even after the
|
|
// underlying group is deleted.
|
|
type jitUserGroupRef struct {
|
|
ID schema.UserGroupID `json:"id"`
|
|
Name string `json:"name"`
|
|
}
|
|
|
|
// jitNetworkAuditSnapshot is the payload stored in JIT audit-event diffs.
|
|
// It embeds the network so existing consumers continue to see the same
|
|
// fields (including jit_user_group_ids), and adds a parallel jit_user_groups
|
|
// list with the resolved names captured at event time.
|
|
type jitNetworkAuditSnapshot struct {
|
|
*schema.Network
|
|
JITUserGroups []jitUserGroupRef `json:"jit_user_groups"`
|
|
}
|
|
|
|
func newJITNetworkAuditSnapshot(ctx context.Context, network *schema.Network) jitNetworkAuditSnapshot {
|
|
snap := jitNetworkAuditSnapshot{Network: network}
|
|
if network == nil {
|
|
return snap
|
|
}
|
|
snap.JITUserGroups = make([]jitUserGroupRef, 0, len(network.JITUserGroupIDs))
|
|
for _, gid := range network.JITUserGroupIDs {
|
|
ref := jitUserGroupRef{ID: gid}
|
|
grp := &schema.UserGroup{ID: gid}
|
|
if err := grp.Get(ctx); err == nil {
|
|
ref.Name = grp.Name
|
|
}
|
|
snap.JITUserGroups = append(snap.JITUserGroups, ref)
|
|
}
|
|
return snap
|
|
}
|
|
|
|
func jitRequestToGroupIDs(ids []string) []schema.UserGroupID {
|
|
out := make([]schema.UserGroupID, 0, len(ids))
|
|
for _, id := range ids {
|
|
if id == "" {
|
|
continue
|
|
}
|
|
out = append(out, schema.UserGroupID(id))
|
|
}
|
|
return out
|
|
}
|
|
|
|
// handleApproveRequest - approves a JIT request
|
|
func handleApproveRequest(w http.ResponseWriter, r *http.Request, networkID string, user *schema.User, requestID string, expiresAtEpoch int64) {
|
|
// Check if user is admin
|
|
if !proLogic.IsNetworkAdmin(r.Context(), user, networkID) {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("only network admins can approve requests"), "forbidden"))
|
|
return
|
|
}
|
|
|
|
if requestID == "" {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("request_id is required"), "badrequest"))
|
|
return
|
|
}
|
|
|
|
if expiresAtEpoch <= 0 {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("expires_at is required and must be a valid Unix epoch timestamp"), "badrequest"))
|
|
return
|
|
}
|
|
|
|
// Convert epoch to time.Time
|
|
expiresAt := time.Unix(expiresAtEpoch, 0).UTC()
|
|
now := time.Now().UTC()
|
|
|
|
// Validate that expires_at is in the future
|
|
if expiresAt.Before(now) || expiresAt.Equal(now) {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("expires_at must be in the future"), "badrequest"))
|
|
return
|
|
}
|
|
|
|
grant, req, err := proLogic.ApproveJITRequest(r.Context(), requestID, expiresAt, user.Username)
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
// Send approval email to user
|
|
ctx := scope.WithContext(db.WithContext(context.Background()), scope.Level(r.Context()), scope.ID(r.Context()))
|
|
go func(ctx context.Context) {
|
|
network := &schema.Network{Name: networkID}
|
|
_ = network.Get(ctx)
|
|
if err := email.SendJITApprovalEmail(grant, req, network); err != nil {
|
|
slog.Error("failed to send approval notification", "error", err)
|
|
}
|
|
}(ctx)
|
|
logic.LogEvent(r.Context(), &models.Event{
|
|
Action: schema.JitRequestApprove,
|
|
Source: models.Subject{
|
|
ID: user.Username,
|
|
Name: user.Username,
|
|
Type: schema.UserSub,
|
|
},
|
|
TriggeredBy: user.Username,
|
|
Target: models.Subject{
|
|
ID: grant.UserID,
|
|
Name: grant.UserID,
|
|
Type: schema.UserSub,
|
|
},
|
|
NetworkID: schema.NetworkID(networkID),
|
|
Origin: schema.Dashboard,
|
|
})
|
|
|
|
logic.ReturnSuccessResponseWithJson(w, r, grant, "JIT request approved")
|
|
}
|
|
|
|
// handleDenyRequest - denies a JIT request
|
|
func handleDenyRequest(w http.ResponseWriter, r *http.Request, networkID string, user *schema.User, requestID string) {
|
|
// Check if user is admin
|
|
if !proLogic.IsNetworkAdmin(r.Context(), user, networkID) {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("only network admins can deny requests"), "forbidden"))
|
|
return
|
|
}
|
|
|
|
if requestID == "" {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("request_id is required"), "badrequest"))
|
|
return
|
|
}
|
|
|
|
request, err := proLogic.DenyJITRequest(requestID, user.Username)
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
|
|
// Send denial email to requester
|
|
ctx := scope.WithContext(db.WithContext(context.Background()), scope.Level(r.Context()), scope.ID(r.Context()))
|
|
go func(ctx context.Context) {
|
|
network := &schema.Network{Name: networkID}
|
|
_ = network.Get(ctx)
|
|
if err := email.SendJITDeniedEmail(request, network); err != nil {
|
|
slog.Error("failed to send JIT denied notification", "error", err)
|
|
}
|
|
}(ctx)
|
|
|
|
logic.LogEvent(r.Context(), &models.Event{
|
|
Action: schema.JitRequestDeny,
|
|
Source: models.Subject{
|
|
ID: user.Username,
|
|
Name: user.Username,
|
|
Type: schema.UserSub,
|
|
},
|
|
TriggeredBy: user.Username,
|
|
Target: models.Subject{
|
|
ID: request.UserID,
|
|
Name: request.UserID,
|
|
Type: schema.UserSub,
|
|
},
|
|
NetworkID: schema.NetworkID(networkID),
|
|
Origin: schema.Dashboard,
|
|
})
|
|
|
|
logic.ReturnSuccessResponse(w, r, "JIT request denied")
|
|
}
|
|
|
|
// @Summary Delete/revoke a JIT grant
|
|
// @Router /api/v1/jit [delete]
|
|
// @Tags JIT
|
|
// @Security oauth
|
|
// @Produce json
|
|
// @Param network query string true "Network ID"
|
|
// @Param grant_id query string true "Grant ID to revoke"
|
|
// @Success 200 {object} models.SuccessResponse
|
|
// @Failure 400 {object} models.ErrorResponse
|
|
// @Failure 500 {object} models.ErrorResponse
|
|
func deleteJITGrant(w http.ResponseWriter, r *http.Request) {
|
|
// Check if JIT feature is enabled
|
|
featureFlags := logic.GetFeatureFlags(r.Context())
|
|
if !featureFlags.EnableJIT {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("JIT feature is not enabled"), "forbidden"))
|
|
return
|
|
}
|
|
|
|
networkID := r.URL.Query().Get("network")
|
|
grantID := r.URL.Query().Get("grant_id")
|
|
|
|
if networkID == "" || grantID == "" {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("network and grant_id are required"), "badrequest"))
|
|
return
|
|
}
|
|
|
|
username := r.Header.Get("user")
|
|
if username == "" {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("user not found in request"), "unauthorized"))
|
|
return
|
|
}
|
|
|
|
user := &schema.User{Username: username}
|
|
err := user.GetWithMembership(r.Context())
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "unauthorized"))
|
|
return
|
|
}
|
|
|
|
// Check if user is admin
|
|
if !proLogic.IsNetworkAdmin(r.Context(), user, networkID) {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("only network admins can revoke grants"), "forbidden"))
|
|
return
|
|
}
|
|
|
|
grant := schema.JITGrant{ID: grantID}
|
|
if err := grant.Get(r.Context()); err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
|
|
if grant.NetworkID != networkID {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("grant does not belong to this network"), "badrequest"))
|
|
return
|
|
}
|
|
|
|
// Delete all grants for this user on this network (in case there are multiple)
|
|
if err := proLogic.DeactivateUserGrantsOnNetwork(r.Context(), networkID, grant.UserID); err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
|
return
|
|
}
|
|
|
|
// Update associated request status to "expired" for all approved requests from this user
|
|
request := schema.JITRequest{
|
|
NetworkID: networkID,
|
|
UserID: grant.UserID,
|
|
}
|
|
allRequests, err := request.ListByNetwork(r.Context())
|
|
var revokedRequest *schema.JITRequest
|
|
if err == nil {
|
|
for _, req := range allRequests {
|
|
if req.UserID == grant.UserID && req.Status == "approved" {
|
|
req.Status = "expired"
|
|
req.RevokedAt = time.Now().UTC()
|
|
if err := req.Update(r.Context()); err != nil {
|
|
logger.Log(0, "failed to update request status when revoking grant:", err.Error())
|
|
// Don't fail the operation, just log
|
|
} else {
|
|
// Use the first approved request for email notification
|
|
if revokedRequest == nil {
|
|
revokedRequest = &req
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Send email notification to user
|
|
if revokedRequest != nil {
|
|
network := &schema.Network{Name: networkID}
|
|
err := network.Get(r.Context())
|
|
if err == nil {
|
|
if err := email.SendJITExpirationEmail(&grant, revokedRequest, network, true, user.Username); err != nil {
|
|
slog.Warn("failed to send revocation email", "grant_id", grantID, "user", revokedRequest.UserName, "error", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Remove user's ext clients and host nodes from the network.
|
|
if err := proLogic.RemoveUserJITNetworkAccess(r.Context(), networkID, grant.UserID); err != nil {
|
|
logger.Log(0, "failed to remove network access when revoking grant:", err.Error())
|
|
}
|
|
|
|
logic.LogEvent(r.Context(), &models.Event{
|
|
Action: schema.JitGrantRevoke,
|
|
Source: models.Subject{
|
|
ID: user.Username,
|
|
Name: user.Username,
|
|
Type: schema.UserSub,
|
|
},
|
|
TriggeredBy: user.Username,
|
|
Target: models.Subject{
|
|
ID: grant.UserID,
|
|
Name: grant.UserID,
|
|
Type: schema.UserSub,
|
|
},
|
|
NetworkID: schema.NetworkID(networkID),
|
|
Origin: schema.Dashboard,
|
|
})
|
|
|
|
logic.ReturnSuccessResponse(w, r, "JIT grant revoked")
|
|
}
|
|
|
|
// @Summary Get user JIT networks status
|
|
// @Router /api/v1/jit_user/networks [get]
|
|
// @Tags JIT
|
|
// @Security oauth
|
|
// @Produce json
|
|
// @Success 200 {array} models.UserJITNetworkStatus
|
|
// @Failure 400 {object} models.ErrorResponse
|
|
// @Failure 500 {object} models.ErrorResponse
|
|
func getUserJITNetworks(w http.ResponseWriter, r *http.Request) {
|
|
// Check if JIT feature is enabled
|
|
featureFlags := logic.GetFeatureFlags(r.Context())
|
|
if !featureFlags.EnableJIT {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("JIT feature is not enabled"), "forbidden"))
|
|
return
|
|
}
|
|
|
|
username := r.Header.Get("user")
|
|
if username == "" {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("user not found in request"), "unauthorized"))
|
|
return
|
|
}
|
|
|
|
user := &schema.User{Username: username}
|
|
err := user.GetWithMembership(r.Context())
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "unauthorized"))
|
|
return
|
|
}
|
|
|
|
// Get all networks user has access to
|
|
allNetworks, err := (&schema.Network{}).ListAll(r.Context())
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
|
return
|
|
}
|
|
|
|
// Filter networks by user role
|
|
userNetworks := logic.FilterNetworksByRole(r.Context(), allNetworks, user)
|
|
|
|
// Build response with JIT status for each network
|
|
networksWithJITStatus, err := proLogic.GetUserJITNetworksStatus(r.Context(), userNetworks, user)
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
|
return
|
|
}
|
|
|
|
logic.ReturnSuccessResponseWithJson(w, r, networksWithJITStatus, "fetched user JIT network status")
|
|
}
|
|
|
|
// @Summary Request JIT access to a network
|
|
// @Router /api/v1/jit_user/request [post]
|
|
// @Tags JIT
|
|
// @Security oauth
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param network query string true "Network ID"
|
|
// @Param body body models.JITAccessRequest true "JIT access request"
|
|
// @Success 200 {object} schema.JITRequest
|
|
// @Failure 400 {object} models.ErrorResponse
|
|
// @Failure 500 {object} models.ErrorResponse
|
|
func requestJITAccess(w http.ResponseWriter, r *http.Request) {
|
|
// Check if JIT feature is enabled
|
|
featureFlags := logic.GetFeatureFlags(r.Context())
|
|
if !featureFlags.EnableJIT {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("JIT feature is not enabled"), "forbidden"))
|
|
return
|
|
}
|
|
|
|
username := r.Header.Get("user")
|
|
if username == "" {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("user not found in request"), "unauthorized"))
|
|
return
|
|
}
|
|
network := r.URL.Query().Get("network")
|
|
|
|
user := &schema.User{Username: username}
|
|
err := user.GetWithMembership(r.Context())
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "unauthorized"))
|
|
return
|
|
}
|
|
|
|
var req models.JITAccessRequest
|
|
err = json.NewDecoder(r.Body).Decode(&req)
|
|
if err != nil {
|
|
logger.Log(0, "error decoding request body:", err.Error())
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
req.NetworkID = network
|
|
// Validate required fields
|
|
if req.NetworkID == "" {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("network_id is required"), "badrequest"))
|
|
return
|
|
}
|
|
|
|
if req.Reason == "" {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("reason is required"), "badrequest"))
|
|
return
|
|
}
|
|
// Check if user has access to the network by role
|
|
allNetworks, err := (&schema.Network{}).ListAll(r.Context())
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
|
|
return
|
|
}
|
|
|
|
// Filter networks by user role
|
|
userNetworks := logic.FilterNetworksByRole(r.Context(), allNetworks, user)
|
|
hasAccess := false
|
|
for _, network := range userNetworks {
|
|
if network.Name == req.NetworkID {
|
|
hasAccess = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !hasAccess {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(errors.New("user does not have access to this network"), "forbidden"))
|
|
return
|
|
}
|
|
|
|
// Create the JIT request
|
|
request, err := proLogic.CreateJITRequest(r.Context(), req.NetworkID, user.Username, req.Reason)
|
|
if err != nil {
|
|
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "badrequest"))
|
|
return
|
|
}
|
|
|
|
// Send email notifications to network admins
|
|
ctx := scope.WithContext(db.WithContext(context.Background()), scope.Level(r.Context()), scope.ID(r.Context()))
|
|
go func(ctx context.Context) {
|
|
network := &schema.Network{Name: req.NetworkID}
|
|
_ = network.Get(ctx)
|
|
if err := email.SendJITRequestEmails(ctx, request, network); err != nil {
|
|
slog.Error("failed to send JIT request notifications", "error", err)
|
|
}
|
|
}(ctx)
|
|
|
|
logic.LogEvent(r.Context(), &models.Event{
|
|
Action: schema.JitRequestCreate,
|
|
Source: models.Subject{
|
|
ID: user.Username,
|
|
Name: user.Username,
|
|
Type: schema.UserSub,
|
|
},
|
|
TriggeredBy: user.Username,
|
|
Target: models.Subject{
|
|
ID: request.ID,
|
|
Name: req.NetworkID,
|
|
Type: schema.NetworkSub,
|
|
},
|
|
NetworkID: schema.NetworkID(req.NetworkID),
|
|
Origin: schema.ClientApp,
|
|
})
|
|
|
|
logic.ReturnSuccessResponseWithJson(w, r, request, "JIT access request created")
|
|
}
|