diff --git a/go.mod b/go.mod index 2c3aa0865a3..9f1a6b3340c 100644 --- a/go.mod +++ b/go.mod @@ -17,8 +17,8 @@ require ( github.com/spf13/cobra v1.3.0 github.com/spf13/pflag v1.0.5 github.com/vishvananda/netlink v1.1.0 - golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 - golang.org/x/sys v0.0.0-20220412211240-33da011f77ad + golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9 + golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a golang.zx2c4.com/wireguard v0.0.0-20211209221555-9c9e7e272434 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20211215182854-7a385b3431de golang.zx2c4.com/wireguard/windows v0.5.1 @@ -84,6 +84,7 @@ require ( github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/common v0.33.0 // indirect github.com/prometheus/procfs v0.7.3 // indirect + github.com/rogpeppe/go-internal v1.8.0 // indirect github.com/spf13/cast v1.5.0 // indirect github.com/srwiley/oksvg v0.0.0-20200311192757-870daf9aa564 // indirect github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9 // indirect @@ -91,15 +92,15 @@ require ( github.com/yuin/goldmark v1.4.1 // indirect golang.org/x/image v0.0.0-20200430140353-33d19683fad8 // indirect golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect - golang.org/x/net v0.0.0-20220412020605-290c469a71a5 // indirect + golang.org/x/net v0.0.0-20220513224357-95641704303c // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect golang.org/x/text v0.3.8-0.20211105212822-18b340fc7af2 // indirect golang.org/x/tools v0.1.10 // indirect - golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f // indirect golang.zx2c4.com/go118/netip v0.0.0-20211111135330-a4a02eeacf9d // indirect golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 // indirect google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa // indirect - gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index e8c19aaedbe..c38b83a2b83 100644 --- a/go.sum +++ b/go.sum @@ -461,7 +461,6 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= @@ -509,6 +508,7 @@ github.com/pion/turn/v2 v2.0.7 h1:SZhc00WDovK6czaN1RSiHqbwANtIO6wfZQsU0m0KNE8= github.com/pion/turn/v2 v2.0.7/go.mod h1:+y7xl719J8bAEVpSXBXvTxStjJv3hbz9YFflvkpcGPw= github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o= github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -548,7 +548,8 @@ github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0 github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= +github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rs/cors v1.8.0 h1:P2KMzcFwrPoSjkF1WLRPsp3UMLyql8L4v9hQpVeK5so= github.com/rs/cors v1.8.0/go.mod h1:EBwu+T5AvHOcXwvZIkQFjUN6s8Czyqw12GL/Y0tUyRM= github.com/rs/xid v1.3.0 h1:6NjYksEUlhurdVehpc7S7dk6DAmcKv8V9gG0FsVN2U4= @@ -646,8 +647,9 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211202192323-5770296d904e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 h1:71vQrMauZZhcTVK6KdYM+rklehEEwb3E+ZhaE5jrPrE= golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9 h1:NUzdAbFtCJSXU20AOXgeqaUwg8Ypg4MPYmL+d+rsB5c= +golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -754,8 +756,8 @@ golang.org/x/net v0.0.0-20211208012354-db4efeb81f4b/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220412020605-290c469a71a5 h1:bRb386wvrE+oBNdF1d/Xh9mQrfQ4ecYhW5qJ5GvTGT4= -golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220513224357-95641704303c h1:nF9mHSvoKBLkQNQhJZNsc66z2UzAMUbLGjC95CF3pU0= +golang.org/x/net v0.0.0-20220513224357-95641704303c/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -887,8 +889,8 @@ golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20211214234402-4825e8c3871d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad h1:ntjMns5wyP/fN65tdBD4g8J5w8n015+iIIs9rtjXkY0= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a h1:N2T1jUrTQE9Re6TFF5PhvEHXHCguynGhKjWVsIUt5cY= +golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -971,8 +973,9 @@ golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f h1:GGU+dLjvlC3qDwqYgL6UgRmHXhOOgns0bZu2Ty5mm6U= +golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.zx2c4.com/go118/netip v0.0.0-20211111135330-a4a02eeacf9d h1:9+v0G0naRhLPOJEeJOL6NuXTtAHHwmkyZlgQJ0XcQ8I= golang.zx2c4.com/go118/netip v0.0.0-20211111135330-a4a02eeacf9d/go.mod h1:5yyfuiqVIJ7t+3MqrpTQ+QqRkMWiESiyDvPNvKYCecg= golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 h1:Ug9qvr1myri/zFN6xL17LSCBGFDnphBBhzmILHsM5TY= @@ -1138,8 +1141,8 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U= -gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= diff --git a/management/server/account.go b/management/server/account.go index 4221e5f59bb..459cabcd3a9 100644 --- a/management/server/account.go +++ b/management/server/account.go @@ -7,7 +7,6 @@ import ( cacheStore "github.com/eko/gocache/v2/store" "github.com/netbirdio/netbird/management/server/idp" "github.com/netbirdio/netbird/management/server/jwtclaims" - "github.com/netbirdio/netbird/util" gocache "github.com/patrickmn/go-cache" "github.com/rs/xid" log "github.com/sirupsen/logrus" @@ -35,7 +34,7 @@ type AccountManager interface { accountId string, keyName string, keyType SetupKeyType, - expiresIn *util.Duration, + expiresIn time.Duration, ) (*SetupKey, error) RevokeSetupKey(accountId string, keyId string) (*SetupKey, error) RenameSetupKey(accountId string, keyId string, newName string) (*SetupKey, error) @@ -55,6 +54,7 @@ type AccountManager interface { GetUsersFromAccount(accountId string) ([]*UserInfo, error) GetGroup(accountId, groupID string) (*Group, error) SaveGroup(accountId string, group *Group) error + UpdateGroup(accountID string, groupID string, operations []GroupUpdateOperation) (*Group, error) DeleteGroup(accountId, groupID string) error ListGroups(accountId string) ([]*Group, error) GroupAddPeer(accountId, groupID, peerKey string) error @@ -62,6 +62,7 @@ type AccountManager interface { GroupListPeers(accountId, groupID string) ([]*Peer, error) GetRule(accountId, ruleID string) (*Rule, error) SaveRule(accountID string, rule *Rule) error + UpdateRule(accountID string, ruleID string, operations []RuleUpdateOperation) (*Rule, error) DeleteRule(accountId, ruleID string) error ListRules(accountId string) ([]*Rule, error) } @@ -222,14 +223,14 @@ func (am *DefaultAccountManager) AddSetupKey( accountId string, keyName string, keyType SetupKeyType, - expiresIn *util.Duration, + expiresIn time.Duration, ) (*SetupKey, error) { am.mux.Lock() defer am.mux.Unlock() keyDuration := DefaultSetupKeyDuration - if expiresIn != nil { - keyDuration = expiresIn.Duration + if expiresIn != 0 { + keyDuration = expiresIn } account, err := am.Store.GetAccount(accountId) @@ -633,7 +634,9 @@ func addAllGroup(account *Account) { defaultRule := &Rule{ ID: xid.New().String(), - Name: "Default", + Name: DefaultRuleName, + Description: DefaultRuleDescription, + Disabled: false, Source: []string{allGroup.ID}, Destination: []string{allGroup.ID}, } @@ -687,3 +690,19 @@ func getAccountSetupKeyByKey(acc *Account, key string) *SetupKey { } return nil } + +func removeFromList(inputList []string, toRemove []string) []string { + toRemoveMap := make(map[string]struct{}) + for _, item := range toRemove { + toRemoveMap[item] = struct{}{} + } + + var resultList []string + for _, item := range inputList { + _, ok := toRemoveMap[item] + if !ok { + resultList = append(resultList, item) + } + } + return resultList +} diff --git a/management/server/file_store.go b/management/server/file_store.go index a3abebb0cc9..bb108514b47 100644 --- a/management/server/file_store.go +++ b/management/server/file_store.go @@ -184,8 +184,8 @@ func (s *FileStore) DeletePeer(accountId string, peerKey string) (*Peer, error) delete(s.PeerKeyId2SrcRulesId, peerKey) // cleanup groups - var peers []string for _, g := range account.Groups { + var peers []string for _, p := range g.Peers { if p != peerKey { peers = append(peers, p) diff --git a/management/server/group.go b/management/server/group.go index de57ac2f39e..00ae3604ce8 100644 --- a/management/server/group.go +++ b/management/server/group.go @@ -17,6 +17,26 @@ type Group struct { Peers []string } +const ( + // UpdateGroupName indicates a name update operation + UpdateGroupName GroupUpdateOperationType = iota + // InsertPeersToGroup indicates insert peers to group operation + InsertPeersToGroup + // RemovePeersFromGroup indicates a remove peers from group operation + RemovePeersFromGroup + // UpdateGroupPeers indicates a replacement of group peers list + UpdateGroupPeers +) + +// GroupUpdateOperationType operation type +type GroupUpdateOperationType int + +// GroupUpdateOperation operation object with type and values to be applied +type GroupUpdateOperation struct { + Type GroupUpdateOperationType + Values []string +} + func (g *Group) Copy() *Group { return &Group{ ID: g.ID, @@ -63,6 +83,56 @@ func (am *DefaultAccountManager) SaveGroup(accountID string, group *Group) error return am.updateAccountPeers(account) } +// UpdateGroup updates a group using a list of operations +func (am *DefaultAccountManager) UpdateGroup(accountID string, + groupID string, operations []GroupUpdateOperation) (*Group, error) { + am.mux.Lock() + defer am.mux.Unlock() + + account, err := am.Store.GetAccount(accountID) + if err != nil { + return nil, status.Errorf(codes.NotFound, "account not found") + } + + groupToUpdate, ok := account.Groups[groupID] + if !ok { + return nil, status.Errorf(codes.NotFound, "group %s no longer exists", groupID) + } + + group := groupToUpdate.Copy() + + for _, operation := range operations { + switch operation.Type { + case UpdateGroupName: + group.Name = operation.Values[0] + case UpdateGroupPeers: + group.Peers = operation.Values + case InsertPeersToGroup: + sourceList := group.Peers + resultList := removeFromList(sourceList, operation.Values) + group.Peers = append(resultList, operation.Values...) + case RemovePeersFromGroup: + sourceList := group.Peers + resultList := removeFromList(sourceList, operation.Values) + group.Peers = resultList + } + } + + account.Groups[groupID] = group + + account.Network.IncSerial() + if err = am.Store.SaveAccount(account); err != nil { + return nil, err + } + + err = am.updateAccountPeers(account) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to update account peers") + } + + return group, nil +} + // DeleteGroup object of the peers func (am *DefaultAccountManager) DeleteGroup(accountID, groupID string) error { am.mux.Lock() diff --git a/management/server/http/api/cfg.yaml b/management/server/http/api/cfg.yaml new file mode 100644 index 00000000000..5c0afab66e1 --- /dev/null +++ b/management/server/http/api/cfg.yaml @@ -0,0 +1,5 @@ +package: api +generate: + models: true + embedded-spec: false +output: types.gen.go diff --git a/management/server/http/api/generate.sh b/management/server/http/api/generate.sh new file mode 100644 index 00000000000..d90db7a5f27 --- /dev/null +++ b/management/server/http/api/generate.sh @@ -0,0 +1,16 @@ +#!/bin/bash +set -e + +if ! which realpath > /dev/null 2>&1 +then + echo realpath is not installed + echo run: brew install coreutils + exit 1 +fi + +old_pwd=$(pwd) +script_path=$(dirname $(realpath "$0")) +cd "$script_path" +go install github.com/deepmap/oapi-codegen/cmd/oapi-codegen@v1.11.0 +oapi-codegen --config cfg.yaml openapi.yml +cd "$old_pwd" \ No newline at end of file diff --git a/management/server/http/api/openapi.yml b/management/server/http/api/openapi.yml new file mode 100644 index 00000000000..f8af2710546 --- /dev/null +++ b/management/server/http/api/openapi.yml @@ -0,0 +1,942 @@ +openapi: 3.0.1 +info: + title: NetBird REST API + description: API to manipulate groups, rules and retrieve information about peers and users + version: 0.0.1 +tags: + - name: Users + description: Interact with and view information about users. + - name: Peers + description: Interact with and view information about peers. + - name: Setup Keys + description: Interact with and view information about setup keys. + - name: Groups + description: Interact with and view information about groups. + - name: Rules + description: Interact with and view information about rules. +components: + schemas: + User: + type: object + properties: + id: + description: User ID + type: string + email: + description: User's email address + type: string + name: + description: User's name from idp provider + type: string + role: + description: User's Netbird account role + type: string + required: + - id + - email + - name + - role + PeerMinimum: + type: object + properties: + id: + description: Peer ID + type: string + name: + description: Peer's hostname + type: string + required: + - id + - name + Peer: + allOf: + - $ref: '#/components/schemas/PeerMinimum' + - type: object + properties: + ip: + description: Peer's IP address + type: string + connected: + description: Peer to Management connection status + type: boolean + last_seen: + description: Last time peer connected to Netbird's management service + type: string + format: date-time + os: + description: Peer's operating system and version + type: string + version: + description: Peer's daemon or cli version + type: string + groups: + description: Groups that the peer belongs to + type: array + items: + $ref: '#/components/schemas/GroupMinimum' + activated_by: + description: Provides information of who activated the Peer. User or Setup Key + type: object + properties: + type: + type: string + value: + type: string + required: + - type + - value + required: + - ip + - connected + - last_seen + - os + - version + - groups + - activated_by + SetupKey: + type: object + properties: + id: + description: Setup Key ID + type: string + key: + description: Setup Key value + type: string + name: + description: Setup key name identifier + type: string + expires: + description: Setup Key expiration date + type: string + format: date-time + type: + description: Setup key type, one-off for single time usage and reusable + type: string + valid: + description: Setup key validity status + type: boolean + revoked: + description: Setup key revocation status + type: boolean + used_times: + description: Usage count of setup key + type: integer + last_used: + description: Setup key last usage date + type: string + format: date-time + state: + description: Setup key status, "valid", "overused","expired" or "revoked" + type: string + required: + - id + - key + - name + - expires + - type + - valid + - revoked + - used_times + - last_used + - state + SetupKeyRequest: + type: object + properties: + name: + description: Setup Key name + type: string + type: + description: Setup key type, one-off for single time usage and reusable + type: string + expires_in: + description: Expiration time in seconds + type: integer + revoked: + description: Setup key revocation status + type: boolean + required: + - name + - type + - expires_in + - revoked + GroupMinimum: + type: object + properties: + id: + description: Group ID + type: string + name: + description: Group Name identifier + type: string + peers_count: + description: Count of peers associated to the group + type: integer + required: + - id + - name + - peers_count + Group: + allOf: + - $ref: '#/components/schemas/GroupMinimum' + - type: object + properties: + peers: + description: List of peers object + type: array + items: + $ref: '#/components/schemas/PeerMinimum' + required: + - peers + GroupPatchOperation: + type: object + properties: + op: + description: Patch operation type + type: string + enum: [ "replace","add","remove" ] + path: + description: Group field to update in form / + type: string + enum: [ "name","peers" ] + value: + description: Values to be applied + type: array + items: + type: string + required: + - op + - path + - value + RuleMinimum: + type: object + properties: + name: + description: Rule name identifier + type: string + description: + description: Rule friendly description + type: string + disabled: + description: Rules status + type: boolean + flow: + description: Rule flow, currently, only "bidirect" for bi-directional traffic is accepted + type: string + required: + - name + - description + - disabled + - flow + Rule: + allOf: + - type: object + properties: + id: + description: Rule ID + type: string + required: + - id + - $ref: '#/components/schemas/RuleMinimum' + - type: object + properties: + sources: + description: Rule source groups + type: array + items: + $ref: '#/components/schemas/GroupMinimum' + destinations: + description: Rule destination groups + type: array + items: + $ref: '#/components/schemas/GroupMinimum' + required: + - sources + - destinations + RulePatchOperation: + type: object + properties: + op: + description: Patch operation type + type: string + enum: [ "replace","add","remove" ] + path: + description: Rule field to update in form / + type: string + enum: [ "name","description","disabled","flow","sources","destinations" ] + value: + description: Values to be applied + type: array + items: + type: string + required: + - op + - path + - value + responses: + not_found: + description: Resource not found + content: {} + validation_failed_simple: + description: Validation failed + content: {} + bad_request: + description: Bad Request + content: {} + internal_error: + description: Internal Server Error + content: { } + validation_failed: + description: Validation failed + content: {} + forbidden: + description: Forbidden + content: {} + requires_authentication: + description: Requires authentication + content: {} + securitySchemes: + BearerAuth: + type: http + scheme: bearer + bearerFormat: JWT +security: + - BearerAuth: [ ] +paths: + /api/users: + get: + summary: Returns a list of all users + tags: [Users] + security: + - BearerAuth: [] + responses: + '200': + description: A JSON array of Users + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '500': + "$ref": "#/components/responses/internal_error" + /api/peers: + get: + summary: Returns a list of all peers + tags: [Peers] + security: + - BearerAuth: [] + responses: + '200': + description: A JSON Array of Peers + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Peer' + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '500': + "$ref": "#/components/responses/internal_error" + /api/peers/{id}: + get: + summary: Get information about a peer + tags: [Peers] + security: + - BearerAuth: [ ] + parameters: + - in: path + name: id + required: true + schema: + type: string + description: The Peer ID + responses: + '200': + description: A Peer object + content: + application/json: + schema: + $ref: '#/components/schemas/Peer' + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '500': + "$ref": "#/components/responses/internal_error" + put: + summary: Update information about a peer + tags: [Peers] + security: + - BearerAuth: [ ] + parameters: + - in: path + name: id + required: true + schema: + type: string + description: The Peer ID + requestBody: + description: update to peers + content: + 'application/json': + schema: + type: object + properties: + name: + type: string + required: + - name + responses: + '200': + description: A Peer object + content: + application/json: + schema: + $ref: '#/components/schemas/Peer' + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '500': + "$ref": "#/components/responses/internal_error" + delete: + summary: Delete a peer + tags: [Peers] + security: + - BearerAuth: [ ] + parameters: + - in: path + name: id + required: true + schema: + type: string + description: The Peer ID + responses: + '200': + description: Delete status code + content: {} + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '500': + "$ref": "#/components/responses/internal_error" + /api/setup-keys: + get: + summary: Returns a list of all Setup Keys + tags: [Setup Keys] + security: + - BearerAuth: [ ] + responses: + '200': + description: A JSON Array of Setup keys + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/SetupKey' + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '500': + "$ref": "#/components/responses/internal_error" + post: + summary: Creates a Setup Key + tags: [Setup Keys] + security: + - BearerAuth: [ ] + requestBody: + description: New Setup Key request + content: + 'application/json': + schema: + $ref: '#/components/schemas/SetupKeyRequest' + responses: + '200': + description: A Setup Keys Object + content: + application/json: + schema: + $ref: '#/components/schemas/SetupKey' + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '500': + "$ref": "#/components/responses/internal_error" + /api/setup-keys/{id}: + get: + summary: Get information about a Setup Key + tags: [Setup Keys] + security: + - BearerAuth: [ ] + parameters: + - in: path + name: id + required: true + schema: + type: string + description: The Setup Key ID + responses: + '200': + description: A Setup Key object + content: + application/json: + schema: + $ref: '#/components/schemas/SetupKey' + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '500': + "$ref": "#/components/responses/internal_error" + put: + summary: Update information about a Setup Key + tags: [Setup Keys] + security: + - BearerAuth: [ ] + parameters: + - in: path + name: id + required: true + schema: + type: string + description: The Setup Key ID + requestBody: + description: update to Setup Key + content: + 'application/json': + schema: + $ref: '#/components/schemas/SetupKeyRequest' + responses: + '200': + description: A Setup Key object + content: + application/json: + schema: + $ref: '#/components/schemas/SetupKey' + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '500': + "$ref": "#/components/responses/internal_error" + delete: + summary: Delete a Setup Key + tags: [Setup Keys] + security: + - BearerAuth: [ ] + parameters: + - in: path + name: id + required: true + schema: + type: string + description: The Setup Key ID + responses: + '200': + description: Delete status code + content: {} + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '500': + "$ref": "#/components/responses/internal_error" + /api/groups: + get: + summary: Returns a list of all Groups + tags: [Groups] + security: + - BearerAuth: [ ] + responses: + '200': + description: A JSON Array of Groups + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Group' + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '500': + "$ref": "#/components/responses/internal_error" + post: + summary: Creates a Group + tags: [Groups] + security: + - BearerAuth: [ ] + requestBody: + description: New Group request + content: + 'application/json': + schema: + type: object + properties: + name: + type: string + peers: + type: array + items: + type: string + required: + - name + responses: + '200': + description: A Group Object + content: + application/json: + schema: + $ref: '#/components/schemas/Group' + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '500': + "$ref": "#/components/responses/internal_error" + /api/groups/{id}: + get: + summary: Get information about a Group + tags: [Groups] + security: + - BearerAuth: [ ] + parameters: + - in: path + name: id + required: true + schema: + type: string + description: The Group ID + responses: + '200': + description: A Group object + content: + application/json: + schema: + $ref: '#/components/schemas/Group' + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '500': + "$ref": "#/components/responses/internal_error" + put: + summary: Update/Replace a Group + tags: [Groups] + security: + - BearerAuth: [ ] + parameters: + - in: path + name: id + required: true + schema: + type: string + description: The Group ID + requestBody: + description: Update Group request + content: + 'application/json': + schema: + type: object + properties: + Name: + type: string + Peers: + type: array + items: + type: string + responses: + '200': + description: A Group object + content: + application/json: + schema: + $ref: '#/components/schemas/Group' + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '500': + "$ref": "#/components/responses/internal_error" + patch: + summary: Update information about a Group + tags: [ Groups ] + security: + - BearerAuth: [ ] + parameters: + - in: path + name: id + required: true + schema: + type: string + description: The Group ID + requestBody: + description: Update Group request using a list of json patch objects + content: + 'application/json': + schema: + type: array + items: + $ref: '#/components/schemas/GroupPatchOperation' + responses: + '200': + description: A Group object + content: + application/json: + schema: + $ref: '#/components/schemas/Group' + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '500': + "$ref": "#/components/responses/internal_error" + delete: + summary: Delete a Group + tags: [Groups] + security: + - BearerAuth: [ ] + parameters: + - in: path + name: id + required: true + schema: + type: string + description: The Group ID + responses: + '200': + description: Delete status code + content: {} + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '500': + "$ref": "#/components/responses/internal_error" + /api/rules: + get: + summary: Returns a list of all Rules + tags: [Rules] + security: + - BearerAuth: [ ] + responses: + '200': + description: A JSON Array of Rules + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Rule' + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '500': + "$ref": "#/components/responses/internal_error" + post: + summary: Creates a Rule + tags: [Rules] + security: + - BearerAuth: [ ] + requestBody: + description: New Rule request + content: + 'application/json': + schema: + allOf: + - $ref: '#/components/schemas/RuleMinimum' + - type: object + properties: + sources: + type: array + items: + type: string + destinations: + type: array + items: + type: string + responses: + '200': + description: A Rule Object + content: + application/json: + schema: + $ref: '#/components/schemas/Rule' + /api/rules/{id}: + get: + summary: Get information about a Rules + tags: [Rules] + security: + - BearerAuth: [ ] + parameters: + - in: path + name: id + required: true + schema: + type: string + description: The Rule ID + responses: + '200': + description: A Rule object + content: + application/json: + schema: + $ref: '#/components/schemas/Rule' + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '500': + "$ref": "#/components/responses/internal_error" + put: + summary: Update/Replace a Rule + tags: [Rules] + security: + - BearerAuth: [ ] + parameters: + - in: path + name: id + required: true + schema: + type: string + description: The Rule ID + requestBody: + description: Update Rule request + content: + 'application/json': + schema: + allOf: + - $ref: '#/components/schemas/RuleMinimum' + - type: object + properties: + sources: + type: array + items: + type: string + destinations: + type: array + items: + type: string + responses: + '200': + description: A Rule object + content: + application/json: + schema: + $ref: '#/components/schemas/Rule' + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '500': + "$ref": "#/components/responses/internal_error" + patch: + summary: Update information about a Rule + tags: [ Rules ] + security: + - BearerAuth: [ ] + parameters: + - in: path + name: id + required: true + schema: + type: string + description: The Rule ID + requestBody: + description: Update Rule request using a list of json patch objects + content: + 'application/json': + schema: + type: array + items: + $ref: '#/components/schemas/RulePatchOperation' + responses: + '200': + description: A Rule object + content: + application/json: + schema: + $ref: '#/components/schemas/Rule' + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '500': + "$ref": "#/components/responses/internal_error" + delete: + summary: Delete a Rule + tags: [Rules] + security: + - BearerAuth: [ ] + parameters: + - in: path + name: id + required: true + schema: + type: string + description: The Rule ID + responses: + '200': + description: Delete status code + content: {} + '400': + "$ref": "#/components/responses/bad_request" + '401': + "$ref": "#/components/responses/requires_authentication" + '403': + "$ref": "#/components/responses/forbidden" + '500': + "$ref": "#/components/responses/internal_error" \ No newline at end of file diff --git a/management/server/http/api/types.gen.go b/management/server/http/api/types.gen.go new file mode 100644 index 00000000000..a58b3352279 --- /dev/null +++ b/management/server/http/api/types.gen.go @@ -0,0 +1,339 @@ +// Package api provides primitives to interact with the openapi HTTP API. +// +// Code generated by github.com/deepmap/oapi-codegen version v1.11.0 DO NOT EDIT. +package api + +import ( + "time" +) + +const ( + BearerAuthScopes = "BearerAuth.Scopes" +) + +// Defines values for GroupPatchOperationOp. +const ( + GroupPatchOperationOpAdd GroupPatchOperationOp = "add" + GroupPatchOperationOpRemove GroupPatchOperationOp = "remove" + GroupPatchOperationOpReplace GroupPatchOperationOp = "replace" +) + +// Defines values for GroupPatchOperationPath. +const ( + GroupPatchOperationPathName GroupPatchOperationPath = "name" + GroupPatchOperationPathPeers GroupPatchOperationPath = "peers" +) + +// Defines values for RulePatchOperationOp. +const ( + RulePatchOperationOpAdd RulePatchOperationOp = "add" + RulePatchOperationOpRemove RulePatchOperationOp = "remove" + RulePatchOperationOpReplace RulePatchOperationOp = "replace" +) + +// Defines values for RulePatchOperationPath. +const ( + RulePatchOperationPathDescription RulePatchOperationPath = "description" + RulePatchOperationPathDestinations RulePatchOperationPath = "destinations" + RulePatchOperationPathDisabled RulePatchOperationPath = "disabled" + RulePatchOperationPathFlow RulePatchOperationPath = "flow" + RulePatchOperationPathName RulePatchOperationPath = "name" + RulePatchOperationPathSources RulePatchOperationPath = "sources" +) + +// Group defines model for Group. +type Group struct { + // Group ID + Id string `json:"id"` + + // Group Name identifier + Name string `json:"name"` + + // List of peers object + Peers []PeerMinimum `json:"peers"` + + // Count of peers associated to the group + PeersCount int `json:"peers_count"` +} + +// GroupMinimum defines model for GroupMinimum. +type GroupMinimum struct { + // Group ID + Id string `json:"id"` + + // Group Name identifier + Name string `json:"name"` + + // Count of peers associated to the group + PeersCount int `json:"peers_count"` +} + +// GroupPatchOperation defines model for GroupPatchOperation. +type GroupPatchOperation struct { + // Patch operation type + Op GroupPatchOperationOp `json:"op"` + + // Group field to update in form / + Path GroupPatchOperationPath `json:"path"` + + // Values to be applied + Value []string `json:"value"` +} + +// Patch operation type +type GroupPatchOperationOp string + +// Group field to update in form / +type GroupPatchOperationPath string + +// Peer defines model for Peer. +type Peer struct { + // Provides information of who activated the Peer. User or Setup Key + ActivatedBy struct { + Type string `json:"type"` + Value string `json:"value"` + } `json:"activated_by"` + + // Peer to Management connection status + Connected bool `json:"connected"` + + // Groups that the peer belongs to + Groups []GroupMinimum `json:"groups"` + + // Peer ID + Id string `json:"id"` + + // Peer's IP address + Ip string `json:"ip"` + + // Last time peer connected to Netbird's management service + LastSeen time.Time `json:"last_seen"` + + // Peer's hostname + Name string `json:"name"` + + // Peer's operating system and version + Os string `json:"os"` + + // Peer's daemon or cli version + Version string `json:"version"` +} + +// PeerMinimum defines model for PeerMinimum. +type PeerMinimum struct { + // Peer ID + Id string `json:"id"` + + // Peer's hostname + Name string `json:"name"` +} + +// Rule defines model for Rule. +type Rule struct { + // Rule friendly description + Description string `json:"description"` + + // Rule destination groups + Destinations []GroupMinimum `json:"destinations"` + + // Rules status + Disabled bool `json:"disabled"` + + // Rule flow, currently, only "bidirect" for bi-directional traffic is accepted + Flow string `json:"flow"` + + // Rule ID + Id string `json:"id"` + + // Rule name identifier + Name string `json:"name"` + + // Rule source groups + Sources []GroupMinimum `json:"sources"` +} + +// RuleMinimum defines model for RuleMinimum. +type RuleMinimum struct { + // Rule friendly description + Description string `json:"description"` + + // Rules status + Disabled bool `json:"disabled"` + + // Rule flow, currently, only "bidirect" for bi-directional traffic is accepted + Flow string `json:"flow"` + + // Rule name identifier + Name string `json:"name"` +} + +// RulePatchOperation defines model for RulePatchOperation. +type RulePatchOperation struct { + // Patch operation type + Op RulePatchOperationOp `json:"op"` + + // Rule field to update in form / + Path RulePatchOperationPath `json:"path"` + + // Values to be applied + Value []string `json:"value"` +} + +// Patch operation type +type RulePatchOperationOp string + +// Rule field to update in form / +type RulePatchOperationPath string + +// SetupKey defines model for SetupKey. +type SetupKey struct { + // Setup Key expiration date + Expires time.Time `json:"expires"` + + // Setup Key ID + Id string `json:"id"` + + // Setup Key value + Key string `json:"key"` + + // Setup key last usage date + LastUsed time.Time `json:"last_used"` + + // Setup key name identifier + Name string `json:"name"` + + // Setup key revocation status + Revoked bool `json:"revoked"` + + // Setup key status, "valid", "overused","expired" or "revoked" + State string `json:"state"` + + // Setup key type, one-off for single time usage and reusable + Type string `json:"type"` + + // Usage count of setup key + UsedTimes int `json:"used_times"` + + // Setup key validity status + Valid bool `json:"valid"` +} + +// SetupKeyRequest defines model for SetupKeyRequest. +type SetupKeyRequest struct { + // Expiration time in seconds + ExpiresIn int `json:"expires_in"` + + // Setup Key name + Name string `json:"name"` + + // Setup key revocation status + Revoked bool `json:"revoked"` + + // Setup key type, one-off for single time usage and reusable + Type string `json:"type"` +} + +// User defines model for User. +type User struct { + // User's email address + Email string `json:"email"` + + // User ID + Id string `json:"id"` + + // User's name from idp provider + Name string `json:"name"` + + // User's Netbird account role + Role string `json:"role"` +} + +// PostApiGroupsJSONBody defines parameters for PostApiGroups. +type PostApiGroupsJSONBody struct { + Name string `json:"name"` + Peers *[]string `json:"peers,omitempty"` +} + +// PatchApiGroupsIdJSONBody defines parameters for PatchApiGroupsId. +type PatchApiGroupsIdJSONBody = []GroupPatchOperation + +// PutApiGroupsIdJSONBody defines parameters for PutApiGroupsId. +type PutApiGroupsIdJSONBody struct { + Name *string `json:"Name,omitempty"` + Peers *[]string `json:"Peers,omitempty"` +} + +// PutApiPeersIdJSONBody defines parameters for PutApiPeersId. +type PutApiPeersIdJSONBody struct { + Name string `json:"name"` +} + +// PostApiRulesJSONBody defines parameters for PostApiRules. +type PostApiRulesJSONBody struct { + // Rule friendly description + Description string `json:"description"` + Destinations *[]string `json:"destinations,omitempty"` + + // Rules status + Disabled bool `json:"disabled"` + + // Rule flow, currently, only "bidirect" for bi-directional traffic is accepted + Flow string `json:"flow"` + + // Rule name identifier + Name string `json:"name"` + Sources *[]string `json:"sources,omitempty"` +} + +// PatchApiRulesIdJSONBody defines parameters for PatchApiRulesId. +type PatchApiRulesIdJSONBody = []RulePatchOperation + +// PutApiRulesIdJSONBody defines parameters for PutApiRulesId. +type PutApiRulesIdJSONBody struct { + // Rule friendly description + Description string `json:"description"` + Destinations *[]string `json:"destinations,omitempty"` + + // Rules status + Disabled bool `json:"disabled"` + + // Rule flow, currently, only "bidirect" for bi-directional traffic is accepted + Flow string `json:"flow"` + + // Rule name identifier + Name string `json:"name"` + Sources *[]string `json:"sources,omitempty"` +} + +// PostApiSetupKeysJSONBody defines parameters for PostApiSetupKeys. +type PostApiSetupKeysJSONBody = SetupKeyRequest + +// PutApiSetupKeysIdJSONBody defines parameters for PutApiSetupKeysId. +type PutApiSetupKeysIdJSONBody = SetupKeyRequest + +// PostApiGroupsJSONRequestBody defines body for PostApiGroups for application/json ContentType. +type PostApiGroupsJSONRequestBody PostApiGroupsJSONBody + +// PatchApiGroupsIdJSONRequestBody defines body for PatchApiGroupsId for application/json ContentType. +type PatchApiGroupsIdJSONRequestBody = PatchApiGroupsIdJSONBody + +// PutApiGroupsIdJSONRequestBody defines body for PutApiGroupsId for application/json ContentType. +type PutApiGroupsIdJSONRequestBody PutApiGroupsIdJSONBody + +// PutApiPeersIdJSONRequestBody defines body for PutApiPeersId for application/json ContentType. +type PutApiPeersIdJSONRequestBody PutApiPeersIdJSONBody + +// PostApiRulesJSONRequestBody defines body for PostApiRules for application/json ContentType. +type PostApiRulesJSONRequestBody PostApiRulesJSONBody + +// PatchApiRulesIdJSONRequestBody defines body for PatchApiRulesId for application/json ContentType. +type PatchApiRulesIdJSONRequestBody = PatchApiRulesIdJSONBody + +// PutApiRulesIdJSONRequestBody defines body for PutApiRulesId for application/json ContentType. +type PutApiRulesIdJSONRequestBody PutApiRulesIdJSONBody + +// PostApiSetupKeysJSONRequestBody defines body for PostApiSetupKeys for application/json ContentType. +type PostApiSetupKeysJSONRequestBody = PostApiSetupKeysJSONBody + +// PutApiSetupKeysIdJSONRequestBody defines body for PutApiSetupKeysId for application/json ContentType. +type PutApiSetupKeysIdJSONRequestBody = PutApiSetupKeysIdJSONBody diff --git a/management/server/http/handler/groups.go b/management/server/http/handler/groups.go index cf179a52290..66fa8e3e276 100644 --- a/management/server/http/handler/groups.go +++ b/management/server/http/handler/groups.go @@ -3,6 +3,9 @@ package handler import ( "encoding/json" "fmt" + "github.com/netbirdio/netbird/management/server/http/api" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" "net/http" "github.com/netbirdio/netbird/management/server" @@ -13,26 +16,6 @@ import ( log "github.com/sirupsen/logrus" ) -// GroupResponse is a response sent to the client -type GroupResponse struct { - ID string - Name string - Peers []GroupPeerResponse `json:",omitempty"` -} - -// GroupPeerResponse is a response sent to the client -type GroupPeerResponse struct { - Key string - Name string -} - -// GroupRequest to create or update group -type GroupRequest struct { - ID string - Name string - Peers []string -} - // Groups is a handler that returns groups of the account type Groups struct { jwtExtractor jwtclaims.ClaimsExtractor @@ -50,14 +33,14 @@ func NewGroups(accountManager server.AccountManager, authAudience string) *Group // GetAllGroupsHandler list for the account func (h *Groups) GetAllGroupsHandler(w http.ResponseWriter, r *http.Request) { - account, err := h.getGroupAccount(r) + account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r) if err != nil { log.Error(err) http.Redirect(w, r, "/", http.StatusInternalServerError) return } - var groups []*GroupResponse + var groups []*api.Group for _, g := range account.Groups { groups = append(groups, toGroupResponse(account, g)) } @@ -65,31 +48,209 @@ func (h *Groups) GetAllGroupsHandler(w http.ResponseWriter, r *http.Request) { writeJSONObject(w, groups) } -func (h *Groups) CreateOrUpdateGroupHandler(w http.ResponseWriter, r *http.Request) { - account, err := h.getGroupAccount(r) +// UpdateGroupHandler handles update to a group identified by a given ID +func (h *Groups) UpdateGroupHandler(w http.ResponseWriter, r *http.Request) { + account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r) + if err != nil { + http.Redirect(w, r, "/", http.StatusInternalServerError) + return + } + + vars := mux.Vars(r) + groupID, ok := vars["id"] + if !ok { + http.Error(w, "group ID field is missing", http.StatusBadRequest) + return + } + if len(groupID) == 0 { + http.Error(w, "group ID can't be empty", http.StatusUnprocessableEntity) + return + } + + _, ok = account.Groups[groupID] + if !ok { + http.Error(w, fmt.Sprintf("couldn't find group with ID %s", groupID), http.StatusNotFound) + return + } + + allGroup, err := account.GetGroupAll() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + if allGroup.ID == groupID { + http.Error(w, "updating group ALL is not allowed", http.StatusMethodNotAllowed) + return + } + + var req api.PutApiGroupsIdJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + if *req.Name == "" { + http.Error(w, "group name shouldn't be empty", http.StatusUnprocessableEntity) + return + } + + group := server.Group{ + ID: groupID, + Name: *req.Name, + Peers: peerIPsToKeys(account, req.Peers), + } + + if err := h.accountManager.SaveGroup(account.Id, &group); err != nil { + log.Errorf("failed updating group %s under account %s %v", groupID, account.Id, err) + http.Redirect(w, r, "/", http.StatusInternalServerError) + return + } + + writeJSONObject(w, toGroupResponse(account, &group)) +} + +// PatchGroupHandler handles patch updates to a group identified by a given ID +func (h *Groups) PatchGroupHandler(w http.ResponseWriter, r *http.Request) { + account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r) + if err != nil { + http.Redirect(w, r, "/", http.StatusInternalServerError) + return + } + + vars := mux.Vars(r) + groupID := vars["id"] + if len(groupID) == 0 { + http.Error(w, "invalid group Id", http.StatusBadRequest) + return + } + + _, ok := account.Groups[groupID] + if !ok { + http.Error(w, fmt.Sprintf("couldn't find group id %s", groupID), http.StatusNotFound) + return + } + + allGroup, err := account.GetGroupAll() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if allGroup.ID == groupID { + http.Error(w, "updating group ALL is not allowed", http.StatusMethodNotAllowed) + return + } + + var req api.PatchApiGroupsIdJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + if len(req) == 0 { + http.Error(w, "no patch instruction received", http.StatusBadRequest) + return + } + + var operations []server.GroupUpdateOperation + + for _, patch := range req { + switch patch.Path { + case api.GroupPatchOperationPathName: + if patch.Op != api.GroupPatchOperationOpReplace { + http.Error(w, fmt.Sprintf("Name field only accepts replace operation, got %s", patch.Op), + http.StatusBadRequest) + return + } + + if len(patch.Value) == 0 || patch.Value[0] == "" { + http.Error(w, "Group name shouldn't be empty", http.StatusUnprocessableEntity) + return + } + + operations = append(operations, server.GroupUpdateOperation{ + Type: server.UpdateGroupName, + Values: patch.Value, + }) + case api.GroupPatchOperationPathPeers: + switch patch.Op { + case api.GroupPatchOperationOpReplace: + peerKeys := peerIPsToKeys(account, &patch.Value) + operations = append(operations, server.GroupUpdateOperation{ + Type: server.UpdateGroupPeers, + Values: peerKeys, + }) + case api.GroupPatchOperationOpRemove: + peerKeys := peerIPsToKeys(account, &patch.Value) + operations = append(operations, server.GroupUpdateOperation{ + Type: server.RemovePeersFromGroup, + Values: peerKeys, + }) + case api.GroupPatchOperationOpAdd: + peerKeys := peerIPsToKeys(account, &patch.Value) + operations = append(operations, server.GroupUpdateOperation{ + Type: server.InsertPeersToGroup, + Values: peerKeys, + }) + default: + http.Error(w, "invalid operation, \"%s\", for Peers field", http.StatusBadRequest) + return + } + default: + http.Error(w, "invalid patch path", http.StatusBadRequest) + return + } + } + + group, err := h.accountManager.UpdateGroup(account.Id, groupID, operations) + + if err != nil { + errStatus, ok := status.FromError(err) + if ok && errStatus.Code() == codes.Internal { + http.Error(w, errStatus.String(), http.StatusInternalServerError) + return + } + + if ok && errStatus.Code() == codes.NotFound { + http.Error(w, errStatus.String(), http.StatusNotFound) + return + } + + log.Errorf("failed updating group %s under account %s %v", groupID, account.Id, err) + http.Redirect(w, r, "/", http.StatusInternalServerError) + return + } + + writeJSONObject(w, toGroupResponse(account, group)) +} + +// CreateGroupHandler handles group creation request +func (h *Groups) CreateGroupHandler(w http.ResponseWriter, r *http.Request) { + account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r) if err != nil { http.Redirect(w, r, "/", http.StatusInternalServerError) return } - var req GroupRequest + var req api.PostApiGroupsJSONRequestBody if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } - if r.Method == http.MethodPost { - req.ID = xid.New().String() + if req.Name == "" { + http.Error(w, "Group name shouldn't be empty", http.StatusUnprocessableEntity) + return } group := server.Group{ - ID: req.ID, + ID: xid.New().String(), Name: req.Name, - Peers: req.Peers, + Peers: peerIPsToKeys(account, req.Peers), } if err := h.accountManager.SaveGroup(account.Id, &group); err != nil { - log.Errorf("failed updating group %s under account %s %v", req.ID, account.Id, err) + log.Errorf("failed creating group \"%s\" under account %s %v", req.Name, account.Id, err) http.Redirect(w, r, "/", http.StatusInternalServerError) return } @@ -97,22 +258,34 @@ func (h *Groups) CreateOrUpdateGroupHandler(w http.ResponseWriter, r *http.Reque writeJSONObject(w, toGroupResponse(account, &group)) } +// DeleteGroupHandler handles group deletion request func (h *Groups) DeleteGroupHandler(w http.ResponseWriter, r *http.Request) { - account, err := h.getGroupAccount(r) + account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r) if err != nil { http.Redirect(w, r, "/", http.StatusInternalServerError) return } aID := account.Id - gID := mux.Vars(r)["id"] - if len(gID) == 0 { + groupID := mux.Vars(r)["id"] + if len(groupID) == 0 { http.Error(w, "invalid group ID", http.StatusBadRequest) return } - if err := h.accountManager.DeleteGroup(aID, gID); err != nil { - log.Errorf("failed delete group %s under account %s %v", gID, aID, err) + allGroup, err := account.GetGroupAll() + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + if allGroup.ID == groupID { + http.Error(w, "deleting group ALL is not allowed", http.StatusMethodNotAllowed) + return + } + + if err := h.accountManager.DeleteGroup(aID, groupID); err != nil { + log.Errorf("failed delete group %s under account %s %v", groupID, aID, err) http.Redirect(w, r, "/", http.StatusInternalServerError) return } @@ -120,8 +293,9 @@ func (h *Groups) DeleteGroupHandler(w http.ResponseWriter, r *http.Request) { writeJSONObject(w, "") } +// GetGroupHandler returns a group func (h *Groups) GetGroupHandler(w http.ResponseWriter, r *http.Request) { - account, err := h.getGroupAccount(r) + account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r) if err != nil { http.Redirect(w, r, "/", http.StatusInternalServerError) return @@ -147,39 +321,51 @@ func (h *Groups) GetGroupHandler(w http.ResponseWriter, r *http.Request) { } } -func (h *Groups) getGroupAccount(r *http.Request) (*server.Account, error) { - jwtClaims := h.jwtExtractor.ExtractClaimsFromRequestContext(r, h.authAudience) - - account, err := h.accountManager.GetAccountWithAuthorizationClaims(jwtClaims) - if err != nil { - return nil, fmt.Errorf("failed getting account of a user %s: %v", jwtClaims.UserId, err) +func peerIPsToKeys(account *server.Account, peerIPs *[]string) []string { + var mappedPeerKeys []string + if peerIPs == nil { + return mappedPeerKeys } - return account, nil + peersChecked := make(map[string]struct{}) + + for _, requestPeersIP := range *peerIPs { + _, ok := peersChecked[requestPeersIP] + if ok { + continue + } + peersChecked[requestPeersIP] = struct{}{} + for _, accountPeer := range account.Peers { + if accountPeer.IP.String() == requestPeersIP { + mappedPeerKeys = append(mappedPeerKeys, accountPeer.Key) + } + } + } + return mappedPeerKeys } -func toGroupResponse(account *server.Account, group *server.Group) *GroupResponse { - cache := make(map[string]GroupPeerResponse) - gr := GroupResponse{ - ID: group.ID, - Name: group.Name, +func toGroupResponse(account *server.Account, group *server.Group) *api.Group { + cache := make(map[string]api.PeerMinimum) + gr := api.Group{ + Id: group.ID, + Name: group.Name, + PeersCount: len(group.Peers), } for _, pid := range group.Peers { - peerResp, ok := cache[pid] + _, ok := cache[pid] if !ok { peer, ok := account.Peers[pid] if !ok { continue } - peerResp = GroupPeerResponse{ - Key: peer.Key, + peerResp := api.PeerMinimum{ + Id: peer.IP.String(), Name: peer.Name, } cache[pid] = peerResp + gr.Peers = append(gr.Peers, peerResp) } - gr.Peers = append(gr.Peers, peerResp) } - return &gr } diff --git a/management/server/http/handler/groups_test.go b/management/server/http/handler/groups_test.go index ed8d46568ab..ba68d4d2b28 100644 --- a/management/server/http/handler/groups_test.go +++ b/management/server/http/handler/groups_test.go @@ -4,7 +4,9 @@ import ( "bytes" "encoding/json" "fmt" + "github.com/netbirdio/netbird/management/server/http/api" "io" + "net" "net/http" "net/http/httptest" "strings" @@ -18,6 +20,11 @@ import ( "github.com/netbirdio/netbird/management/server/mock_server" ) +var TestPeers = map[string]*server.Peer{ + "A": &server.Peer{Key: "A", IP: net.ParseIP("100.100.100.100")}, + "B": &server.Peer{Key: "B", IP: net.ParseIP("200.200.200.200")}, +} + func initGroupTestData(groups ...*server.Group) *Groups { return &Groups{ accountManager: &mock_server.MockAccountManager{ @@ -36,10 +43,38 @@ func initGroupTestData(groups ...*server.Group) *Groups { Name: "Group", }, nil }, + UpdateGroupFunc: func(_ string, groupID string, operations []server.GroupUpdateOperation) (*server.Group, error) { + var group server.Group + group.ID = groupID + for _, operation := range operations { + switch operation.Type { + case server.UpdateGroupName: + group.Name = operation.Values[0] + case server.UpdateGroupPeers, server.InsertPeersToGroup: + group.Peers = operation.Values + case server.RemovePeersFromGroup: + default: + return nil, fmt.Errorf("no operation") + } + } + return &group, nil + }, + GetPeerByIPFunc: func(_ string, peerIP string) (*server.Peer, error) { + for _, peer := range TestPeers { + if peer.IP.String() == peerIP { + return peer, nil + } + } + return nil, fmt.Errorf("peer not found") + }, GetAccountWithAuthorizationClaimsFunc: func(claims jwtclaims.AuthorizationClaims) (*server.Account, error) { return &server.Account{ Id: claims.AccountId, Domain: "hotmail.com", + Peers: TestPeers, + Groups: map[string]*server.Group{ + "id-existed": &server.Group{ID: "id-existed", Peers: []string{"A", "B"}}, + "id-all": &server.Group{ID: "id-all", Name: "All"}}, }, nil }, }, @@ -125,41 +160,114 @@ func TestGetGroup(t *testing.T) { } } -func TestSaveGroup(t *testing.T) { +func TestWriteGroup(t *testing.T) { tt := []struct { name string expectedStatus int expectedBody bool - expectedGroup *server.Group + expectedGroup *api.Group requestType string requestPath string requestBody io.Reader }{ { - name: "SaveGroup POST OK", + name: "Write Group POST OK", requestType: http.MethodPost, requestPath: "/api/groups", requestBody: bytes.NewBuffer( []byte(`{"Name":"Default POSTed Group"}`)), expectedStatus: http.StatusOK, expectedBody: true, - expectedGroup: &server.Group{ - ID: "id-was-set", + expectedGroup: &api.Group{ + Id: "id-was-set", Name: "Default POSTed Group", }, }, { - name: "SaveGroup PUT OK", - requestType: http.MethodPut, + name: "Write Group POST Invalid Name", + requestType: http.MethodPost, requestPath: "/api/groups", requestBody: bytes.NewBuffer( - []byte(`{"ID":"id-existed","Name":"Default POSTed Group"}`)), + []byte(`{"name":""}`)), + expectedStatus: http.StatusUnprocessableEntity, + expectedBody: false, + }, + { + name: "Write Group PUT OK", + requestType: http.MethodPut, + requestPath: "/api/groups/id-existed", + requestBody: bytes.NewBuffer( + []byte(`{"Name":"Default POSTed Group"}`)), expectedStatus: http.StatusOK, - expectedGroup: &server.Group{ - ID: "id-existed", + expectedGroup: &api.Group{ + Id: "id-existed", Name: "Default POSTed Group", }, }, + { + name: "Write Group PUT Invalid Name", + requestType: http.MethodPut, + requestPath: "/api/groups/id-existed", + requestBody: bytes.NewBuffer( + []byte(`{"Name":""}`)), + expectedStatus: http.StatusUnprocessableEntity, + expectedBody: false, + }, + { + name: "Write Group PUT All Group Name", + requestType: http.MethodPut, + requestPath: "/api/groups/id-all", + requestBody: bytes.NewBuffer( + []byte(`{"Name":"super"}`)), + expectedStatus: http.StatusMethodNotAllowed, + expectedBody: false, + }, + { + name: "Write Group PATCH Name OK", + requestType: http.MethodPatch, + requestPath: "/api/groups/id-existed", + requestBody: bytes.NewBuffer( + []byte(`[{"op":"replace","path":"name","value":["Default POSTed Group"]}]`)), + expectedStatus: http.StatusOK, + expectedGroup: &api.Group{ + Id: "id-existed", + Name: "Default POSTed Group", + }, + }, + { + name: "Write Group PATCH Invalid Name OP", + requestType: http.MethodPatch, + requestPath: "/api/groups/id-existed", + requestBody: bytes.NewBuffer( + []byte(`[{"op":"insert","path":"name","value":[""]}]`)), + expectedStatus: http.StatusBadRequest, + expectedBody: false, + }, + { + name: "Write Group PATCH Invalid Name", + requestType: http.MethodPatch, + requestPath: "/api/groups/id-existed", + requestBody: bytes.NewBuffer( + []byte(`[{"op":"replace","path":"name","value":[]}]`)), + expectedStatus: http.StatusUnprocessableEntity, + expectedBody: false, + }, + { + name: "Write Group PATCH Peers OK", + requestType: http.MethodPatch, + requestPath: "/api/groups/id-existed", + requestBody: bytes.NewBuffer( + []byte(`[{"op":"replace","path":"peers","value":["100.100.100.100","200.200.200.200"]}]`)), + expectedStatus: http.StatusOK, + expectedBody: true, + expectedGroup: &api.Group{ + Id: "id-existed", + PeersCount: 2, + Peers: []api.PeerMinimum{ + {Id: "100.100.100.100"}, + {Id: "200.200.200.200"}}, + }, + }, } p := initGroupTestData() @@ -170,7 +278,9 @@ func TestSaveGroup(t *testing.T) { req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody) router := mux.NewRouter() - router.HandleFunc("/api/groups", p.CreateOrUpdateGroupHandler).Methods("PUT", "POST") + router.HandleFunc("/api/groups", p.CreateGroupHandler).Methods("POST") + router.HandleFunc("/api/groups/{id}", p.UpdateGroupHandler).Methods("PUT") + router.HandleFunc("/api/groups/{id}", p.PatchGroupHandler).Methods("PATCH") router.ServeHTTP(recorder, req) res := recorder.Result() @@ -191,11 +301,10 @@ func TestSaveGroup(t *testing.T) { return } - got := &server.Group{} + got := &api.Group{} if err = json.Unmarshal(content, &got); err != nil { t.Fatalf("Sent content is not in correct json format; %v", err) } - assert.Equal(t, got, tc.expectedGroup) }) } diff --git a/management/server/http/handler/peers.go b/management/server/http/handler/peers.go index a8958e08607..2765372a4dc 100644 --- a/management/server/http/handler/peers.go +++ b/management/server/http/handler/peers.go @@ -3,13 +3,12 @@ package handler import ( "encoding/json" "fmt" - "github.com/netbirdio/netbird/management/server/jwtclaims" - "net/http" - "time" - "github.com/gorilla/mux" "github.com/netbirdio/netbird/management/server" + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/jwtclaims" log "github.com/sirupsen/logrus" + "net/http" ) //Peers is a handler that returns peers of the account @@ -19,21 +18,6 @@ type Peers struct { jwtExtractor jwtclaims.ClaimsExtractor } -//PeerResponse is a response sent to the client -type PeerResponse struct { - Name string - IP string - Connected bool - LastSeen time.Time - OS string - Version string -} - -//PeerRequest is a request sent by the client -type PeerRequest struct { - Name string -} - func NewPeers(accountManager server.AccountManager, authAudience string) *Peers { return &Peers{ accountManager: accountManager, @@ -42,21 +26,21 @@ func NewPeers(accountManager server.AccountManager, authAudience string) *Peers } } -func (h *Peers) updatePeer(accountId string, peer *server.Peer, w http.ResponseWriter, r *http.Request) { - req := &PeerRequest{} +func (h *Peers) updatePeer(account *server.Account, peer *server.Peer, w http.ResponseWriter, r *http.Request) { + req := &api.PutApiPeersIdJSONBody{} peerIp := peer.IP err := json.NewDecoder(r.Body).Decode(&req) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } - peer, err = h.accountManager.RenamePeer(accountId, peer.Key, req.Name) + peer, err = h.accountManager.RenamePeer(account.Id, peer.Key, req.Name) if err != nil { - log.Errorf("failed updating peer %s under account %s %v", peerIp, accountId, err) + log.Errorf("failed updating peer %s under account %s %v", peerIp, account.Id, err) http.Redirect(w, r, "/", http.StatusInternalServerError) return } - writeJSONObject(w, toPeerResponse(peer)) + writeJSONObject(w, toPeerResponse(peer, account)) } func (h *Peers) deletePeer(accountId string, peer *server.Peer, w http.ResponseWriter, r *http.Request) { @@ -69,19 +53,8 @@ func (h *Peers) deletePeer(accountId string, peer *server.Peer, w http.ResponseW writeJSONObject(w, "") } -func (h *Peers) getPeerAccount(r *http.Request) (*server.Account, error) { - jwtClaims := h.jwtExtractor.ExtractClaimsFromRequestContext(r, h.authAudience) - - account, err := h.accountManager.GetAccountWithAuthorizationClaims(jwtClaims) - if err != nil { - return nil, fmt.Errorf("failed getting account of a user %s: %v", jwtClaims.UserId, err) - } - - return account, nil -} - func (h *Peers) HandlePeer(w http.ResponseWriter, r *http.Request) { - account, err := h.getPeerAccount(r) + account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r) if err != nil { log.Error(err) http.Redirect(w, r, "/", http.StatusInternalServerError) @@ -105,10 +78,10 @@ func (h *Peers) HandlePeer(w http.ResponseWriter, r *http.Request) { h.deletePeer(account.Id, peer, w, r) return case http.MethodPut: - h.updatePeer(account.Id, peer, w, r) + h.updatePeer(account, peer, w, r) return case http.MethodGet: - writeJSONObject(w, toPeerResponse(peer)) + writeJSONObject(w, toPeerResponse(peer, account)) return default: @@ -120,16 +93,16 @@ func (h *Peers) HandlePeer(w http.ResponseWriter, r *http.Request) { func (h *Peers) GetPeers(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: - account, err := h.getPeerAccount(r) + account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r) if err != nil { log.Error(err) http.Redirect(w, r, "/", http.StatusInternalServerError) return } - respBody := []*PeerResponse{} + respBody := []*api.Peer{} for _, peer := range account.Peers { - respBody = append(respBody, toPeerResponse(peer)) + respBody = append(respBody, toPeerResponse(peer, account)) } writeJSONObject(w, respBody) return @@ -138,13 +111,35 @@ func (h *Peers) GetPeers(w http.ResponseWriter, r *http.Request) { } } -func toPeerResponse(peer *server.Peer) *PeerResponse { - return &PeerResponse{ +func toPeerResponse(peer *server.Peer, account *server.Account) *api.Peer { + var groupsInfo []api.GroupMinimum + groupsChecked := make(map[string]struct{}) + for _, group := range account.Groups { + _, ok := groupsChecked[group.ID] + if ok { + continue + } + groupsChecked[group.ID] = struct{}{} + for _, pk := range group.Peers { + if pk == peer.Key { + info := api.GroupMinimum{ + Id: group.ID, + Name: group.Name, + PeersCount: len(group.Peers), + } + groupsInfo = append(groupsInfo, info) + break + } + } + } + return &api.Peer{ + Id: peer.IP.String(), Name: peer.Name, - IP: peer.IP.String(), + Ip: peer.IP.String(), Connected: peer.Status.Connected, LastSeen: peer.Status.LastSeen, - OS: fmt.Sprintf("%s %s", peer.Meta.OS, peer.Meta.Core), + Os: fmt.Sprintf("%s %s", peer.Meta.OS, peer.Meta.Core), Version: peer.Meta.WtVersion, + Groups: groupsInfo, } } diff --git a/management/server/http/handler/peers_test.go b/management/server/http/handler/peers_test.go index c3ade8ff81b..132ee1d6d14 100644 --- a/management/server/http/handler/peers_test.go +++ b/management/server/http/handler/peers_test.go @@ -2,6 +2,7 @@ package handler import ( "encoding/json" + "github.com/netbirdio/netbird/management/server/http/api" "io" "net" "net/http" @@ -98,7 +99,7 @@ func TestGetPeers(t *testing.T) { t.Fatalf("I don't know what I expected; %v", err) } - respBody := []*PeerResponse{} + respBody := []*api.Peer{} err = json.Unmarshal(content, &respBody) if err != nil { t.Fatalf("Sent content is not in correct json format; %v", err) @@ -107,8 +108,8 @@ func TestGetPeers(t *testing.T) { got := respBody[0] assert.Equal(t, got.Name, peer.Name) assert.Equal(t, got.Version, peer.Meta.WtVersion) - assert.Equal(t, got.IP, peer.IP.String()) - assert.Equal(t, got.OS, "OS core") + assert.Equal(t, got.Ip, peer.IP.String()) + assert.Equal(t, got.Os, "OS core") }) } } diff --git a/management/server/http/handler/rules.go b/management/server/http/handler/rules.go index 514222f8b59..16220f30b5e 100644 --- a/management/server/http/handler/rules.go +++ b/management/server/http/handler/rules.go @@ -3,43 +3,17 @@ package handler import ( "encoding/json" "fmt" - "net/http" - + "github.com/gorilla/mux" "github.com/netbirdio/netbird/management/server" + "github.com/netbirdio/netbird/management/server/http/api" "github.com/netbirdio/netbird/management/server/jwtclaims" "github.com/rs/xid" - - "github.com/gorilla/mux" log "github.com/sirupsen/logrus" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "net/http" ) -const FlowBidirectString = "bidirect" - -// RuleResponse is a response sent to the client -type RuleResponse struct { - ID string - Name string - Source []RuleGroupResponse - Destination []RuleGroupResponse - Flow string -} - -// RuleGroupResponse is a response sent to the client -type RuleGroupResponse struct { - ID string - Name string - PeersCount int -} - -// RuleRequest to create or update rule -type RuleRequest struct { - ID string - Name string - Source []string - Destination []string - Flow string -} - // Rules is a handler that returns rules of the account type Rules struct { jwtExtractor jwtclaims.ClaimsExtractor @@ -57,14 +31,14 @@ func NewRules(accountManager server.AccountManager, authAudience string) *Rules // GetAllRulesHandler list for the account func (h *Rules) GetAllRulesHandler(w http.ResponseWriter, r *http.Request) { - account, err := h.getRuleAccount(r) + account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r) if err != nil { log.Error(err) http.Redirect(w, r, "/", http.StatusInternalServerError) return } - rules := []*RuleResponse{} + rules := []*api.Rule{} for _, r := range account.Rules { rules = append(rules, toRuleResponse(account, r)) } @@ -72,32 +46,273 @@ func (h *Rules) GetAllRulesHandler(w http.ResponseWriter, r *http.Request) { writeJSONObject(w, rules) } -func (h *Rules) CreateOrUpdateRuleHandler(w http.ResponseWriter, r *http.Request) { - account, err := h.getRuleAccount(r) +// UpdateRuleHandler handles update to a rule identified by a given ID +func (h *Rules) UpdateRuleHandler(w http.ResponseWriter, r *http.Request) { + account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r) + if err != nil { + http.Redirect(w, r, "/", http.StatusInternalServerError) + return + } + + vars := mux.Vars(r) + ruleID := vars["id"] + if len(ruleID) == 0 { + http.Error(w, "invalid rule Id", http.StatusBadRequest) + return + } + + _, ok := account.Rules[ruleID] + if !ok { + http.Error(w, fmt.Sprintf("couldn't find rule id %s", ruleID), http.StatusNotFound) + return + } + + var req api.PutApiRulesIdJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + if req.Name == "" { + http.Error(w, "Rule name shouldn't be empty", http.StatusUnprocessableEntity) + return + } + + var reqSources []string + if req.Sources != nil { + reqSources = *req.Sources + } + + var reqDestinations []string + if req.Destinations != nil { + reqDestinations = *req.Destinations + } + + rule := server.Rule{ + ID: ruleID, + Name: req.Name, + Source: reqSources, + Destination: reqDestinations, + Disabled: req.Disabled, + Description: req.Description, + } + + switch req.Flow { + case server.TrafficFlowBidirectString: + rule.Flow = server.TrafficFlowBidirect + default: + http.Error(w, "unknown flow type", http.StatusBadRequest) + return + } + + if err := h.accountManager.SaveRule(account.Id, &rule); err != nil { + log.Errorf("failed updating rule \"%s\" under account %s %v", ruleID, account.Id, err) + http.Redirect(w, r, "/", http.StatusInternalServerError) + return + } + + resp := toRuleResponse(account, &rule) + + writeJSONObject(w, &resp) +} + +// PatchRuleHandler handles patch updates to a rule identified by a given ID +func (h *Rules) PatchRuleHandler(w http.ResponseWriter, r *http.Request) { + account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r) + if err != nil { + http.Redirect(w, r, "/", http.StatusInternalServerError) + return + } + + vars := mux.Vars(r) + ruleID := vars["id"] + if len(ruleID) == 0 { + http.Error(w, "invalid rule Id", http.StatusBadRequest) + return + } + + _, ok := account.Rules[ruleID] + if !ok { + http.Error(w, fmt.Sprintf("couldn't find rule id %s", ruleID), http.StatusNotFound) + return + } + + var req api.PatchApiRulesIdJSONRequestBody + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + if len(req) == 0 { + http.Error(w, "no patch instruction received", http.StatusBadRequest) + return + } + + var operations []server.RuleUpdateOperation + + for _, patch := range req { + switch patch.Path { + case api.RulePatchOperationPathName: + if patch.Op != api.RulePatchOperationOpReplace { + http.Error(w, fmt.Sprintf("Name field only accepts replace operation, got %s", patch.Op), + http.StatusBadRequest) + return + } + if len(patch.Value) == 0 || patch.Value[0] == "" { + http.Error(w, "Rule name shouldn't be empty", http.StatusUnprocessableEntity) + return + } + operations = append(operations, server.RuleUpdateOperation{ + Type: server.UpdateRuleName, + Values: patch.Value, + }) + case api.RulePatchOperationPathDescription: + if patch.Op != api.RulePatchOperationOpReplace { + http.Error(w, fmt.Sprintf("Description field only accepts replace operation, got %s", patch.Op), + http.StatusBadRequest) + return + } + operations = append(operations, server.RuleUpdateOperation{ + Type: server.UpdateRuleDescription, + Values: patch.Value, + }) + case api.RulePatchOperationPathFlow: + if patch.Op != api.RulePatchOperationOpReplace { + http.Error(w, fmt.Sprintf("Flow field only accepts replace operation, got %s", patch.Op), + http.StatusBadRequest) + return + } + operations = append(operations, server.RuleUpdateOperation{ + Type: server.UpdateRuleFlow, + Values: patch.Value, + }) + case api.RulePatchOperationPathDisabled: + if patch.Op != api.RulePatchOperationOpReplace { + http.Error(w, fmt.Sprintf("Disabled field only accepts replace operation, got %s", patch.Op), + http.StatusBadRequest) + return + } + operations = append(operations, server.RuleUpdateOperation{ + Type: server.UpdateRuleStatus, + Values: patch.Value, + }) + case api.RulePatchOperationPathSources: + switch patch.Op { + case api.RulePatchOperationOpReplace: + operations = append(operations, server.RuleUpdateOperation{ + Type: server.UpdateSourceGroups, + Values: patch.Value, + }) + case api.RulePatchOperationOpRemove: + operations = append(operations, server.RuleUpdateOperation{ + Type: server.RemoveGroupsFromSource, + Values: patch.Value, + }) + case api.RulePatchOperationOpAdd: + operations = append(operations, server.RuleUpdateOperation{ + Type: server.InsertGroupsToSource, + Values: patch.Value, + }) + default: + http.Error(w, "invalid operation, \"%s\", for Source field", http.StatusBadRequest) + return + } + case api.RulePatchOperationPathDestinations: + switch patch.Op { + case api.RulePatchOperationOpReplace: + operations = append(operations, server.RuleUpdateOperation{ + Type: server.UpdateDestinationGroups, + Values: patch.Value, + }) + case api.RulePatchOperationOpRemove: + operations = append(operations, server.RuleUpdateOperation{ + Type: server.RemoveGroupsFromDestination, + Values: patch.Value, + }) + case api.RulePatchOperationOpAdd: + operations = append(operations, server.RuleUpdateOperation{ + Type: server.InsertGroupsToDestination, + Values: patch.Value, + }) + default: + http.Error(w, "invalid operation, \"%s\", for Destination field", http.StatusBadRequest) + return + } + default: + http.Error(w, "invalid patch path", http.StatusBadRequest) + return + } + } + + rule, err := h.accountManager.UpdateRule(account.Id, ruleID, operations) + + if err != nil { + errStatus, ok := status.FromError(err) + if ok && errStatus.Code() == codes.Internal { + http.Error(w, errStatus.String(), http.StatusInternalServerError) + return + } + + if ok && errStatus.Code() == codes.NotFound { + http.Error(w, errStatus.String(), http.StatusNotFound) + return + } + + if ok && errStatus.Code() == codes.InvalidArgument { + http.Error(w, errStatus.String(), http.StatusBadRequest) + return + } + + log.Errorf("failed updating rule %s under account %s %v", ruleID, account.Id, err) + http.Redirect(w, r, "/", http.StatusInternalServerError) + return + } + + resp := toRuleResponse(account, rule) + + writeJSONObject(w, &resp) +} + +// CreateRuleHandler handles rule creation request +func (h *Rules) CreateRuleHandler(w http.ResponseWriter, r *http.Request) { + account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r) if err != nil { http.Redirect(w, r, "/", http.StatusInternalServerError) return } - var req RuleRequest + var req api.PostApiRulesJSONRequestBody if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } - if r.Method == http.MethodPost { - req.ID = xid.New().String() + if req.Name == "" { + http.Error(w, "Rule name shouldn't be empty", http.StatusUnprocessableEntity) + return + } + + var reqSources []string + if req.Sources != nil { + reqSources = *req.Sources + } + + var reqDestinations []string + if req.Destinations != nil { + reqDestinations = *req.Destinations } rule := server.Rule{ - ID: req.ID, + ID: xid.New().String(), Name: req.Name, - Source: req.Source, - Destination: req.Destination, + Source: reqSources, + Destination: reqDestinations, + Disabled: req.Disabled, + Description: req.Description, } switch req.Flow { - case FlowBidirectString: + case server.TrafficFlowBidirectString: rule.Flow = server.TrafficFlowBidirect default: http.Error(w, "unknown flow type", http.StatusBadRequest) @@ -105,16 +320,19 @@ func (h *Rules) CreateOrUpdateRuleHandler(w http.ResponseWriter, r *http.Request } if err := h.accountManager.SaveRule(account.Id, &rule); err != nil { - log.Errorf("failed updating rule %s under account %s %v", req.ID, account.Id, err) + log.Errorf("failed creating rule \"%s\" under account %s %v", req.Name, account.Id, err) http.Redirect(w, r, "/", http.StatusInternalServerError) return } - writeJSONObject(w, &req) + resp := toRuleResponse(account, &rule) + + writeJSONObject(w, &resp) } +// DeleteRuleHandler handles rule deletion request func (h *Rules) DeleteRuleHandler(w http.ResponseWriter, r *http.Request) { - account, err := h.getRuleAccount(r) + account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r) if err != nil { http.Redirect(w, r, "/", http.StatusInternalServerError) return @@ -136,8 +354,9 @@ func (h *Rules) DeleteRuleHandler(w http.ResponseWriter, r *http.Request) { writeJSONObject(w, "") } +// GetRuleHandler handles a group Get request identified by ID func (h *Rules) GetRuleHandler(w http.ResponseWriter, r *http.Request) { - account, err := h.getRuleAccount(r) + account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r) if err != nil { http.Redirect(w, r, "/", http.StatusInternalServerError) return @@ -163,47 +382,54 @@ func (h *Rules) GetRuleHandler(w http.ResponseWriter, r *http.Request) { } } -func (h *Rules) getRuleAccount(r *http.Request) (*server.Account, error) { - jwtClaims := h.jwtExtractor.ExtractClaimsFromRequestContext(r, h.authAudience) - - account, err := h.accountManager.GetAccountWithAuthorizationClaims(jwtClaims) - if err != nil { - return nil, fmt.Errorf("failed getting account of a user %s: %v", jwtClaims.UserId, err) - } - - return account, nil -} - -func toRuleResponse(account *server.Account, rule *server.Rule) *RuleResponse { - gr := RuleResponse{ - ID: rule.ID, - Name: rule.Name, +func toRuleResponse(account *server.Account, rule *server.Rule) *api.Rule { + cache := make(map[string]api.GroupMinimum) + gr := api.Rule{ + Id: rule.ID, + Name: rule.Name, + Description: rule.Description, + Disabled: rule.Disabled, } switch rule.Flow { case server.TrafficFlowBidirect: - gr.Flow = FlowBidirectString + gr.Flow = server.TrafficFlowBidirectString default: gr.Flow = "unknown" } for _, gid := range rule.Source { + _, ok := cache[gid] + if ok { + continue + } + if group, ok := account.Groups[gid]; ok { - gr.Source = append(gr.Source, RuleGroupResponse{ - ID: group.ID, + minimum := api.GroupMinimum{ + Id: group.ID, Name: group.Name, PeersCount: len(group.Peers), - }) + } + + gr.Sources = append(gr.Sources, minimum) + cache[gid] = minimum } } for _, gid := range rule.Destination { + cachedMinimum, ok := cache[gid] + if ok { + gr.Destinations = append(gr.Destinations, cachedMinimum) + continue + } if group, ok := account.Groups[gid]; ok { - gr.Destination = append(gr.Destination, RuleGroupResponse{ - ID: group.ID, + minimum := api.GroupMinimum{ + Id: group.ID, Name: group.Name, PeersCount: len(group.Peers), - }) + } + gr.Destinations = append(gr.Destinations, minimum) + cache[gid] = minimum } } diff --git a/management/server/http/handler/rules_test.go b/management/server/http/handler/rules_test.go index 958beb4c6d0..3d307c3fed1 100644 --- a/management/server/http/handler/rules_test.go +++ b/management/server/http/handler/rules_test.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "fmt" + "github.com/netbirdio/netbird/management/server/http/api" "io" "net/http" "net/http/httptest" @@ -39,10 +40,41 @@ func initRulesTestData(rules ...*server.Rule) *Rules { Flow: server.TrafficFlowBidirect, }, nil }, + UpdateRuleFunc: func(_ string, ruleID string, operations []server.RuleUpdateOperation) (*server.Rule, error) { + var rule server.Rule + rule.ID = ruleID + for _, operation := range operations { + switch operation.Type { + case server.UpdateRuleName: + rule.Name = operation.Values[0] + case server.UpdateRuleDescription: + rule.Description = operation.Values[0] + case server.UpdateRuleFlow: + if server.TrafficFlowBidirectString == operation.Values[0] { + rule.Flow = server.TrafficFlowBidirect + } else { + rule.Flow = 100 + } + case server.UpdateSourceGroups, server.InsertGroupsToSource: + rule.Source = operation.Values + case server.UpdateDestinationGroups, server.InsertGroupsToDestination: + rule.Destination = operation.Values + case server.RemoveGroupsFromSource, server.RemoveGroupsFromDestination: + default: + return nil, fmt.Errorf("no operation") + } + } + return &rule, nil + }, GetAccountWithAuthorizationClaimsFunc: func(claims jwtclaims.AuthorizationClaims) (*server.Account, error) { return &server.Account{ Id: claims.AccountId, Domain: "hotmail.com", + Rules: map[string]*server.Rule{"id-existed": &server.Rule{ID: "id-existed"}}, + Groups: map[string]*server.Group{ + "F": &server.Group{ID: "F"}, + "G": &server.Group{ID: "G"}, + }, }, nil }, }, @@ -117,52 +149,118 @@ func TestRulesGetRule(t *testing.T) { t.Fatalf("I don't know what I expected; %v", err) } - var got RuleResponse + var got api.Rule if err = json.Unmarshal(content, &got); err != nil { t.Fatalf("Sent content is not in correct json format; %v", err) } - assert.Equal(t, got.ID, rule.ID) + assert.Equal(t, got.Id, rule.ID) assert.Equal(t, got.Name, rule.Name) }) } } -func TestRulesSaveRule(t *testing.T) { +func TestRulesWriteRule(t *testing.T) { tt := []struct { name string expectedStatus int expectedBody bool - expectedRule *server.Rule + expectedRule *api.Rule requestType string requestPath string requestBody io.Reader }{ { - name: "SaveRule POST OK", + name: "WriteRule POST OK", requestType: http.MethodPost, requestPath: "/api/rules", requestBody: bytes.NewBuffer( []byte(`{"Name":"Default POSTed Rule","Flow":"bidirect"}`)), expectedStatus: http.StatusOK, expectedBody: true, - expectedRule: &server.Rule{ - ID: "id-was-set", + expectedRule: &api.Rule{ + Id: "id-was-set", Name: "Default POSTed Rule", - Flow: server.TrafficFlowBidirect, + Flow: server.TrafficFlowBidirectString, }, }, { - name: "SaveRule PUT OK", - requestType: http.MethodPut, + name: "WriteRule POST Invalid Name", + requestType: http.MethodPost, requestPath: "/api/rules", requestBody: bytes.NewBuffer( - []byte(`{"ID":"id-existed","Name":"Default POSTed Rule","Flow":"bidirect"}`)), + []byte(`{"Name":"","Flow":"bidirect"}`)), + expectedStatus: http.StatusUnprocessableEntity, + expectedBody: false, + }, + { + name: "WriteRule PUT OK", + requestType: http.MethodPut, + requestPath: "/api/rules/id-existed", + requestBody: bytes.NewBuffer( + []byte(`{"Name":"Default POSTed Rule","Flow":"bidirect"}`)), expectedStatus: http.StatusOK, - expectedRule: &server.Rule{ - ID: "id-existed", + expectedBody: true, + expectedRule: &api.Rule{ + Id: "id-existed", Name: "Default POSTed Rule", - Flow: server.TrafficFlowBidirect, + Flow: server.TrafficFlowBidirectString, + }, + }, + { + name: "WriteRule PUT Invalid Name", + requestType: http.MethodPut, + requestPath: "/api/rules/id-existed", + requestBody: bytes.NewBuffer( + []byte(`{"Name":"","Flow":"bidirect"}`)), + expectedStatus: http.StatusUnprocessableEntity, + }, + { + name: "Write Rule PATCH Name OK", + requestType: http.MethodPatch, + requestPath: "/api/rules/id-existed", + requestBody: bytes.NewBuffer( + []byte(`[{"op":"replace","path":"name","value":["Default POSTed Rule"]}]`)), + expectedStatus: http.StatusOK, + expectedBody: true, + expectedRule: &api.Rule{ + Id: "id-existed", + Name: "Default POSTed Rule", + Flow: server.TrafficFlowBidirectString, + }, + }, + { + name: "Write Rule PATCH Invalid Name OP", + requestType: http.MethodPatch, + requestPath: "/api/rules/id-existed", + requestBody: bytes.NewBuffer( + []byte(`[{"op":"insert","path":"name","value":[""]}]`)), + expectedStatus: http.StatusBadRequest, + expectedBody: false, + }, + { + name: "Write Rule PATCH Invalid Name", + requestType: http.MethodPatch, + requestPath: "/api/rules/id-existed", + requestBody: bytes.NewBuffer( + []byte(`[{"op":"replace","path":"name","value":[]}]`)), + expectedStatus: http.StatusUnprocessableEntity, + expectedBody: false, + }, + { + name: "Write Rule PATCH Sources OK", + requestType: http.MethodPatch, + requestPath: "/api/rules/id-existed", + requestBody: bytes.NewBuffer( + []byte(`[{"op":"replace","path":"sources","value":["G","F"]}]`)), + expectedStatus: http.StatusOK, + expectedBody: true, + expectedRule: &api.Rule{ + Id: "id-existed", + Flow: server.TrafficFlowBidirectString, + Sources: []api.GroupMinimum{ + {Id: "G"}, + {Id: "F"}}, }, }, } @@ -175,7 +273,9 @@ func TestRulesSaveRule(t *testing.T) { req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody) router := mux.NewRouter() - router.HandleFunc("/api/rules", p.CreateOrUpdateRuleHandler).Methods("PUT", "POST") + router.HandleFunc("/api/rules", p.CreateRuleHandler).Methods("POST") + router.HandleFunc("/api/rules/{id}", p.UpdateRuleHandler).Methods("PUT") + router.HandleFunc("/api/rules/{id}", p.PatchRuleHandler).Methods("PATCH") router.ServeHTTP(recorder, req) res := recorder.Result() @@ -196,16 +296,13 @@ func TestRulesSaveRule(t *testing.T) { return } - got := &RuleRequest{} + got := &api.Rule{} if err = json.Unmarshal(content, &got); err != nil { t.Fatalf("Sent content is not in correct json format; %v", err) } - if tc.requestType != http.MethodPost { - assert.Equal(t, got.ID, tc.expectedRule.ID) - } - assert.Equal(t, got.Name, tc.expectedRule.Name) - assert.Equal(t, got.Flow, "bidirect") + assert.Equal(t, got, tc.expectedRule) + }) } } diff --git a/management/server/http/handler/setupkeys.go b/management/server/http/handler/setupkeys.go index 1cb12e86816..b64c9b03eae 100644 --- a/management/server/http/handler/setupkeys.go +++ b/management/server/http/handler/setupkeys.go @@ -2,56 +2,34 @@ package handler import ( "encoding/json" - "fmt" - "github.com/netbirdio/netbird/management/server/jwtclaims" - "net/http" - "time" - "github.com/gorilla/mux" "github.com/netbirdio/netbird/management/server" - "github.com/netbirdio/netbird/util" + "github.com/netbirdio/netbird/management/server/http/api" + "github.com/netbirdio/netbird/management/server/jwtclaims" log "github.com/sirupsen/logrus" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "net/http" + "time" ) // SetupKeys is a handler that returns a list of setup keys of the account type SetupKeys struct { accountManager server.AccountManager + jwtExtractor jwtclaims.ClaimsExtractor authAudience string } -// SetupKeyResponse is a response sent to the client -type SetupKeyResponse struct { - Id string - Key string - Name string - Expires time.Time - Type server.SetupKeyType - Valid bool - Revoked bool - UsedTimes int - LastUsed time.Time - State string -} - -// SetupKeyRequest is a request sent by client. This object contains fields that can be modified -type SetupKeyRequest struct { - Name string - Type server.SetupKeyType - ExpiresIn *util.Duration - Revoked bool -} - func NewSetupKeysHandler(accountManager server.AccountManager, authAudience string) *SetupKeys { return &SetupKeys{ accountManager: accountManager, authAudience: authAudience, + jwtExtractor: *jwtclaims.NewClaimsExtractor(nil), } } func (h *SetupKeys) updateKey(accountId string, keyId string, w http.ResponseWriter, r *http.Request) { - req := &SetupKeyRequest{} + req := &api.PutApiSetupKeysIdJSONRequestBody{} err := json.NewDecoder(r.Body).Decode(&req) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) @@ -96,19 +74,28 @@ func (h *SetupKeys) getKey(accountId string, keyId string, w http.ResponseWriter } func (h *SetupKeys) createKey(accountId string, w http.ResponseWriter, r *http.Request) { - req := &SetupKeyRequest{} + req := &api.PostApiSetupKeysJSONRequestBody{} err := json.NewDecoder(r.Body).Decode(&req) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } - if !(req.Type == server.SetupKeyReusable || req.Type == server.SetupKeyOneOff) { + if req.Name == "" { + http.Error(w, "Setup key name shouldn't be empty", http.StatusUnprocessableEntity) + return + } + + if !(server.SetupKeyType(req.Type) == server.SetupKeyReusable || + server.SetupKeyType(req.Type) == server.SetupKeyOneOff) { + http.Error(w, "unknown setup key type "+string(req.Type), http.StatusBadRequest) return } - setupKey, err := h.accountManager.AddSetupKey(accountId, req.Name, req.Type, req.ExpiresIn) + expiresIn := time.Duration(req.ExpiresIn) * time.Second + + setupKey, err := h.accountManager.AddSetupKey(accountId, req.Name, server.SetupKeyType(req.Type), expiresIn) if err != nil { errStatus, ok := status.FromError(err) if ok && errStatus.Code() == codes.NotFound { @@ -122,20 +109,8 @@ func (h *SetupKeys) createKey(accountId string, w http.ResponseWriter, r *http.R writeSuccess(w, setupKey) } -func (h *SetupKeys) getSetupKeyAccount(r *http.Request) (*server.Account, error) { - extractor := jwtclaims.NewClaimsExtractor(nil) - jwtClaims := extractor.ExtractClaimsFromRequestContext(r, h.authAudience) - - account, err := h.accountManager.GetAccountWithAuthorizationClaims(jwtClaims) - if err != nil { - return nil, fmt.Errorf("failed getting account of a user %s: %v", jwtClaims.UserId, err) - } - - return account, nil -} - func (h *SetupKeys) HandleKey(w http.ResponseWriter, r *http.Request) { - account, err := h.getSetupKeyAccount(r) + account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r) if err != nil { log.Error(err) http.Redirect(w, r, "/", http.StatusInternalServerError) @@ -163,7 +138,7 @@ func (h *SetupKeys) HandleKey(w http.ResponseWriter, r *http.Request) { func (h *SetupKeys) GetKeys(w http.ResponseWriter, r *http.Request) { - account, err := h.getSetupKeyAccount(r) + account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r) if err != nil { log.Error(err) http.Redirect(w, r, "/", http.StatusInternalServerError) @@ -178,7 +153,7 @@ func (h *SetupKeys) GetKeys(w http.ResponseWriter, r *http.Request) { w.WriteHeader(200) w.Header().Set("Content-Type", "application/json") - respBody := []*SetupKeyResponse{} + respBody := []*api.SetupKey{} for _, key := range account.SetupKeys { respBody = append(respBody, toResponseBody(key)) } @@ -204,7 +179,7 @@ func writeSuccess(w http.ResponseWriter, key *server.SetupKey) { } } -func toResponseBody(key *server.SetupKey) *SetupKeyResponse { +func toResponseBody(key *server.SetupKey) *api.SetupKey { var state string if key.IsExpired() { state = "expired" @@ -215,12 +190,12 @@ func toResponseBody(key *server.SetupKey) *SetupKeyResponse { } else { state = "valid" } - return &SetupKeyResponse{ + return &api.SetupKey{ Id: key.Id, Key: key.Key, Name: key.Name, Expires: key.ExpiresAt, - Type: key.Type, + Type: string(key.Type), Valid: key.IsValid(), Revoked: key.Revoked, UsedTimes: key.UsedTimes, diff --git a/management/server/http/handler/users.go b/management/server/http/handler/users.go index 572f59319e7..113a0dfba05 100644 --- a/management/server/http/handler/users.go +++ b/management/server/http/handler/users.go @@ -1,7 +1,7 @@ package handler import ( - "fmt" + "github.com/netbirdio/netbird/management/server/http/api" "net/http" log "github.com/sirupsen/logrus" @@ -16,13 +16,6 @@ type UserHandler struct { jwtExtractor jwtclaims.ClaimsExtractor } -type UserResponse struct { - ID string `json:"id"` - Email string `json:"email"` - Name string `json:"name"` - Role string `json:"role"` -} - func NewUserHandler(accountManager server.AccountManager, authAudience string) *UserHandler { return &UserHandler{ accountManager: accountManager, @@ -31,37 +24,26 @@ func NewUserHandler(accountManager server.AccountManager, authAudience string) * } } -func (u *UserHandler) getAccountId(r *http.Request) (*server.Account, error) { - jwtClaims := u.jwtExtractor.ExtractClaimsFromRequestContext(r, u.authAudience) - - account, err := u.accountManager.GetAccountWithAuthorizationClaims(jwtClaims) - if err != nil { - return nil, fmt.Errorf("failed getting account of a user %s: %v", jwtClaims.UserId, err) - } - - return account, nil -} - // GetUsers returns a list of users of the account this user belongs to. // It also gathers additional user data (like email and name) from the IDP manager. -func (u *UserHandler) GetUsers(w http.ResponseWriter, r *http.Request) { +func (h *UserHandler) GetUsers(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "", http.StatusBadRequest) } - account, err := u.getAccountId(r) + account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r) if err != nil { log.Error(err) } - data, err := u.accountManager.GetUsersFromAccount(account.Id) + data, err := h.accountManager.GetUsersFromAccount(account.Id) if err != nil { log.Error(err) http.Redirect(w, r, "/", http.StatusInternalServerError) return } - users := []*UserResponse{} + users := []*api.User{} for _, r := range data { users = append(users, toUserResponse(r)) } @@ -69,9 +51,9 @@ func (u *UserHandler) GetUsers(w http.ResponseWriter, r *http.Request) { writeJSONObject(w, users) } -func toUserResponse(user *server.UserInfo) *UserResponse { - return &UserResponse{ - ID: user.ID, +func toUserResponse(user *server.UserInfo) *api.User { + return &api.User{ + Id: user.ID, Name: user.Name, Email: user.Email, Role: user.Role, diff --git a/management/server/http/handler/util.go b/management/server/http/handler/util.go index 08b1c7d3ec1..5ae2a0d6fc8 100644 --- a/management/server/http/handler/util.go +++ b/management/server/http/handler/util.go @@ -3,6 +3,9 @@ package handler import ( "encoding/json" "errors" + "fmt" + "github.com/netbirdio/netbird/management/server" + "github.com/netbirdio/netbird/management/server/jwtclaims" "net/http" "time" ) @@ -47,3 +50,17 @@ func (d *Duration) UnmarshalJSON(b []byte) error { return errors.New("invalid duration") } } + +func getJWTAccount(accountManager server.AccountManager, + jwtExtractor jwtclaims.ClaimsExtractor, + authAudience string, r *http.Request) (*server.Account, error) { + + jwtClaims := jwtExtractor.ExtractClaimsFromRequestContext(r, authAudience) + + account, err := accountManager.GetAccountWithAuthorizationClaims(jwtClaims) + if err != nil { + return nil, fmt.Errorf("failed getting account of a user %s: %v", jwtClaims.UserId, err) + } + + return account, nil +} diff --git a/management/server/http/server.go b/management/server/http/server.go index 61ebc4dfe37..639e1d0124a 100644 --- a/management/server/http/server.go +++ b/management/server/http/server.go @@ -103,13 +103,12 @@ func (s *Server) Start() error { rulesHandler := handler.NewRules(s.accountManager, s.config.AuthAudience) peersHandler := handler.NewPeers(s.accountManager, s.config.AuthAudience) keysHandler := handler.NewSetupKeysHandler(s.accountManager, s.config.AuthAudience) + userHandler := handler.NewUserHandler(s.accountManager, s.config.AuthAudience) + r.HandleFunc("/api/peers", peersHandler.GetPeers).Methods("GET", "OPTIONS") r.HandleFunc("/api/peers/{id}", peersHandler.HandlePeer). Methods("GET", "PUT", "DELETE", "OPTIONS") - - userHandler := handler.NewUserHandler(s.accountManager, s.config.AuthAudience) r.HandleFunc("/api/users", userHandler.GetUsers).Methods("GET", "OPTIONS") - r.HandleFunc("/api/setup-keys", keysHandler.GetKeys).Methods("GET", "POST", "OPTIONS") r.HandleFunc("/api/setup-keys/{id}", keysHandler.HandleKey).Methods("GET", "PUT", "OPTIONS") @@ -118,14 +117,16 @@ func (s *Server) Start() error { Methods("GET", "PUT", "DELETE", "OPTIONS") r.HandleFunc("/api/rules", rulesHandler.GetAllRulesHandler).Methods("GET", "OPTIONS") - r.HandleFunc("/api/rules", rulesHandler.CreateOrUpdateRuleHandler). - Methods("POST", "PUT", "OPTIONS") + r.HandleFunc("/api/rules", rulesHandler.CreateRuleHandler).Methods("POST", "OPTIONS") + r.HandleFunc("/api/rules/{id}", rulesHandler.UpdateRuleHandler).Methods("PUT", "OPTIONS") + r.HandleFunc("/api/rules/{id}", rulesHandler.PatchRuleHandler).Methods("PATCH", "OPTIONS") r.HandleFunc("/api/rules/{id}", rulesHandler.GetRuleHandler).Methods("GET", "OPTIONS") r.HandleFunc("/api/rules/{id}", rulesHandler.DeleteRuleHandler).Methods("DELETE", "OPTIONS") r.HandleFunc("/api/groups", groupsHandler.GetAllGroupsHandler).Methods("GET", "OPTIONS") - r.HandleFunc("/api/groups", groupsHandler.CreateOrUpdateGroupHandler). - Methods("POST", "PUT", "OPTIONS") + r.HandleFunc("/api/groups", groupsHandler.CreateGroupHandler).Methods("POST", "OPTIONS") + r.HandleFunc("/api/groups/{id}", groupsHandler.UpdateGroupHandler).Methods("PUT", "OPTIONS") + r.HandleFunc("/api/groups/{id}", groupsHandler.PatchGroupHandler).Methods("PATCH", "OPTIONS") r.HandleFunc("/api/groups/{id}", groupsHandler.GetGroupHandler).Methods("GET", "OPTIONS") r.HandleFunc("/api/groups/{id}", groupsHandler.DeleteGroupHandler).Methods("DELETE", "OPTIONS") http.Handle("/", r) diff --git a/management/server/jwtclaims/extractor.go b/management/server/jwtclaims/extractor.go index 1a9432459ec..a794dd3b431 100644 --- a/management/server/jwtclaims/extractor.go +++ b/management/server/jwtclaims/extractor.go @@ -36,6 +36,9 @@ func NewClaimsExtractor(e ExtractClaims) *ClaimsExtractor { // ExtractClaimsFromRequestContext extracts claims from the request context previously filled by the JWT token (after auth) func ExtractClaimsFromRequestContext(r *http.Request, authAudience string) AuthorizationClaims { + if r.Context().Value(TokenUserProperty) == nil { + return AuthorizationClaims{} + } token := r.Context().Value(TokenUserProperty).(*jwt.Token) return ExtractClaimsWithToken(token, authAudience) } diff --git a/management/server/mock_server/account_mock.go b/management/server/mock_server/account_mock.go index 341f7a3f2da..c49ee5bbe24 100644 --- a/management/server/mock_server/account_mock.go +++ b/management/server/mock_server/account_mock.go @@ -3,15 +3,15 @@ package mock_server import ( "github.com/netbirdio/netbird/management/server" "github.com/netbirdio/netbird/management/server/jwtclaims" - "github.com/netbirdio/netbird/util" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "time" ) type MockAccountManager struct { GetOrCreateAccountByUserFunc func(userId, domain string) (*server.Account, error) GetAccountByUserFunc func(userId string) (*server.Account, error) - AddSetupKeyFunc func(accountId string, keyName string, keyType server.SetupKeyType, expiresIn *util.Duration) (*server.SetupKey, error) + AddSetupKeyFunc func(accountId string, keyName string, keyType server.SetupKeyType, expiresIn time.Duration) (*server.SetupKey, error) RevokeSetupKeyFunc func(accountId string, keyId string) (*server.SetupKey, error) RenameSetupKeyFunc func(accountId string, keyId string, newName string) (*server.SetupKey, error) GetAccountByIdFunc func(accountId string) (*server.Account, error) @@ -28,6 +28,7 @@ type MockAccountManager struct { AddPeerFunc func(setupKey string, userId string, peer *server.Peer) (*server.Peer, error) GetGroupFunc func(accountID, groupID string) (*server.Group, error) SaveGroupFunc func(accountID string, group *server.Group) error + UpdateGroupFunc func(accountID string, groupID string, operations []server.GroupUpdateOperation) (*server.Group, error) DeleteGroupFunc func(accountID, groupID string) error ListGroupsFunc func(accountID string) ([]*server.Group, error) GroupAddPeerFunc func(accountID, groupID, peerKey string) error @@ -35,12 +36,14 @@ type MockAccountManager struct { GroupListPeersFunc func(accountID, groupID string) ([]*server.Peer, error) GetRuleFunc func(accountID, ruleID string) (*server.Rule, error) SaveRuleFunc func(accountID string, rule *server.Rule) error + UpdateRuleFunc func(accountID string, ruleID string, operations []server.RuleUpdateOperation) (*server.Rule, error) DeleteRuleFunc func(accountID, ruleID string) error ListRulesFunc func(accountID string) ([]*server.Rule, error) GetUsersFromAccountFunc func(accountID string) ([]*server.UserInfo, error) UpdatePeerMetaFunc func(peerKey string, meta server.PeerSystemMeta) error } +// GetUsersFromAccount mock implementation of GetUsersFromAccount from server.AccountManager interface func (am *MockAccountManager) GetUsersFromAccount(accountID string) ([]*server.UserInfo, error) { if am.GetUsersFromAccountFunc != nil { return am.GetUsersFromAccountFunc(accountID) @@ -48,6 +51,7 @@ func (am *MockAccountManager) GetUsersFromAccount(accountID string) ([]*server.U return nil, status.Errorf(codes.Unimplemented, "method GetUsersFromAccount not implemented") } +// GetOrCreateAccountByUser mock implementation of GetOrCreateAccountByUser from server.AccountManager interface func (am *MockAccountManager) GetOrCreateAccountByUser( userId, domain string, ) (*server.Account, error) { @@ -60,6 +64,7 @@ func (am *MockAccountManager) GetOrCreateAccountByUser( ) } +// GetAccountByUser mock implementation of GetAccountByUser from server.AccountManager interface func (am *MockAccountManager) GetAccountByUser(userId string) (*server.Account, error) { if am.GetAccountByUserFunc != nil { return am.GetAccountByUserFunc(userId) @@ -67,11 +72,12 @@ func (am *MockAccountManager) GetAccountByUser(userId string) (*server.Account, return nil, status.Errorf(codes.Unimplemented, "method GetAccountByUser not implemented") } +// AddSetupKey mock implementation of AddSetupKey from server.AccountManager interface func (am *MockAccountManager) AddSetupKey( accountId string, keyName string, keyType server.SetupKeyType, - expiresIn *util.Duration, + expiresIn time.Duration, ) (*server.SetupKey, error) { if am.AddSetupKeyFunc != nil { return am.AddSetupKeyFunc(accountId, keyName, keyType, expiresIn) @@ -79,6 +85,7 @@ func (am *MockAccountManager) AddSetupKey( return nil, status.Errorf(codes.Unimplemented, "method AddSetupKey not implemented") } +// RevokeSetupKey mock implementation of RevokeSetupKey from server.AccountManager interface func (am *MockAccountManager) RevokeSetupKey( accountId string, keyId string, @@ -89,6 +96,7 @@ func (am *MockAccountManager) RevokeSetupKey( return nil, status.Errorf(codes.Unimplemented, "method RevokeSetupKey not implemented") } +// RenameSetupKey mock implementation of RenameSetupKey from server.AccountManager interface func (am *MockAccountManager) RenameSetupKey( accountId string, keyId string, @@ -100,6 +108,7 @@ func (am *MockAccountManager) RenameSetupKey( return nil, status.Errorf(codes.Unimplemented, "method RenameSetupKey not implemented") } +// GetAccountById mock implementation of GetAccountById from server.AccountManager interface func (am *MockAccountManager) GetAccountById(accountId string) (*server.Account, error) { if am.GetAccountByIdFunc != nil { return am.GetAccountByIdFunc(accountId) @@ -107,6 +116,7 @@ func (am *MockAccountManager) GetAccountById(accountId string) (*server.Account, return nil, status.Errorf(codes.Unimplemented, "method GetAccountById not implemented") } +// GetAccountByUserOrAccountId mock implementation of GetAccountByUserOrAccountId from server.AccountManager interface func (am *MockAccountManager) GetAccountByUserOrAccountId( userId, accountId, domain string, ) (*server.Account, error) { @@ -119,6 +129,7 @@ func (am *MockAccountManager) GetAccountByUserOrAccountId( ) } +// GetAccountWithAuthorizationClaims mock implementation of GetAccountWithAuthorizationClaims from server.AccountManager interface func (am *MockAccountManager) GetAccountWithAuthorizationClaims( claims jwtclaims.AuthorizationClaims, ) (*server.Account, error) { @@ -131,6 +142,7 @@ func (am *MockAccountManager) GetAccountWithAuthorizationClaims( ) } +// AccountExists mock implementation of AccountExists from server.AccountManager interface func (am *MockAccountManager) AccountExists(accountId string) (*bool, error) { if am.AccountExistsFunc != nil { return am.AccountExistsFunc(accountId) @@ -138,6 +150,7 @@ func (am *MockAccountManager) AccountExists(accountId string) (*bool, error) { return nil, status.Errorf(codes.Unimplemented, "method AccountExists not implemented") } +// GetPeer mock implementation of GetPeer from server.AccountManager interface func (am *MockAccountManager) GetPeer(peerKey string) (*server.Peer, error) { if am.GetPeerFunc != nil { return am.GetPeerFunc(peerKey) @@ -145,6 +158,7 @@ func (am *MockAccountManager) GetPeer(peerKey string) (*server.Peer, error) { return nil, status.Errorf(codes.Unimplemented, "method GetPeer not implemented") } +// MarkPeerConnected mock implementation of MarkPeerConnected from server.AccountManager interface func (am *MockAccountManager) MarkPeerConnected(peerKey string, connected bool) error { if am.MarkPeerConnectedFunc != nil { return am.MarkPeerConnectedFunc(peerKey, connected) @@ -152,6 +166,7 @@ func (am *MockAccountManager) MarkPeerConnected(peerKey string, connected bool) return status.Errorf(codes.Unimplemented, "method MarkPeerConnected not implemented") } +// RenamePeer mock implementation of RenamePeer from server.AccountManager interface func (am *MockAccountManager) RenamePeer( accountId string, peerKey string, @@ -163,6 +178,7 @@ func (am *MockAccountManager) RenamePeer( return nil, status.Errorf(codes.Unimplemented, "method RenamePeer not implemented") } +// DeletePeer mock implementation of DeletePeer from server.AccountManager interface func (am *MockAccountManager) DeletePeer(accountId string, peerKey string) (*server.Peer, error) { if am.DeletePeerFunc != nil { return am.DeletePeerFunc(accountId, peerKey) @@ -170,6 +186,7 @@ func (am *MockAccountManager) DeletePeer(accountId string, peerKey string) (*ser return nil, status.Errorf(codes.Unimplemented, "method DeletePeer not implemented") } +// GetPeerByIP mock implementation of GetPeerByIP from server.AccountManager interface func (am *MockAccountManager) GetPeerByIP(accountId string, peerIP string) (*server.Peer, error) { if am.GetPeerByIPFunc != nil { return am.GetPeerByIPFunc(accountId, peerIP) @@ -177,6 +194,7 @@ func (am *MockAccountManager) GetPeerByIP(accountId string, peerIP string) (*ser return nil, status.Errorf(codes.Unimplemented, "method GetPeerByIP not implemented") } +// GetNetworkMap mock implementation of GetNetworkMap from server.AccountManager interface func (am *MockAccountManager) GetNetworkMap(peerKey string) (*server.NetworkMap, error) { if am.GetNetworkMapFunc != nil { return am.GetNetworkMapFunc(peerKey) @@ -184,6 +202,7 @@ func (am *MockAccountManager) GetNetworkMap(peerKey string) (*server.NetworkMap, return nil, status.Errorf(codes.Unimplemented, "method GetNetworkMap not implemented") } +// AddPeer mock implementation of AddPeer from server.AccountManager interface func (am *MockAccountManager) AddPeer( setupKey string, userId string, @@ -195,6 +214,7 @@ func (am *MockAccountManager) AddPeer( return nil, status.Errorf(codes.Unimplemented, "method AddPeer not implemented") } +// GetGroup mock implementation of GetGroup from server.AccountManager interface func (am *MockAccountManager) GetGroup(accountID, groupID string) (*server.Group, error) { if am.GetGroupFunc != nil { return am.GetGroupFunc(accountID, groupID) @@ -202,6 +222,7 @@ func (am *MockAccountManager) GetGroup(accountID, groupID string) (*server.Group return nil, status.Errorf(codes.Unimplemented, "method GetGroup not implemented") } +// SaveGroup mock implementation of SaveGroup from server.AccountManager interface func (am *MockAccountManager) SaveGroup(accountID string, group *server.Group) error { if am.SaveGroupFunc != nil { return am.SaveGroupFunc(accountID, group) @@ -209,6 +230,15 @@ func (am *MockAccountManager) SaveGroup(accountID string, group *server.Group) e return status.Errorf(codes.Unimplemented, "method SaveGroup not implemented") } +// UpdateGroup mock implementation of UpdateGroup from server.AccountManager interface +func (am *MockAccountManager) UpdateGroup(accountID string, groupID string, operations []server.GroupUpdateOperation) (*server.Group, error) { + if am.UpdateGroupFunc != nil { + return am.UpdateGroupFunc(accountID, groupID, operations) + } + return nil, status.Errorf(codes.Unimplemented, "method UpdateGroup not implemented") +} + +// DeleteGroup mock implementation of DeleteGroup from server.AccountManager interface func (am *MockAccountManager) DeleteGroup(accountID, groupID string) error { if am.DeleteGroupFunc != nil { return am.DeleteGroupFunc(accountID, groupID) @@ -216,6 +246,7 @@ func (am *MockAccountManager) DeleteGroup(accountID, groupID string) error { return status.Errorf(codes.Unimplemented, "method DeleteGroup not implemented") } +// ListGroups mock implementation of ListGroups from server.AccountManager interface func (am *MockAccountManager) ListGroups(accountID string) ([]*server.Group, error) { if am.ListGroupsFunc != nil { return am.ListGroupsFunc(accountID) @@ -223,6 +254,7 @@ func (am *MockAccountManager) ListGroups(accountID string) ([]*server.Group, err return nil, status.Errorf(codes.Unimplemented, "method ListGroups not implemented") } +// GroupAddPeer mock implementation of GroupAddPeer from server.AccountManager interface func (am *MockAccountManager) GroupAddPeer(accountID, groupID, peerKey string) error { if am.GroupAddPeerFunc != nil { return am.GroupAddPeerFunc(accountID, groupID, peerKey) @@ -230,6 +262,7 @@ func (am *MockAccountManager) GroupAddPeer(accountID, groupID, peerKey string) e return status.Errorf(codes.Unimplemented, "method GroupAddPeer not implemented") } +// GroupDeletePeer mock implementation of GroupDeletePeer from server.AccountManager interface func (am *MockAccountManager) GroupDeletePeer(accountID, groupID, peerKey string) error { if am.GroupDeletePeerFunc != nil { return am.GroupDeletePeerFunc(accountID, groupID, peerKey) @@ -237,6 +270,7 @@ func (am *MockAccountManager) GroupDeletePeer(accountID, groupID, peerKey string return status.Errorf(codes.Unimplemented, "method GroupDeletePeer not implemented") } +// GroupListPeers mock implementation of GroupListPeers from server.AccountManager interface func (am *MockAccountManager) GroupListPeers(accountID, groupID string) ([]*server.Peer, error) { if am.GroupListPeersFunc != nil { return am.GroupListPeersFunc(accountID, groupID) @@ -244,6 +278,7 @@ func (am *MockAccountManager) GroupListPeers(accountID, groupID string) ([]*serv return nil, status.Errorf(codes.Unimplemented, "method GroupListPeers not implemented") } +// GetRule mock implementation of GetRule from server.AccountManager interface func (am *MockAccountManager) GetRule(accountID, ruleID string) (*server.Rule, error) { if am.GetRuleFunc != nil { return am.GetRuleFunc(accountID, ruleID) @@ -251,6 +286,7 @@ func (am *MockAccountManager) GetRule(accountID, ruleID string) (*server.Rule, e return nil, status.Errorf(codes.Unimplemented, "method GetRule not implemented") } +// SaveRule mock implementation of SaveRule from server.AccountManager interface func (am *MockAccountManager) SaveRule(accountID string, rule *server.Rule) error { if am.SaveRuleFunc != nil { return am.SaveRuleFunc(accountID, rule) @@ -258,6 +294,15 @@ func (am *MockAccountManager) SaveRule(accountID string, rule *server.Rule) erro return status.Errorf(codes.Unimplemented, "method SaveRule not implemented") } +// UpdateRule mock implementation of UpdateRule from server.AccountManager interface +func (am *MockAccountManager) UpdateRule(accountID string, ruleID string, operations []server.RuleUpdateOperation) (*server.Rule, error) { + if am.UpdateRuleFunc != nil { + return am.UpdateRuleFunc(accountID, ruleID, operations) + } + return nil, status.Errorf(codes.Unimplemented, "method UpdateRule not implemented") +} + +// DeleteRule mock implementation of DeleteRule from server.AccountManager interface func (am *MockAccountManager) DeleteRule(accountID, ruleID string) error { if am.DeleteRuleFunc != nil { return am.DeleteRuleFunc(accountID, ruleID) @@ -265,6 +310,7 @@ func (am *MockAccountManager) DeleteRule(accountID, ruleID string) error { return status.Errorf(codes.Unimplemented, "method DeleteRule not implemented") } +// ListRules mock implementation of ListRules from server.AccountManager interface func (am *MockAccountManager) ListRules(accountID string) ([]*server.Rule, error) { if am.ListRulesFunc != nil { return am.ListRulesFunc(accountID) @@ -272,6 +318,7 @@ func (am *MockAccountManager) ListRules(accountID string) ([]*server.Rule, error return nil, status.Errorf(codes.Unimplemented, "method ListRules not implemented") } +// UpdatePeerMeta mock implementation of UpdatePeerMeta from server.AccountManager interface func (am *MockAccountManager) UpdatePeerMeta(peerKey string, meta server.PeerSystemMeta) error { if am.UpdatePeerMetaFunc != nil { return am.UpdatePeerMetaFunc(peerKey, meta) @@ -279,6 +326,7 @@ func (am *MockAccountManager) UpdatePeerMeta(peerKey string, meta server.PeerSys return status.Errorf(codes.Unimplemented, "method UpdatePeerMetaFunc not implemented") } +// IsUserAdmin mock implementation of IsUserAdmin from server.AccountManager interface func (am *MockAccountManager) IsUserAdmin(claims jwtclaims.AuthorizationClaims) (bool, error) { if am.IsUserAdminFunc != nil { return am.IsUserAdminFunc(claims) diff --git a/management/server/peer.go b/management/server/peer.go index e76f2b067ec..735baced7ed 100644 --- a/management/server/peer.go +++ b/management/server/peer.go @@ -360,6 +360,9 @@ func (am *DefaultAccountManager) getPeersByACL(account *Account, peerKey string) groups := map[string]*Group{} for _, r := range srcRules { + if r.Disabled { + continue + } if r.Flow == TrafficFlowBidirect { for _, gid := range r.Destination { if group, ok := account.Groups[gid]; ok { @@ -370,6 +373,9 @@ func (am *DefaultAccountManager) getPeersByACL(account *Account, peerKey string) } for _, r := range dstRules { + if r.Disabled { + continue + } if r.Flow == TrafficFlowBidirect { for _, gid := range r.Source { if group, ok := account.Groups[gid]; ok { diff --git a/management/server/peer_test.go b/management/server/peer_test.go index c48ac99750d..b99b27e8417 100644 --- a/management/server/peer_test.go +++ b/management/server/peer_test.go @@ -220,4 +220,36 @@ func TestAccountManager_GetNetworkMapWithRule(t *testing.T) { networkMap2.Peers[0].Key, ) } + + rule.Disabled = true + err = manager.SaveRule(account.Id, &rule) + if err != nil { + t.Errorf("expecting rule to be added, got failure %v", err) + return + } + + networkMap1, err = manager.GetNetworkMap(peerKey1.PublicKey().String()) + if err != nil { + t.Fatal(err) + return + } + + if len(networkMap1.Peers) != 0 { + t.Errorf( + "expecting Account NetworkMap to have 0 peers, got %v: %v", + len(networkMap1.Peers), + networkMap1.Peers, + ) + return + } + + networkMap2, err = manager.GetNetworkMap(peerKey2.PublicKey().String()) + if err != nil { + t.Fatal(err) + return + } + + if len(networkMap2.Peers) != 0 { + t.Errorf("expecting Account NetworkMap to have 0 peers, got %v", len(networkMap2.Peers)) + } } diff --git a/management/server/rule.go b/management/server/rule.go index ebb8caab26e..82689f34730 100644 --- a/management/server/rule.go +++ b/management/server/rule.go @@ -3,6 +3,7 @@ package server import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "strings" ) // TrafficFlowType defines allowed direction of the traffic in the rule @@ -11,6 +12,12 @@ type TrafficFlowType int const ( // TrafficFlowBidirect allows traffic to both direction TrafficFlowBidirect TrafficFlowType = iota + // TrafficFlowBidirectString allows traffic to both direction + TrafficFlowBidirectString = "bidirect" + // DefaultRuleName is a name for the Default rule that is created for every account + DefaultRuleName = "Default" + // DefaultRuleDescription is a description for the Default rule that is created for every account + DefaultRuleDescription = "This is a default rule that allows connections between all the resources" ) // Rule of ACL for groups @@ -21,6 +28,12 @@ type Rule struct { // Name of the rule visible in the UI Name string + // Description of the rule visible in the UI + Description string + + // Disabled status of rule in the system + Disabled bool + // Source list of groups IDs of peers Source []string @@ -31,10 +44,44 @@ type Rule struct { Flow TrafficFlowType } +const ( + // UpdateRuleName indicates a rule name update operation + UpdateRuleName RuleUpdateOperationType = iota + // UpdateRuleDescription indicates a rule description update operation + UpdateRuleDescription + // UpdateRuleStatus indicates a rule status update operation + UpdateRuleStatus + // UpdateRuleFlow indicates a rule flow update operation + UpdateRuleFlow + // InsertGroupsToSource indicates an insert groups to source rule operation + InsertGroupsToSource + // RemoveGroupsFromSource indicates an remove groups from source rule operation + RemoveGroupsFromSource + // UpdateSourceGroups indicates a replacement of source group list of a rule operation + UpdateSourceGroups + // InsertGroupsToDestination indicates an insert groups to destination rule operation + InsertGroupsToDestination + // RemoveGroupsFromDestination indicates an remove groups from destination rule operation + RemoveGroupsFromDestination + // UpdateDestinationGroups indicates a replacement of destination group list of a rule operation + UpdateDestinationGroups +) + +// RuleUpdateOperationType operation type +type RuleUpdateOperationType int + +// RuleUpdateOperation operation object with type and values to be applied +type RuleUpdateOperation struct { + Type RuleUpdateOperationType + Values []string +} + func (r *Rule) Copy() *Rule { return &Rule{ ID: r.ID, Name: r.Name, + Description: r.Description, + Disabled: r.Disabled, Source: r.Source[:], Destination: r.Destination[:], Flow: r.Flow, @@ -79,6 +126,81 @@ func (am *DefaultAccountManager) SaveRule(accountID string, rule *Rule) error { return am.updateAccountPeers(account) } +// UpdateRule updates a rule using a list of operations +func (am *DefaultAccountManager) UpdateRule(accountID string, ruleID string, + operations []RuleUpdateOperation) (*Rule, error) { + am.mux.Lock() + defer am.mux.Unlock() + + account, err := am.Store.GetAccount(accountID) + if err != nil { + return nil, status.Errorf(codes.NotFound, "account not found") + } + + ruleToUpdate, ok := account.Rules[ruleID] + if !ok { + return nil, status.Errorf(codes.NotFound, "rule %s no longer exists", ruleID) + } + + rule := ruleToUpdate.Copy() + + for _, operation := range operations { + switch operation.Type { + case UpdateRuleName: + rule.Name = operation.Values[0] + case UpdateRuleDescription: + rule.Description = operation.Values[0] + case UpdateRuleFlow: + if operation.Values[0] != TrafficFlowBidirectString { + return nil, status.Errorf(codes.InvalidArgument, "failed to parse flow") + } + rule.Flow = TrafficFlowBidirect + case UpdateRuleStatus: + if strings.ToLower(operation.Values[0]) == "true" { + rule.Disabled = true + } else if strings.ToLower(operation.Values[0]) == "false" { + rule.Disabled = false + } else { + return nil, status.Errorf(codes.InvalidArgument, "failed to parse status") + } + case UpdateSourceGroups: + rule.Source = operation.Values + case InsertGroupsToSource: + sourceList := rule.Source + resultList := removeFromList(sourceList, operation.Values) + rule.Source = append(resultList, operation.Values...) + case RemoveGroupsFromSource: + sourceList := rule.Source + resultList := removeFromList(sourceList, operation.Values) + rule.Source = resultList + case UpdateDestinationGroups: + rule.Destination = operation.Values + case InsertGroupsToDestination: + sourceList := rule.Destination + resultList := removeFromList(sourceList, operation.Values) + rule.Destination = append(resultList, operation.Values...) + case RemoveGroupsFromDestination: + sourceList := rule.Destination + resultList := removeFromList(sourceList, operation.Values) + rule.Destination = resultList + } + } + + account.Rules[ruleID] = rule + + account.Network.IncSerial() + if err = am.Store.SaveAccount(account); err != nil { + return nil, err + } + + err = am.updateAccountPeers(account) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to update account peers") + } + + return rule, nil +} + // DeleteRule of ACL from the store func (am *DefaultAccountManager) DeleteRule(accountID, ruleID string) error { am.mux.Lock()