From 65069c1787428cb073196cd76a6e6514c4b19bda Mon Sep 17 00:00:00 2001 From: Givi Khojanashvili Date: Wed, 25 May 2022 20:26:50 +0400 Subject: [PATCH] feat(ac): add access control middleware (#321) --- management/server/account.go | 1 + .../server/http/middleware/access_control.go | 50 +++++++++++++++++++ management/server/http/server.go | 6 ++- management/server/mock_server/account_mock.go | 14 +++++- management/server/user.go | 18 +++++++ 5 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 management/server/http/middleware/access_control.go diff --git a/management/server/account.go b/management/server/account.go index 6ef9f0650..6126916c6 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -35,6 +35,7 @@ type AccountManager interface { GetAccountById(accountId string) (*Account, error) GetAccountByUserOrAccountId(userId, accountId, domain string) (*Account, error) GetAccountWithAuthorizationClaims(claims jwtclaims.AuthorizationClaims) (*Account, error) + IsUserAdmin(claims jwtclaims.AuthorizationClaims) (bool, error) AccountExists(accountId string) (*bool, error) AddAccount(accountId, userId, domain string) (*Account, error) GetPeer(peerKey string) (*Peer, error) diff --git a/management/server/http/middleware/access_control.go b/management/server/http/middleware/access_control.go new file mode 100644 index 000000000..51b36fc5e --- /dev/null +++ b/management/server/http/middleware/access_control.go @@ -0,0 +1,50 @@ +package middleware + +import ( + "fmt" + "net/http" + + "github.com/netbirdio/netbird/management/server/jwtclaims" +) + +type IsUserAdminFunc func(claims jwtclaims.AuthorizationClaims) (bool, error) + +// AccessControll middleware to restrict to make POST/PUT/DELETE requests by admin only +type AccessControll struct { + jwtExtractor jwtclaims.ClaimsExtractor + isUserAdmin IsUserAdminFunc + audience string +} + +// NewAccessControll instance constructor +func NewAccessControll(audience string, isUserAdmin IsUserAdminFunc) *AccessControll { + return &AccessControll{ + isUserAdmin: isUserAdmin, + audience: audience, + jwtExtractor: *jwtclaims.NewClaimsExtractor(nil), + } +} + +// Handler method of the middleware which forbinneds all modify requests for non admin users +func (a *AccessControll) Handler(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + jwtClaims := a.jwtExtractor.ExtractClaimsFromRequestContext(r, a.audience) + + ok, err := a.isUserAdmin(jwtClaims) + if err != nil { + http.Error(w, fmt.Sprintf("error get user from JWT: %v", err), http.StatusUnauthorized) + return + + } + + if !ok { + switch r.Method { + case http.MethodDelete, http.MethodPost, http.MethodPatch, http.MethodPut: + http.Error(w, "user is not admin", http.StatusForbidden) + return + } + } + + h.ServeHTTP(w, r) + }) +} diff --git a/management/server/http/server.go b/management/server/http/server.go index f8c9b6d33..61ebc4dfe 100644 --- a/management/server/http/server.go +++ b/management/server/http/server.go @@ -92,8 +92,12 @@ func (s *Server) Start() error { corsMiddleware := cors.AllowAll() + acMiddleware := middleware.NewAccessControll( + s.config.AuthAudience, + s.accountManager.IsUserAdmin) + r := mux.NewRouter() - r.Use(jwtMiddleware.Handler, corsMiddleware.Handler) + r.Use(jwtMiddleware.Handler, corsMiddleware.Handler, acMiddleware.Handler) groupsHandler := handler.NewGroups(s.accountManager, s.config.AuthAudience) rulesHandler := handler.NewRules(s.accountManager, s.config.AuthAudience) diff --git a/management/server/mock_server/account_mock.go b/management/server/mock_server/account_mock.go index ed95c727e..3a5aa5ec4 100644 --- a/management/server/mock_server/account_mock.go +++ b/management/server/mock_server/account_mock.go @@ -17,6 +17,7 @@ type MockAccountManager struct { GetAccountByIdFunc func(accountId string) (*server.Account, error) GetAccountByUserOrAccountIdFunc func(userId, accountId, domain string) (*server.Account, error) GetAccountWithAuthorizationClaimsFunc func(claims jwtclaims.AuthorizationClaims) (*server.Account, error) + IsUserAdminFunc func(claims jwtclaims.AuthorizationClaims) (bool, error) AccountExistsFunc func(accountId string) (*bool, error) AddAccountFunc func(accountId, userId, domain string) (*server.Account, error) GetPeerFunc func(peerKey string) (*server.Peer, error) @@ -193,7 +194,11 @@ func (am *MockAccountManager) GetNetworkMap(peerKey string) (*server.NetworkMap, return nil, status.Errorf(codes.Unimplemented, "method GetNetworkMap not implemented") } -func (am *MockAccountManager) AddPeer(setupKey string, userId string, peer *server.Peer) (*server.Peer, error) { +func (am *MockAccountManager) AddPeer( + setupKey string, + userId string, + peer *server.Peer, +) (*server.Peer, error) { if am.AddPeerFunc != nil { return am.AddPeerFunc(setupKey, userId, peer) } @@ -283,3 +288,10 @@ func (am *MockAccountManager) UpdatePeerMeta(peerKey string, meta server.PeerSys } return status.Errorf(codes.Unimplemented, "method UpdatePeerMetaFunc not implemented") } + +func (am *MockAccountManager) IsUserAdmin(claims jwtclaims.AuthorizationClaims) (bool, error) { + if am.IsUserAdminFunc != nil { + return am.IsUserAdminFunc(claims) + } + return false, status.Errorf(codes.Unimplemented, "method IsUserAdmin not implemented") +} diff --git a/management/server/user.go b/management/server/user.go index 9b9db6b8e..0691c6db7 100644 --- a/management/server/user.go +++ b/management/server/user.go @@ -1,10 +1,13 @@ package server import ( + "fmt" "strings" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + + "github.com/netbirdio/netbird/management/server/jwtclaims" ) const ( @@ -87,3 +90,18 @@ func (am *DefaultAccountManager) GetAccountByUser(userId string) (*Account, erro return am.Store.GetUserAccount(userId) } + +// IsUserAdmin flag for current user authenticated by JWT token +func (am *DefaultAccountManager) IsUserAdmin(claims jwtclaims.AuthorizationClaims) (bool, error) { + account, err := am.GetAccountWithAuthorizationClaims(claims) + if err != nil { + return false, fmt.Errorf("get account: %v", err) + } + + user, ok := account.Users[claims.UserId] + if !ok { + return false, fmt.Errorf("no such user") + } + + return user.Role == UserRoleAdmin, nil +}