merge conflict and onDenied handler

This commit is contained in:
pascal
2026-09-21 16:44:39 +02:00
parent b4f31f65d2
commit 3fe19eb86d
31 changed files with 252 additions and 257 deletions
@@ -21,12 +21,13 @@ import (
"github.com/netbirdio/netbird/shared/management/status"
)
// AuthErrorHandler is called when an auth error occurs during permission validation.
// If it returns true, the error is considered handled and the default error response is skipped.
type AuthErrorHandler func(w http.ResponseWriter, r *http.Request, userAuth *auth.UserAuth, err error) bool
// PermissionDeniedHandler is called when the user's role does not grant the requested operation.
// It is not called for validation failures such as blocked users or foreign accounts.
// If it returns true, the request is considered handled and the default 403 response is skipped.
type PermissionDeniedHandler func(w http.ResponseWriter, r *http.Request, userAuth *auth.UserAuth) bool
type Manager interface {
WithPermission(module modules.Module, operation operations.Operation, handlerFunc func(w http.ResponseWriter, r *http.Request, auth *auth.UserAuth), authErrHandler ...AuthErrorHandler) http.HandlerFunc
WithPermission(module modules.Module, operation operations.Operation, handlerFunc func(w http.ResponseWriter, r *http.Request, auth *auth.UserAuth), onDenied ...PermissionDeniedHandler) http.HandlerFunc
ValidateUserPermissions(ctx context.Context, accountID, userID string, module modules.Module, operation operations.Operation) (bool, context.Context, error)
ValidateRoleModuleAccess(ctx context.Context, accountID string, role roles.RolePermissions, module modules.Module, operation operations.Operation) bool
ValidateAccountAccess(ctx context.Context, accountID string, user *types.User, allowOwnerAndAdmin bool) (context.Context, error)
@@ -45,18 +46,30 @@ func NewManager(store store.Store) Manager {
}
}
// WithPermission wraps an HTTP handler with permission checking logic.
// An optional AuthErrorHandler can be provided to intercept auth errors before the default response is written.
// The wrapped handler receives a request whose context is enriched by the permission validation.
func (m *managerImpl) WithPermission(
module modules.Module,
operation operations.Operation,
handlerFunc func(w http.ResponseWriter, r *http.Request, auth *auth.UserAuth),
authErrHandler ...AuthErrorHandler,
onDenied ...PermissionDeniedHandler,
) http.HandlerFunc {
var onAuthErr AuthErrorHandler
if len(authErrHandler) > 0 {
onAuthErr = authErrHandler[0]
return WithPermission(m, module, operation, handlerFunc, onDenied...)
}
// WithPermission wraps an HTTP handler with permission checking performed by the given manager.
// Implementations embedding another Manager must route their own WithPermission through this
// function so that their ValidateUserPermissions override is the one consulted.
// An optional PermissionDeniedHandler can serve a reduced, self-scoped response when the role denies the operation.
// The wrapped handler receives a request whose context is enriched by the permission validation.
func WithPermission(
m Manager,
module modules.Module,
operation operations.Operation,
handlerFunc func(w http.ResponseWriter, r *http.Request, auth *auth.UserAuth),
onDenied ...PermissionDeniedHandler,
) http.HandlerFunc {
var deniedHandler PermissionDeniedHandler
if len(onDenied) > 0 {
deniedHandler = onDenied[0]
}
return func(w http.ResponseWriter, r *http.Request) {
@@ -68,23 +81,19 @@ func (m *managerImpl) WithPermission(
}
allowed, ctx, err := m.ValidateUserPermissions(r.Context(), userAuth.AccountId, userAuth.UserId, module, operation)
enriched := r.WithContext(ctx)
if err != nil {
if onAuthErr != nil && onAuthErr(w, enriched, &userAuth, err) {
return
}
log.WithContext(ctx).Errorf("failed to validate permissions for user %s on account %s: %v", userAuth.UserId, userAuth.AccountId, err)
util.WriteError(ctx, status.NewPermissionValidationError(err), w)
return
}
enriched := r.WithContext(ctx)
if !allowed {
permErr := status.NewPermissionDeniedError()
if onAuthErr != nil && onAuthErr(w, enriched, &userAuth, permErr) {
if deniedHandler != nil && deniedHandler(w, enriched, &userAuth) {
return
}
log.WithContext(ctx).Tracef("user %s on account %s is not allowed to %s in %s", userAuth.UserId, userAuth.AccountId, operation, module)
util.WriteError(ctx, permErr, w)
util.WriteError(ctx, status.NewPermissionDeniedError(), w)
return
}
@@ -120,10 +120,10 @@ func (mr *MockManagerMockRecorder) ValidateUserPermissions(ctx, accountID, userI
}
// WithPermission mocks base method.
func (m *MockManager) WithPermission(module modules.Module, operation operations.Operation, handlerFunc func(http.ResponseWriter, *http.Request, *auth.UserAuth), authErrHandler ...AuthErrorHandler) http.HandlerFunc {
func (m *MockManager) WithPermission(module modules.Module, operation operations.Operation, handlerFunc func(http.ResponseWriter, *http.Request, *auth.UserAuth), onDenied ...PermissionDeniedHandler) http.HandlerFunc {
m.ctrl.T.Helper()
varargs := []any{module, operation, handlerFunc}
for _, a := range authErrHandler {
for _, a := range onDenied {
varargs = append(varargs, a)
}
ret := m.ctrl.Call(m, "WithPermission", varargs...)
@@ -132,8 +132,8 @@ func (m *MockManager) WithPermission(module modules.Module, operation operations
}
// WithPermission indicates an expected call of WithPermission.
func (mr *MockManagerMockRecorder) WithPermission(module, operation, handlerFunc any, authErrHandler ...any) *gomock.Call {
func (mr *MockManagerMockRecorder) WithPermission(module, operation, handlerFunc any, onDenied ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{module, operation, handlerFunc}, authErrHandler...)
varargs := append([]any{module, operation, handlerFunc}, onDenied...)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WithPermission", reflect.TypeOf((*MockManager)(nil).WithPermission), varargs...)
}
@@ -16,7 +16,6 @@ type Manager interface {
CreateService(ctx context.Context, accountID, userID string, service *Service) (*Service, error)
UpdateService(ctx context.Context, accountID, userID string, service *Service) (*Service, error)
DeleteService(ctx context.Context, accountID, userID, serviceID string) error
DeleteAllServices(ctx context.Context, accountID, userID string) error
SetCertificateIssuedAt(ctx context.Context, accountID, serviceID string) error
SetStatus(ctx context.Context, accountID, serviceID string, status Status) error
ReloadAllServicesForAccount(ctx context.Context, accountID string) error
@@ -85,20 +85,6 @@ func (mr *MockManagerMockRecorder) DeleteAccountCluster(ctx, accountID, userID,
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAccountCluster", reflect.TypeOf((*MockManager)(nil).DeleteAccountCluster), ctx, accountID, userID, clusterAddress)
}
// DeleteAllServices mocks base method.
func (m *MockManager) DeleteAllServices(ctx context.Context, accountID, userID string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteAllServices", ctx, accountID, userID)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteAllServices indicates an expected call of DeleteAllServices.
func (mr *MockManagerMockRecorder) DeleteAllServices(ctx, accountID, userID any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAllServices", reflect.TypeOf((*MockManager)(nil).DeleteAllServices), ctx, accountID, userID)
}
// DeleteService mocks base method.
func (m *MockManager) DeleteService(ctx context.Context, accountID, userID, serviceID string) error {
m.ctrl.T.Helper()
@@ -20,15 +20,13 @@ import (
)
type handler struct {
manager rpservice.Manager
permissionsManager permissions.Manager
manager rpservice.Manager
}
// RegisterEndpoints registers all service HTTP endpoints.
func RegisterEndpoints(manager rpservice.Manager, domainManager domainmanager.Manager, accessLogsManager accesslogs.Manager, permissionsManager permissions.Manager, router *mux.Router) {
h := &handler{
manager: manager,
permissionsManager: permissionsManager,
manager: manager,
}
domainRouter := router.PathPrefix("/reverse-proxies").Subrouter()
@@ -842,43 +842,6 @@ func (m *Manager) DeleteService(ctx context.Context, accountID, userID, serviceI
return nil
}
func (m *Manager) DeleteAllServices(ctx context.Context, accountID, userID string) error {
var services []*service.Service
err := m.store.ExecuteInTransaction(ctx, func(transaction store.Store) error {
var err error
services, err = transaction.GetAccountServices(ctx, store.LockingStrengthUpdate, accountID)
if err != nil {
return err
}
for _, svc := range services {
if err = transaction.DeleteServiceTargets(ctx, accountID, svc.ID); err != nil {
return fmt.Errorf("failed to delete service targets: %w", err)
}
if err = transaction.DeleteService(ctx, accountID, svc.ID); err != nil {
return fmt.Errorf("failed to delete service: %w", err)
}
}
return nil
})
if err != nil {
return err
}
oidcCfg := m.proxyController.GetOIDCValidationConfig()
for _, svc := range services {
m.accountManager.StoreEvent(ctx, userID, svc.ID, accountID, activity.ServiceDeleted, svc.EventMeta())
m.proxyController.SendServiceUpdateToCluster(ctx, accountID, svc.ToProtoMapping(service.Delete, "", oidcCfg), svc.ProxyCluster)
}
m.accountManager.UpdateAccountPeers(ctx, accountID, types.UpdateReason{Resource: types.UpdateResourceService, Operation: types.UpdateOperationDelete})
return nil
}
// SetCertificateIssuedAt sets the certificate issued timestamp to the current time.
// Call this when receiving a gRPC notification that the certificate was issued.
func (m *Manager) SetCertificateIssuedAt(ctx context.Context, accountID, serviceID string) error {
@@ -1052,30 +1052,6 @@ func TestDeleteService_DeletesEphemeralExpose(t *testing.T) {
assert.NoError(t, err, "new expose should succeed after API delete")
}
func TestDeleteAllServices_DeletesEphemeralExposes(t *testing.T) {
ctx := context.Background()
mgr, _ := setupIntegrationTest(t)
for i := range 3 {
_, err := mgr.CreateServiceFromPeer(ctx, testAccountID, testPeerID, &rpservice.ExposeServiceRequest{
Port: uint16(8080 + i),
Mode: "http",
})
require.NoError(t, err)
}
count, err := mgr.store.CountEphemeralServicesByPeer(ctx, store.LockingStrengthNone, testAccountID, testPeerID)
require.NoError(t, err)
assert.Equal(t, int64(3), count, "all ephemeral services should exist")
err = mgr.DeleteAllServices(ctx, testAccountID, testUserID)
require.NoError(t, err)
count, err = mgr.store.CountEphemeralServicesByPeer(ctx, store.LockingStrengthNone, testAccountID, testPeerID)
require.NoError(t, err)
assert.Equal(t, int64(0), count, "all ephemeral services should be deleted after DeleteAllServices")
}
func TestRenewServiceFromPeer(t *testing.T) {
ctx := context.Background()