mirror of
https://github.com/gravitl/netmaker.git
synced 2026-10-12 02:15:06 +00:00
+48
-28
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user