Merge pull request #4088 from gravitl/master

Master
This commit is contained in:
Abhishek Kondur
2026-07-15 00:47:12 +08:00
committed by GitHub
6 changed files with 104 additions and 106 deletions
+48 -28
View File
@@ -508,10 +508,12 @@ func createExtClient(w http.ResponseWriter, r *http.Request) {
return
}
var userName string
if r.Header.Get("ismaster") == "yes" {
var caller *schema.User
isMaster := r.Header.Get("ismaster") == "yes"
if isMaster {
userName = logic.MasterUser
} else {
caller := &schema.User{Username: r.Header.Get("user")}
caller = &schema.User{Username: r.Header.Get("user")}
err = caller.Get(r.Context())
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
@@ -525,17 +527,20 @@ func createExtClient(w http.ResponseWriter, r *http.Request) {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, "internal"))
return
}
// JIT enforcement: Check if user has access (only for desktop app users)
hasAccess, jitGrant, err := logic.CheckJITAccess(gateway.NetID, userName)
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.Internal))
return
}
if !hasAccess {
logic.ReturnErrorResponse(w, r, logic.FormatError(
errors.New("JIT access required: please request access from network admin"),
"forbidden"))
return
var jitGrant *schema.JITGrant
if extClientCreateRequiresJIT(isMaster, caller, gateway.NetID, customExtClient) {
hasAccess, grant, err := logic.CheckJITAccess(gateway.NetID, userName)
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.Internal))
return
}
if !hasAccess {
logic.ReturnErrorResponse(w, r, logic.FormatError(
errors.New("JIT access required: please request access from network admin"),
"forbidden"))
return
}
jitGrant = grant
}
// if device id is sent, we don't want to create another extclient for the same user
// and gw, with the same device id.
@@ -615,21 +620,22 @@ func createExtClient(w http.ResponseWriter, r *http.Request) {
extclient.PublicEndpoint = customExtClient.PublicEndpoint
extclient.Country = customExtClient.Country
extclient.Location = customExtClient.Location
// JIT enforcement: Check if user has access (only for desktop app users)
hasAccess, grant, err := logic.CheckJITAccess(extclient.Network, userName)
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.Internal))
return
}
if !hasAccess {
logic.ReturnErrorResponse(w, r, logic.FormatError(
errors.New("JIT access required: please request access from network admin"),
"forbidden"))
return
}
// Set JIT expiry time if grant exists (nil for admin users or when JIT not enabled)
if grant != nil {
extclient.JITExpiresAt = &grant.ExpiresAt
if extClientCreateRequiresJIT(isMaster, caller, extclient.Network, customExtClient) {
hasAccess, grant, err := logic.CheckJITAccess(extclient.Network, userName)
if err != nil {
logic.ReturnErrorResponse(w, r, logic.FormatError(err, logic.Internal))
return
}
if !hasAccess {
logic.ReturnErrorResponse(w, r, logic.FormatError(
errors.New("JIT access required: please request access from network admin"),
"forbidden"))
return
}
// Set JIT expiry time if grant exists (nil for admin users or when JIT not enabled)
if grant != nil {
extclient.JITExpiresAt = &grant.ExpiresAt
}
}
if extclient.DeviceID != "" {
@@ -1288,3 +1294,17 @@ func bulkUpdateExtClientStatus(w http.ResponseWriter, r *http.Request) {
slog.Info("bulk extclient status completed", "action", eventAction, "updated", updated, "total", len(req.IDs))
}()
}
// extClientCreateRequiresJIT reports whether create must verify JIT access.
// Only client-app requests (device_id / remote_access_client_id) are enforced.
// Dashboard config-file creates skip JIT. Master key skips. When JIT groups are set,
// users outside that allowlist are not subject.
func extClientCreateRequiresJIT(isMaster bool, caller *schema.User, networkID string, custom models.CustomExtClient) bool {
if custom.DeviceID == "" && custom.RemoteAccessClientID == "" {
return false
}
if isMaster {
return false
}
return logic.UserSubjectToNetworkJIT(networkID, caller)
}
+6
View File
@@ -7,3 +7,9 @@ import (
var CheckJITAccess = func(string, string) (bool, *schema.JITGrant, error) {
return true, nil, nil
}
// UserSubjectToNetworkJIT reports whether the user must satisfy JIT for client-app
// extclient creates on the network (JIT enabled + in unscoped / allowlisted groups).
var UserSubjectToNetworkJIT = func(string, *schema.User) bool {
return false
}
+6 -3
View File
@@ -481,12 +481,15 @@ func migrateEgressDomains() {
if len(eg.Domains) > 0 {
continue
}
var legacyDomain string
if err := gormDB.Table(eg.Table()).Where("id = ?", eg.ID).Pluck("domain", &legacyDomain).Error; err != nil {
var legacyDomainPtr *string
if err := gormDB.Table(eg.Table()).Where("id = ?", eg.ID).Select("domain").Scan(&legacyDomainPtr).Error; err != nil {
logger.Log(0, "migration: failed to read legacy egress domain:", eg.ID, err.Error())
continue
}
legacyDomain = strings.TrimSpace(legacyDomain)
if legacyDomainPtr == nil {
continue
}
legacyDomain := strings.TrimSpace(*legacyDomainPtr)
if legacyDomain == "" {
continue
}
+1
View File
@@ -199,6 +199,7 @@ func InitPro() {
logic.StopFlowCleanupLoop = proLogic.StopFlowCleanupLoop
// Expose JIT functions
logic.CheckJITAccess = proLogic.CheckJITAccess
logic.UserSubjectToNetworkJIT = proLogic.UserSubjectToNetworkJIT
logic.AssignVirtualRangeToEgress = proLogic.AssignVirtualRangeToEgress
mq.HandleExporterIntegrationPull = proLogic.HandleExporterIntegrationPull
}
+43 -66
View File
@@ -16,12 +16,13 @@ import (
"golang.org/x/exp/slog"
"github.com/gravitl/netmaker/logic"
"github.com/gravitl/netmaker/models"
)
// userMustSatisfyJIT reports whether an active JIT grant is required for this user on the network.
// Admins must be ruled out by the caller before invoking this. If jit_user_group_ids is empty,
// all non-admin users are subject to JIT. If non-empty, only users belonging to at least one
// listed group are subject; unknown users (nil) are not subject when the list is non-empty.
// If jit_user_group_ids is empty, every user is subject to JIT (including network admins).
// If non-empty, only users belonging to at least one listed group are subject; unknown users (nil)
// are not subject when the list is non-empty.
func userMustSatisfyJIT(network *schema.Network, user *schema.User) bool {
if len(network.JITUserGroupIDs) == 0 {
return true
@@ -45,8 +46,9 @@ type JITStatusResponse struct {
PendingRequest bool `json:"pending_request"`
}
// EnableJITOnNetwork - enables JIT on a network, optionally scoped to jitUserGroupIDs (empty = all non-admins).
// Disconnects ext clients for users who are subject to JIT under the new configuration.
// EnableJITOnNetwork - enables JIT on a network, optionally scoped to jitUserGroupIDs
// (empty = all users). Removes client-app ext clients for users who are subject to JIT
// under the new configuration.
func EnableJITOnNetwork(networkID string, jitUserGroupIDs []schema.UserGroupID) error {
// Check if JIT feature is enabled
featureFlags := GetFeatureFlags()
@@ -171,9 +173,6 @@ func CreateJITRequest(networkID, userName, reason string) (*schema.JITRequest, e
var subjectUser *schema.User
if userGetErr == nil {
subjectUser = reqUser
if IsNetworkAdmin(subjectUser, networkID) {
return nil, errors.New("JIT does not apply to your account on this network")
}
}
if !userMustSatisfyJIT(network, subjectUser) {
return nil, errors.New("JIT does not apply to your account on this network")
@@ -313,15 +312,6 @@ func CheckJITAccess(networkID, userID string) (bool, *schema.JITGrant, error) {
return true, nil, nil
}
// Network admins (per-network or all-networks) bypass JIT access checks.
user := &schema.User{Username: userID}
userGetErr := user.Get(db.WithContext(context.TODO()))
if userGetErr == nil && IsNetworkAdmin(user, networkID) {
return true, nil, nil
}
ctx := db.WithContext(context.Background())
// Check if network has JIT enabled
network := &schema.Network{Name: networkID}
err := network.Get(db.WithContext(context.TODO()))
@@ -334,6 +324,11 @@ func CheckJITAccess(networkID, userID string) (bool, *schema.JITGrant, error) {
return true, nil, nil
}
user := &schema.User{Username: userID}
userGetErr := user.Get(db.WithContext(context.TODO()))
ctx := db.WithContext(context.Background())
var subjectUser *schema.User
if userGetErr == nil {
subjectUser = user
@@ -364,6 +359,24 @@ func CheckJITAccess(networkID, userID string) (bool, *schema.JITGrant, error) {
return true, activeGrant, nil
}
// UserSubjectToNetworkJIT reports whether client-app extclient create must verify a JIT grant
// for this user on the network. False when the feature/network JIT is off or the user is
// outside jit_user_group_ids scope.
func UserSubjectToNetworkJIT(networkID string, user *schema.User) bool {
featureFlags := GetFeatureFlags()
if !featureFlags.EnableJIT {
return false
}
network := &schema.Network{Name: networkID}
if err := network.Get(db.WithContext(context.TODO())); err != nil {
return false
}
if !network.JITEnabled {
return false
}
return userMustSatisfyJIT(network, user)
}
// JITRequestWithGrant - JIT request with grant ID for approved requests
type JITRequestWithGrant struct {
schema.JITRequest
@@ -535,15 +548,7 @@ func GetUserJITNetworksStatus(networks []schema.Network, user *schema.User) ([]U
PendingRequest: false,
}
// Check if user is admin - if so, show JIT as disabled and has access
if IsNetworkAdmin(user, network.Name) {
status.JITEnabled = false
status.JitAppliesToUser = false
status.HasAccess = true
result = append(result, status)
continue
}
// When JIT is enabled it applies to all users unless scoped to jit_user_group_ids.
status.JitAppliesToUser = network.JITEnabled && userMustSatisfyJIT(&network, user)
// Only check JIT status if JIT is enabled on the network
@@ -632,40 +637,6 @@ func ExpireJITGrants() error {
return nil
}
// ReconcileUserGroupJITScope ensures the given user group is removed from the
// JIT allowlist of every network where the group now grants network-admin
// access. Admin group members bypass the JIT check (see CheckJITAccess), so
// leaving an admin group inside JITUserGroupIDs is dead config that confuses
// the UI and audit log. Intended to be called whenever a user group's
// network roles are updated (e.g. user -> admin transition).
func ReconcileUserGroupJITScope(group *schema.UserGroup) error {
if group == nil || group.ID == "" {
return nil
}
// A group with global network-admin (either via the all-networks scope
// holding the network-admin role, or via the global-network-admin role)
// implicitly makes every member an admin everywhere. Prune from every
// network's JIT scope rather than just the ones it has direct roles on.
if groupGrantsGlobalNetworkAdmin(group) {
return RemoveUserGroupFromAllJITScopes(group.ID)
}
for netID := range group.NetworkRoles.Data() {
if netID == schema.AllNetworks {
continue
}
if !groupGrantsNetworkAdminOn(group, netID) {
continue
}
if err := RemoveUserGroupFromNetworkJITScope(netID.String(), group.ID); err != nil {
slog.Warn("failed to clean up JIT scope for admin user group",
"group_id", group.ID, "network", netID, "error", err)
}
}
return nil
}
// groupGrantsNetworkAdminOn reports whether the group grants network-admin
// access on the specific network (network-scoped role only; global admin is
// handled separately by callers via groupGrantsGlobalNetworkAdmin).
@@ -783,8 +754,14 @@ func DisconnectExtClientsFromNetwork(networkID string) error {
return DisconnectExtClientsFromNetworkForScope(network)
}
// DisconnectExtClientsFromNetworkForScope deletes ext clients for users who require a JIT grant
// under the given network configuration (full JIT vs group-scoped).
// extClientFromClientApp reports whether the ext client was created by the desktop/RAC app.
func extClientFromClientApp(client models.ExtClient) bool {
return client.DeviceID != "" || client.RemoteAccessClientID != ""
}
// DisconnectExtClientsFromNetworkForScope removes client-app ext clients for users who require
// a JIT grant under the given network configuration. Admin-managed config files (no device/
// remote_access_client id) are kept.
func DisconnectExtClientsFromNetworkForScope(network *schema.Network) error {
extClients, err := logic.GetNetworkExtClients(network.Name)
if err != nil {
@@ -792,21 +769,21 @@ func DisconnectExtClientsFromNetworkForScope(network *schema.Network) error {
}
for _, client := range extClients {
if !extClientFromClientApp(client) {
continue
}
owner := &schema.User{Username: client.OwnerID}
ownerErr := owner.Get(db.WithContext(context.TODO()))
var ownerPtr *schema.User
if ownerErr == nil {
ownerPtr = owner
if IsNetworkAdmin(ownerPtr, network.Name) {
continue
}
}
if !userMustSatisfyJIT(network, ownerPtr) {
continue
}
if err := logic.DeleteExtClient(client.Network, client.ClientID, false); err != nil {
slog.Warn("failed to delete ext client when enabling JIT",
slog.Warn("failed to delete client-app ext client when enabling JIT",
"client_id", client.ClientID, "network", network.Name, "error", err)
continue
}
-9
View File
@@ -1164,15 +1164,6 @@ func EnsureDefaultUserGroupNetworkPolicies(old, new *schema.UserGroup) error {
}
}
// Members of an admin group bypass JIT, so any network where the group now
// grants admin access must drop it from its JIT scope.
if new != nil {
if err := ReconcileUserGroupJITScope(new); err != nil {
slog.Warn("failed to reconcile JIT scope for updated user group",
"group_id", new.ID, "error", err)
}
}
return nil
}