OpenAPI specification and API Adjusts (#356)

Introduced an OpenAPI specification.
Updated API handlers to use the specification types.

Added patch operation for rules and groups
and methods to the account manager.

HTTP PUT operations require id, fail if not provided.

Use snake_case for HTTP request and response body
This commit is contained in:
Maycon Santos 2022-06-14 10:32:54 +02:00 committed by GitHub
parent a454a1aa28
commit 503a116f7c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 2507 additions and 312 deletions

11
go.mod
View File

@ -17,8 +17,8 @@ require (
github.com/spf13/cobra v1.3.0 github.com/spf13/cobra v1.3.0
github.com/spf13/pflag v1.0.5 github.com/spf13/pflag v1.0.5
github.com/vishvananda/netlink v1.1.0 github.com/vishvananda/netlink v1.1.0
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 golang.org/x/crypto v0.0.0-20220513210258-46612604a0f9
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a
golang.zx2c4.com/wireguard v0.0.0-20211209221555-9c9e7e272434 golang.zx2c4.com/wireguard v0.0.0-20211209221555-9c9e7e272434
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20211215182854-7a385b3431de golang.zx2c4.com/wireguard/wgctrl v0.0.0-20211215182854-7a385b3431de
golang.zx2c4.com/wireguard/windows v0.5.1 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/client_model v0.2.0 // indirect
github.com/prometheus/common v0.33.0 // indirect github.com/prometheus/common v0.33.0 // indirect
github.com/prometheus/procfs v0.7.3 // 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/spf13/cast v1.5.0 // indirect
github.com/srwiley/oksvg v0.0.0-20200311192757-870daf9aa564 // indirect github.com/srwiley/oksvg v0.0.0-20200311192757-870daf9aa564 // indirect
github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9 // indirect github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9 // indirect
@ -91,15 +92,15 @@ require (
github.com/yuin/goldmark v1.4.1 // indirect github.com/yuin/goldmark v1.4.1 // indirect
golang.org/x/image v0.0.0-20200430140353-33d19683fad8 // 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/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/sync v0.0.0-20210220032951-036812b2e83c // indirect
golang.org/x/text v0.3.8-0.20211105212822-18b340fc7af2 // indirect golang.org/x/text v0.3.8-0.20211105212822-18b340fc7af2 // indirect
golang.org/x/tools v0.1.10 // 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/go118/netip v0.0.0-20211111135330-a4a02eeacf9d // indirect
golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 // indirect golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 // indirect
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa // 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.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 // indirect gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect

23
go.sum
View File

@ -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/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/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/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/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.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 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/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 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o=
github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M= 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.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/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= 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/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/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.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 h1:P2KMzcFwrPoSjkF1WLRPsp3UMLyql8L4v9hQpVeK5so=
github.com/rs/cors v1.8.0/go.mod h1:EBwu+T5AvHOcXwvZIkQFjUN6s8Czyqw12GL/Y0tUyRM= github.com/rs/cors v1.8.0/go.mod h1:EBwu+T5AvHOcXwvZIkQFjUN6s8Czyqw12GL/Y0tUyRM=
github.com/rs/xid v1.3.0 h1:6NjYksEUlhurdVehpc7S7dk6DAmcKv8V9gG0FsVN2U4= 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-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-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-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-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-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-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 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-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-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-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-20220513224357-95641704303c h1:nF9mHSvoKBLkQNQhJZNsc66z2UzAMUbLGjC95CF3pU0=
golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 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-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-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/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-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-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-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-20220513210249-45d2b4557a2a h1:N2T1jUrTQE9Re6TFF5PhvEHXHCguynGhKjWVsIUt5cY=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 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= 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-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-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-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-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 h1:9+v0G0naRhLPOJEeJOL6NuXTtAHHwmkyZlgQJ0XcQ8I=
golang.zx2c4.com/go118/netip v0.0.0-20211111135330-a4a02eeacf9d/go.mod h1:5yyfuiqVIJ7t+3MqrpTQ+QqRkMWiESiyDvPNvKYCecg= 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= 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-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-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-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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 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= gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=

View File

@ -7,7 +7,6 @@ import (
cacheStore "github.com/eko/gocache/v2/store" cacheStore "github.com/eko/gocache/v2/store"
"github.com/netbirdio/netbird/management/server/idp" "github.com/netbirdio/netbird/management/server/idp"
"github.com/netbirdio/netbird/management/server/jwtclaims" "github.com/netbirdio/netbird/management/server/jwtclaims"
"github.com/netbirdio/netbird/util"
gocache "github.com/patrickmn/go-cache" gocache "github.com/patrickmn/go-cache"
"github.com/rs/xid" "github.com/rs/xid"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@ -35,7 +34,7 @@ type AccountManager interface {
accountId string, accountId string,
keyName string, keyName string,
keyType SetupKeyType, keyType SetupKeyType,
expiresIn *util.Duration, expiresIn time.Duration,
) (*SetupKey, error) ) (*SetupKey, error)
RevokeSetupKey(accountId string, keyId string) (*SetupKey, error) RevokeSetupKey(accountId string, keyId string) (*SetupKey, error)
RenameSetupKey(accountId string, keyId string, newName string) (*SetupKey, error) RenameSetupKey(accountId string, keyId string, newName string) (*SetupKey, error)
@ -55,6 +54,7 @@ type AccountManager interface {
GetUsersFromAccount(accountId string) ([]*UserInfo, error) GetUsersFromAccount(accountId string) ([]*UserInfo, error)
GetGroup(accountId, groupID string) (*Group, error) GetGroup(accountId, groupID string) (*Group, error)
SaveGroup(accountId string, group *Group) error SaveGroup(accountId string, group *Group) error
UpdateGroup(accountID string, groupID string, operations []GroupUpdateOperation) (*Group, error)
DeleteGroup(accountId, groupID string) error DeleteGroup(accountId, groupID string) error
ListGroups(accountId string) ([]*Group, error) ListGroups(accountId string) ([]*Group, error)
GroupAddPeer(accountId, groupID, peerKey string) error GroupAddPeer(accountId, groupID, peerKey string) error
@ -62,6 +62,7 @@ type AccountManager interface {
GroupListPeers(accountId, groupID string) ([]*Peer, error) GroupListPeers(accountId, groupID string) ([]*Peer, error)
GetRule(accountId, ruleID string) (*Rule, error) GetRule(accountId, ruleID string) (*Rule, error)
SaveRule(accountID string, rule *Rule) error SaveRule(accountID string, rule *Rule) error
UpdateRule(accountID string, ruleID string, operations []RuleUpdateOperation) (*Rule, error)
DeleteRule(accountId, ruleID string) error DeleteRule(accountId, ruleID string) error
ListRules(accountId string) ([]*Rule, error) ListRules(accountId string) ([]*Rule, error)
} }
@ -222,14 +223,14 @@ func (am *DefaultAccountManager) AddSetupKey(
accountId string, accountId string,
keyName string, keyName string,
keyType SetupKeyType, keyType SetupKeyType,
expiresIn *util.Duration, expiresIn time.Duration,
) (*SetupKey, error) { ) (*SetupKey, error) {
am.mux.Lock() am.mux.Lock()
defer am.mux.Unlock() defer am.mux.Unlock()
keyDuration := DefaultSetupKeyDuration keyDuration := DefaultSetupKeyDuration
if expiresIn != nil { if expiresIn != 0 {
keyDuration = expiresIn.Duration keyDuration = expiresIn
} }
account, err := am.Store.GetAccount(accountId) account, err := am.Store.GetAccount(accountId)
@ -633,7 +634,9 @@ func addAllGroup(account *Account) {
defaultRule := &Rule{ defaultRule := &Rule{
ID: xid.New().String(), ID: xid.New().String(),
Name: "Default", Name: DefaultRuleName,
Description: DefaultRuleDescription,
Disabled: false,
Source: []string{allGroup.ID}, Source: []string{allGroup.ID},
Destination: []string{allGroup.ID}, Destination: []string{allGroup.ID},
} }
@ -687,3 +690,19 @@ func getAccountSetupKeyByKey(acc *Account, key string) *SetupKey {
} }
return nil 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
}

View File

@ -184,8 +184,8 @@ func (s *FileStore) DeletePeer(accountId string, peerKey string) (*Peer, error)
delete(s.PeerKeyId2SrcRulesId, peerKey) delete(s.PeerKeyId2SrcRulesId, peerKey)
// cleanup groups // cleanup groups
var peers []string
for _, g := range account.Groups { for _, g := range account.Groups {
var peers []string
for _, p := range g.Peers { for _, p := range g.Peers {
if p != peerKey { if p != peerKey {
peers = append(peers, p) peers = append(peers, p)

View File

@ -17,6 +17,26 @@ type Group struct {
Peers []string 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 { func (g *Group) Copy() *Group {
return &Group{ return &Group{
ID: g.ID, ID: g.ID,
@ -63,6 +83,56 @@ func (am *DefaultAccountManager) SaveGroup(accountID string, group *Group) error
return am.updateAccountPeers(account) 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 // DeleteGroup object of the peers
func (am *DefaultAccountManager) DeleteGroup(accountID, groupID string) error { func (am *DefaultAccountManager) DeleteGroup(accountID, groupID string) error {
am.mux.Lock() am.mux.Lock()

View File

@ -0,0 +1,5 @@
package: api
generate:
models: true
embedded-spec: false
output: types.gen.go

View File

@ -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"

View File

@ -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 /<field>
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 /<field>
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"

View File

@ -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 /<field>
Path GroupPatchOperationPath `json:"path"`
// Values to be applied
Value []string `json:"value"`
}
// Patch operation type
type GroupPatchOperationOp string
// Group field to update in form /<field>
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 /<field>
Path RulePatchOperationPath `json:"path"`
// Values to be applied
Value []string `json:"value"`
}
// Patch operation type
type RulePatchOperationOp string
// Rule field to update in form /<field>
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

View File

@ -3,6 +3,9 @@ package handler
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/netbirdio/netbird/management/server/http/api"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"net/http" "net/http"
"github.com/netbirdio/netbird/management/server" "github.com/netbirdio/netbird/management/server"
@ -13,26 +16,6 @@ import (
log "github.com/sirupsen/logrus" 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 // Groups is a handler that returns groups of the account
type Groups struct { type Groups struct {
jwtExtractor jwtclaims.ClaimsExtractor jwtExtractor jwtclaims.ClaimsExtractor
@ -50,14 +33,14 @@ func NewGroups(accountManager server.AccountManager, authAudience string) *Group
// GetAllGroupsHandler list for the account // GetAllGroupsHandler list for the account
func (h *Groups) GetAllGroupsHandler(w http.ResponseWriter, r *http.Request) { 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 { if err != nil {
log.Error(err) log.Error(err)
http.Redirect(w, r, "/", http.StatusInternalServerError) http.Redirect(w, r, "/", http.StatusInternalServerError)
return return
} }
var groups []*GroupResponse var groups []*api.Group
for _, g := range account.Groups { for _, g := range account.Groups {
groups = append(groups, toGroupResponse(account, g)) groups = append(groups, toGroupResponse(account, g))
} }
@ -65,31 +48,60 @@ func (h *Groups) GetAllGroupsHandler(w http.ResponseWriter, r *http.Request) {
writeJSONObject(w, groups) writeJSONObject(w, groups)
} }
func (h *Groups) CreateOrUpdateGroupHandler(w http.ResponseWriter, r *http.Request) { // UpdateGroupHandler handles update to a group identified by a given ID
account, err := h.getGroupAccount(r) func (h *Groups) UpdateGroupHandler(w http.ResponseWriter, r *http.Request) {
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil { if err != nil {
http.Redirect(w, r, "/", http.StatusInternalServerError) http.Redirect(w, r, "/", http.StatusInternalServerError)
return return
} }
var req GroupRequest 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 { if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
if r.Method == http.MethodPost { if *req.Name == "" {
req.ID = xid.New().String() http.Error(w, "group name shouldn't be empty", http.StatusUnprocessableEntity)
return
} }
group := server.Group{ group := server.Group{
ID: req.ID, ID: groupID,
Name: req.Name, Name: *req.Name,
Peers: req.Peers, Peers: peerIPsToKeys(account, req.Peers),
} }
if err := h.accountManager.SaveGroup(account.Id, &group); err != nil { 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 updating group %s under account %s %v", groupID, account.Id, err)
http.Redirect(w, r, "/", http.StatusInternalServerError) http.Redirect(w, r, "/", http.StatusInternalServerError)
return return
} }
@ -97,22 +109,183 @@ func (h *Groups) CreateOrUpdateGroupHandler(w http.ResponseWriter, r *http.Reque
writeJSONObject(w, toGroupResponse(account, &group)) 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 api.PostApiGroupsJSONRequestBody
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: xid.New().String(),
Name: req.Name,
Peers: peerIPsToKeys(account, req.Peers),
}
if err := h.accountManager.SaveGroup(account.Id, &group); err != nil {
log.Errorf("failed creating group \"%s\" under account %s %v", req.Name, account.Id, err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
}
writeJSONObject(w, toGroupResponse(account, &group))
}
// DeleteGroupHandler handles group deletion request
func (h *Groups) DeleteGroupHandler(w http.ResponseWriter, r *http.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 { if err != nil {
http.Redirect(w, r, "/", http.StatusInternalServerError) http.Redirect(w, r, "/", http.StatusInternalServerError)
return return
} }
aID := account.Id aID := account.Id
gID := mux.Vars(r)["id"] groupID := mux.Vars(r)["id"]
if len(gID) == 0 { if len(groupID) == 0 {
http.Error(w, "invalid group ID", http.StatusBadRequest) http.Error(w, "invalid group ID", http.StatusBadRequest)
return return
} }
if err := h.accountManager.DeleteGroup(aID, gID); err != nil { allGroup, err := account.GetGroupAll()
log.Errorf("failed delete group %s under account %s %v", gID, aID, err) 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) http.Redirect(w, r, "/", http.StatusInternalServerError)
return return
} }
@ -120,8 +293,9 @@ func (h *Groups) DeleteGroupHandler(w http.ResponseWriter, r *http.Request) {
writeJSONObject(w, "") writeJSONObject(w, "")
} }
// GetGroupHandler returns a group
func (h *Groups) GetGroupHandler(w http.ResponseWriter, r *http.Request) { 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 { if err != nil {
http.Redirect(w, r, "/", http.StatusInternalServerError) http.Redirect(w, r, "/", http.StatusInternalServerError)
return 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) { func peerIPsToKeys(account *server.Account, peerIPs *[]string) []string {
jwtClaims := h.jwtExtractor.ExtractClaimsFromRequestContext(r, h.authAudience) var mappedPeerKeys []string
if peerIPs == nil {
account, err := h.accountManager.GetAccountWithAuthorizationClaims(jwtClaims) return mappedPeerKeys
if err != nil {
return nil, fmt.Errorf("failed getting account of a user %s: %v", jwtClaims.UserId, err)
} }
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 { func toGroupResponse(account *server.Account, group *server.Group) *api.Group {
cache := make(map[string]GroupPeerResponse) cache := make(map[string]api.PeerMinimum)
gr := GroupResponse{ gr := api.Group{
ID: group.ID, Id: group.ID,
Name: group.Name, Name: group.Name,
PeersCount: len(group.Peers),
} }
for _, pid := range group.Peers { for _, pid := range group.Peers {
peerResp, ok := cache[pid] _, ok := cache[pid]
if !ok { if !ok {
peer, ok := account.Peers[pid] peer, ok := account.Peers[pid]
if !ok { if !ok {
continue continue
} }
peerResp = GroupPeerResponse{ peerResp := api.PeerMinimum{
Key: peer.Key, Id: peer.IP.String(),
Name: peer.Name, Name: peer.Name,
} }
cache[pid] = peerResp cache[pid] = peerResp
gr.Peers = append(gr.Peers, peerResp)
} }
gr.Peers = append(gr.Peers, peerResp)
} }
return &gr return &gr
} }

View File

@ -4,7 +4,9 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/netbirdio/netbird/management/server/http/api"
"io" "io"
"net"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strings" "strings"
@ -18,6 +20,11 @@ import (
"github.com/netbirdio/netbird/management/server/mock_server" "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 { func initGroupTestData(groups ...*server.Group) *Groups {
return &Groups{ return &Groups{
accountManager: &mock_server.MockAccountManager{ accountManager: &mock_server.MockAccountManager{
@ -36,10 +43,38 @@ func initGroupTestData(groups ...*server.Group) *Groups {
Name: "Group", Name: "Group",
}, nil }, 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) { GetAccountWithAuthorizationClaimsFunc: func(claims jwtclaims.AuthorizationClaims) (*server.Account, error) {
return &server.Account{ return &server.Account{
Id: claims.AccountId, Id: claims.AccountId,
Domain: "hotmail.com", 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 }, nil
}, },
}, },
@ -125,41 +160,114 @@ func TestGetGroup(t *testing.T) {
} }
} }
func TestSaveGroup(t *testing.T) { func TestWriteGroup(t *testing.T) {
tt := []struct { tt := []struct {
name string name string
expectedStatus int expectedStatus int
expectedBody bool expectedBody bool
expectedGroup *server.Group expectedGroup *api.Group
requestType string requestType string
requestPath string requestPath string
requestBody io.Reader requestBody io.Reader
}{ }{
{ {
name: "SaveGroup POST OK", name: "Write Group POST OK",
requestType: http.MethodPost, requestType: http.MethodPost,
requestPath: "/api/groups", requestPath: "/api/groups",
requestBody: bytes.NewBuffer( requestBody: bytes.NewBuffer(
[]byte(`{"Name":"Default POSTed Group"}`)), []byte(`{"Name":"Default POSTed Group"}`)),
expectedStatus: http.StatusOK, expectedStatus: http.StatusOK,
expectedBody: true, expectedBody: true,
expectedGroup: &server.Group{ expectedGroup: &api.Group{
ID: "id-was-set", Id: "id-was-set",
Name: "Default POSTed Group", Name: "Default POSTed Group",
}, },
}, },
{ {
name: "SaveGroup PUT OK", name: "Write Group POST Invalid Name",
requestType: http.MethodPut, requestType: http.MethodPost,
requestPath: "/api/groups", requestPath: "/api/groups",
requestBody: bytes.NewBuffer( 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, expectedStatus: http.StatusOK,
expectedGroup: &server.Group{ expectedGroup: &api.Group{
ID: "id-existed", Id: "id-existed",
Name: "Default POSTed Group", 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() p := initGroupTestData()
@ -170,7 +278,9 @@ func TestSaveGroup(t *testing.T) {
req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody) req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
router := mux.NewRouter() 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) router.ServeHTTP(recorder, req)
res := recorder.Result() res := recorder.Result()
@ -191,11 +301,10 @@ func TestSaveGroup(t *testing.T) {
return return
} }
got := &server.Group{} got := &api.Group{}
if err = json.Unmarshal(content, &got); err != nil { if err = json.Unmarshal(content, &got); err != nil {
t.Fatalf("Sent content is not in correct json format; %v", err) t.Fatalf("Sent content is not in correct json format; %v", err)
} }
assert.Equal(t, got, tc.expectedGroup) assert.Equal(t, got, tc.expectedGroup)
}) })
} }

View File

@ -3,13 +3,12 @@ package handler
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/netbirdio/netbird/management/server/jwtclaims"
"net/http"
"time"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/netbirdio/netbird/management/server" "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" log "github.com/sirupsen/logrus"
"net/http"
) )
//Peers is a handler that returns peers of the account //Peers is a handler that returns peers of the account
@ -19,21 +18,6 @@ type Peers struct {
jwtExtractor jwtclaims.ClaimsExtractor 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 { func NewPeers(accountManager server.AccountManager, authAudience string) *Peers {
return &Peers{ return &Peers{
accountManager: accountManager, 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) { func (h *Peers) updatePeer(account *server.Account, peer *server.Peer, w http.ResponseWriter, r *http.Request) {
req := &PeerRequest{} req := &api.PutApiPeersIdJSONBody{}
peerIp := peer.IP peerIp := peer.IP
err := json.NewDecoder(r.Body).Decode(&req) err := json.NewDecoder(r.Body).Decode(&req)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return 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 { 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) http.Redirect(w, r, "/", http.StatusInternalServerError)
return 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) { 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, "") 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) { 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 { if err != nil {
log.Error(err) log.Error(err)
http.Redirect(w, r, "/", http.StatusInternalServerError) 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) h.deletePeer(account.Id, peer, w, r)
return return
case http.MethodPut: case http.MethodPut:
h.updatePeer(account.Id, peer, w, r) h.updatePeer(account, peer, w, r)
return return
case http.MethodGet: case http.MethodGet:
writeJSONObject(w, toPeerResponse(peer)) writeJSONObject(w, toPeerResponse(peer, account))
return return
default: 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) { func (h *Peers) GetPeers(w http.ResponseWriter, r *http.Request) {
switch r.Method { switch r.Method {
case http.MethodGet: case http.MethodGet:
account, err := h.getPeerAccount(r) account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil { if err != nil {
log.Error(err) log.Error(err)
http.Redirect(w, r, "/", http.StatusInternalServerError) http.Redirect(w, r, "/", http.StatusInternalServerError)
return return
} }
respBody := []*PeerResponse{} respBody := []*api.Peer{}
for _, peer := range account.Peers { for _, peer := range account.Peers {
respBody = append(respBody, toPeerResponse(peer)) respBody = append(respBody, toPeerResponse(peer, account))
} }
writeJSONObject(w, respBody) writeJSONObject(w, respBody)
return return
@ -138,13 +111,35 @@ func (h *Peers) GetPeers(w http.ResponseWriter, r *http.Request) {
} }
} }
func toPeerResponse(peer *server.Peer) *PeerResponse { func toPeerResponse(peer *server.Peer, account *server.Account) *api.Peer {
return &PeerResponse{ 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, Name: peer.Name,
IP: peer.IP.String(), Ip: peer.IP.String(),
Connected: peer.Status.Connected, Connected: peer.Status.Connected,
LastSeen: peer.Status.LastSeen, 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, Version: peer.Meta.WtVersion,
Groups: groupsInfo,
} }
} }

View File

@ -2,6 +2,7 @@ package handler
import ( import (
"encoding/json" "encoding/json"
"github.com/netbirdio/netbird/management/server/http/api"
"io" "io"
"net" "net"
"net/http" "net/http"
@ -98,7 +99,7 @@ func TestGetPeers(t *testing.T) {
t.Fatalf("I don't know what I expected; %v", err) t.Fatalf("I don't know what I expected; %v", err)
} }
respBody := []*PeerResponse{} respBody := []*api.Peer{}
err = json.Unmarshal(content, &respBody) err = json.Unmarshal(content, &respBody)
if err != nil { if err != nil {
t.Fatalf("Sent content is not in correct json format; %v", err) t.Fatalf("Sent content is not in correct json format; %v", err)
@ -107,8 +108,8 @@ func TestGetPeers(t *testing.T) {
got := respBody[0] got := respBody[0]
assert.Equal(t, got.Name, peer.Name) assert.Equal(t, got.Name, peer.Name)
assert.Equal(t, got.Version, peer.Meta.WtVersion) assert.Equal(t, got.Version, peer.Meta.WtVersion)
assert.Equal(t, got.IP, peer.IP.String()) assert.Equal(t, got.Ip, peer.IP.String())
assert.Equal(t, got.OS, "OS core") assert.Equal(t, got.Os, "OS core")
}) })
} }
} }

View File

@ -3,43 +3,17 @@ package handler
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "github.com/gorilla/mux"
"github.com/netbirdio/netbird/management/server" "github.com/netbirdio/netbird/management/server"
"github.com/netbirdio/netbird/management/server/http/api"
"github.com/netbirdio/netbird/management/server/jwtclaims" "github.com/netbirdio/netbird/management/server/jwtclaims"
"github.com/rs/xid" "github.com/rs/xid"
"github.com/gorilla/mux"
log "github.com/sirupsen/logrus" 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 // Rules is a handler that returns rules of the account
type Rules struct { type Rules struct {
jwtExtractor jwtclaims.ClaimsExtractor jwtExtractor jwtclaims.ClaimsExtractor
@ -57,14 +31,14 @@ func NewRules(accountManager server.AccountManager, authAudience string) *Rules
// GetAllRulesHandler list for the account // GetAllRulesHandler list for the account
func (h *Rules) GetAllRulesHandler(w http.ResponseWriter, r *http.Request) { 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 { if err != nil {
log.Error(err) log.Error(err)
http.Redirect(w, r, "/", http.StatusInternalServerError) http.Redirect(w, r, "/", http.StatusInternalServerError)
return return
} }
rules := []*RuleResponse{} rules := []*api.Rule{}
for _, r := range account.Rules { for _, r := range account.Rules {
rules = append(rules, toRuleResponse(account, r)) rules = append(rules, toRuleResponse(account, r))
} }
@ -72,32 +46,59 @@ func (h *Rules) GetAllRulesHandler(w http.ResponseWriter, r *http.Request) {
writeJSONObject(w, rules) writeJSONObject(w, rules)
} }
func (h *Rules) CreateOrUpdateRuleHandler(w http.ResponseWriter, r *http.Request) { // UpdateRuleHandler handles update to a rule identified by a given ID
account, err := h.getRuleAccount(r) func (h *Rules) UpdateRuleHandler(w http.ResponseWriter, r *http.Request) {
account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil { if err != nil {
http.Redirect(w, r, "/", http.StatusInternalServerError) http.Redirect(w, r, "/", http.StatusInternalServerError)
return return
} }
var req RuleRequest 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 { if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
if r.Method == http.MethodPost { if req.Name == "" {
req.ID = xid.New().String() 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{ rule := server.Rule{
ID: req.ID, ID: ruleID,
Name: req.Name, Name: req.Name,
Source: req.Source, Source: reqSources,
Destination: req.Destination, Destination: reqDestinations,
Disabled: req.Disabled,
Description: req.Description,
} }
switch req.Flow { switch req.Flow {
case FlowBidirectString: case server.TrafficFlowBidirectString:
rule.Flow = server.TrafficFlowBidirect rule.Flow = server.TrafficFlowBidirect
default: default:
http.Error(w, "unknown flow type", http.StatusBadRequest) http.Error(w, "unknown flow type", http.StatusBadRequest)
@ -105,16 +106,233 @@ func (h *Rules) CreateOrUpdateRuleHandler(w http.ResponseWriter, r *http.Request
} }
if err := h.accountManager.SaveRule(account.Id, &rule); err != nil { 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 updating rule \"%s\" under account %s %v", ruleID, account.Id, err)
http.Redirect(w, r, "/", http.StatusInternalServerError) http.Redirect(w, r, "/", http.StatusInternalServerError)
return return
} }
writeJSONObject(w, &req) 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 api.PostApiRulesJSONRequestBody
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: xid.New().String(),
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 creating rule \"%s\" under account %s %v", req.Name, account.Id, err)
http.Redirect(w, r, "/", http.StatusInternalServerError)
return
}
resp := toRuleResponse(account, &rule)
writeJSONObject(w, &resp)
}
// DeleteRuleHandler handles rule deletion request
func (h *Rules) DeleteRuleHandler(w http.ResponseWriter, r *http.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 { if err != nil {
http.Redirect(w, r, "/", http.StatusInternalServerError) http.Redirect(w, r, "/", http.StatusInternalServerError)
return return
@ -136,8 +354,9 @@ func (h *Rules) DeleteRuleHandler(w http.ResponseWriter, r *http.Request) {
writeJSONObject(w, "") writeJSONObject(w, "")
} }
// GetRuleHandler handles a group Get request identified by ID
func (h *Rules) GetRuleHandler(w http.ResponseWriter, r *http.Request) { 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 { if err != nil {
http.Redirect(w, r, "/", http.StatusInternalServerError) http.Redirect(w, r, "/", http.StatusInternalServerError)
return 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) { func toRuleResponse(account *server.Account, rule *server.Rule) *api.Rule {
jwtClaims := h.jwtExtractor.ExtractClaimsFromRequestContext(r, h.authAudience) cache := make(map[string]api.GroupMinimum)
gr := api.Rule{
account, err := h.accountManager.GetAccountWithAuthorizationClaims(jwtClaims) Id: rule.ID,
if err != nil { Name: rule.Name,
return nil, fmt.Errorf("failed getting account of a user %s: %v", jwtClaims.UserId, err) Description: rule.Description,
} Disabled: rule.Disabled,
return account, nil
}
func toRuleResponse(account *server.Account, rule *server.Rule) *RuleResponse {
gr := RuleResponse{
ID: rule.ID,
Name: rule.Name,
} }
switch rule.Flow { switch rule.Flow {
case server.TrafficFlowBidirect: case server.TrafficFlowBidirect:
gr.Flow = FlowBidirectString gr.Flow = server.TrafficFlowBidirectString
default: default:
gr.Flow = "unknown" gr.Flow = "unknown"
} }
for _, gid := range rule.Source { for _, gid := range rule.Source {
_, ok := cache[gid]
if ok {
continue
}
if group, ok := account.Groups[gid]; ok { if group, ok := account.Groups[gid]; ok {
gr.Source = append(gr.Source, RuleGroupResponse{ minimum := api.GroupMinimum{
ID: group.ID, Id: group.ID,
Name: group.Name, Name: group.Name,
PeersCount: len(group.Peers), PeersCount: len(group.Peers),
}) }
gr.Sources = append(gr.Sources, minimum)
cache[gid] = minimum
} }
} }
for _, gid := range rule.Destination { 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 { if group, ok := account.Groups[gid]; ok {
gr.Destination = append(gr.Destination, RuleGroupResponse{ minimum := api.GroupMinimum{
ID: group.ID, Id: group.ID,
Name: group.Name, Name: group.Name,
PeersCount: len(group.Peers), PeersCount: len(group.Peers),
}) }
gr.Destinations = append(gr.Destinations, minimum)
cache[gid] = minimum
} }
} }

View File

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/netbirdio/netbird/management/server/http/api"
"io" "io"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
@ -39,10 +40,41 @@ func initRulesTestData(rules ...*server.Rule) *Rules {
Flow: server.TrafficFlowBidirect, Flow: server.TrafficFlowBidirect,
}, nil }, 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) { GetAccountWithAuthorizationClaimsFunc: func(claims jwtclaims.AuthorizationClaims) (*server.Account, error) {
return &server.Account{ return &server.Account{
Id: claims.AccountId, Id: claims.AccountId,
Domain: "hotmail.com", 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 }, nil
}, },
}, },
@ -117,52 +149,118 @@ func TestRulesGetRule(t *testing.T) {
t.Fatalf("I don't know what I expected; %v", err) 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 { if err = json.Unmarshal(content, &got); err != nil {
t.Fatalf("Sent content is not in correct json format; %v", err) 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) assert.Equal(t, got.Name, rule.Name)
}) })
} }
} }
func TestRulesSaveRule(t *testing.T) { func TestRulesWriteRule(t *testing.T) {
tt := []struct { tt := []struct {
name string name string
expectedStatus int expectedStatus int
expectedBody bool expectedBody bool
expectedRule *server.Rule expectedRule *api.Rule
requestType string requestType string
requestPath string requestPath string
requestBody io.Reader requestBody io.Reader
}{ }{
{ {
name: "SaveRule POST OK", name: "WriteRule POST OK",
requestType: http.MethodPost, requestType: http.MethodPost,
requestPath: "/api/rules", requestPath: "/api/rules",
requestBody: bytes.NewBuffer( requestBody: bytes.NewBuffer(
[]byte(`{"Name":"Default POSTed Rule","Flow":"bidirect"}`)), []byte(`{"Name":"Default POSTed Rule","Flow":"bidirect"}`)),
expectedStatus: http.StatusOK, expectedStatus: http.StatusOK,
expectedBody: true, expectedBody: true,
expectedRule: &server.Rule{ expectedRule: &api.Rule{
ID: "id-was-set", Id: "id-was-set",
Name: "Default POSTed Rule", Name: "Default POSTed Rule",
Flow: server.TrafficFlowBidirect, Flow: server.TrafficFlowBidirectString,
}, },
}, },
{ {
name: "SaveRule PUT OK", name: "WriteRule POST Invalid Name",
requestType: http.MethodPut, requestType: http.MethodPost,
requestPath: "/api/rules", requestPath: "/api/rules",
requestBody: bytes.NewBuffer( 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, expectedStatus: http.StatusOK,
expectedRule: &server.Rule{ expectedBody: true,
ID: "id-existed", expectedRule: &api.Rule{
Id: "id-existed",
Name: "Default POSTed Rule", 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) req := httptest.NewRequest(tc.requestType, tc.requestPath, tc.requestBody)
router := mux.NewRouter() 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) router.ServeHTTP(recorder, req)
res := recorder.Result() res := recorder.Result()
@ -196,16 +296,13 @@ func TestRulesSaveRule(t *testing.T) {
return return
} }
got := &RuleRequest{} got := &api.Rule{}
if err = json.Unmarshal(content, &got); err != nil { if err = json.Unmarshal(content, &got); err != nil {
t.Fatalf("Sent content is not in correct json format; %v", err) t.Fatalf("Sent content is not in correct json format; %v", err)
} }
if tc.requestType != http.MethodPost { assert.Equal(t, got, tc.expectedRule)
assert.Equal(t, got.ID, tc.expectedRule.ID)
}
assert.Equal(t, got.Name, tc.expectedRule.Name)
assert.Equal(t, got.Flow, "bidirect")
}) })
} }
} }

View File

@ -2,56 +2,34 @@ package handler
import ( import (
"encoding/json" "encoding/json"
"fmt"
"github.com/netbirdio/netbird/management/server/jwtclaims"
"net/http"
"time"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/netbirdio/netbird/management/server" "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" log "github.com/sirupsen/logrus"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
"net/http"
"time"
) )
// SetupKeys is a handler that returns a list of setup keys of the account // SetupKeys is a handler that returns a list of setup keys of the account
type SetupKeys struct { type SetupKeys struct {
accountManager server.AccountManager accountManager server.AccountManager
jwtExtractor jwtclaims.ClaimsExtractor
authAudience string 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 { func NewSetupKeysHandler(accountManager server.AccountManager, authAudience string) *SetupKeys {
return &SetupKeys{ return &SetupKeys{
accountManager: accountManager, accountManager: accountManager,
authAudience: authAudience, authAudience: authAudience,
jwtExtractor: *jwtclaims.NewClaimsExtractor(nil),
} }
} }
func (h *SetupKeys) updateKey(accountId string, keyId string, w http.ResponseWriter, r *http.Request) { 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) err := json.NewDecoder(r.Body).Decode(&req)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) 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) { func (h *SetupKeys) createKey(accountId string, w http.ResponseWriter, r *http.Request) {
req := &SetupKeyRequest{} req := &api.PostApiSetupKeysJSONRequestBody{}
err := json.NewDecoder(r.Body).Decode(&req) err := json.NewDecoder(r.Body).Decode(&req)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return 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) http.Error(w, "unknown setup key type "+string(req.Type), http.StatusBadRequest)
return 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 { if err != nil {
errStatus, ok := status.FromError(err) errStatus, ok := status.FromError(err)
if ok && errStatus.Code() == codes.NotFound { 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) 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) { 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 { if err != nil {
log.Error(err) log.Error(err)
http.Redirect(w, r, "/", http.StatusInternalServerError) 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) { 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 { if err != nil {
log.Error(err) log.Error(err)
http.Redirect(w, r, "/", http.StatusInternalServerError) http.Redirect(w, r, "/", http.StatusInternalServerError)
@ -178,7 +153,7 @@ func (h *SetupKeys) GetKeys(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200) w.WriteHeader(200)
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
respBody := []*SetupKeyResponse{} respBody := []*api.SetupKey{}
for _, key := range account.SetupKeys { for _, key := range account.SetupKeys {
respBody = append(respBody, toResponseBody(key)) 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 var state string
if key.IsExpired() { if key.IsExpired() {
state = "expired" state = "expired"
@ -215,12 +190,12 @@ func toResponseBody(key *server.SetupKey) *SetupKeyResponse {
} else { } else {
state = "valid" state = "valid"
} }
return &SetupKeyResponse{ return &api.SetupKey{
Id: key.Id, Id: key.Id,
Key: key.Key, Key: key.Key,
Name: key.Name, Name: key.Name,
Expires: key.ExpiresAt, Expires: key.ExpiresAt,
Type: key.Type, Type: string(key.Type),
Valid: key.IsValid(), Valid: key.IsValid(),
Revoked: key.Revoked, Revoked: key.Revoked,
UsedTimes: key.UsedTimes, UsedTimes: key.UsedTimes,

View File

@ -1,7 +1,7 @@
package handler package handler
import ( import (
"fmt" "github.com/netbirdio/netbird/management/server/http/api"
"net/http" "net/http"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@ -16,13 +16,6 @@ type UserHandler struct {
jwtExtractor jwtclaims.ClaimsExtractor 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 { func NewUserHandler(accountManager server.AccountManager, authAudience string) *UserHandler {
return &UserHandler{ return &UserHandler{
accountManager: accountManager, 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. // 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. // 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 { if r.Method != http.MethodGet {
http.Error(w, "", http.StatusBadRequest) http.Error(w, "", http.StatusBadRequest)
} }
account, err := u.getAccountId(r) account, err := getJWTAccount(h.accountManager, h.jwtExtractor, h.authAudience, r)
if err != nil { if err != nil {
log.Error(err) log.Error(err)
} }
data, err := u.accountManager.GetUsersFromAccount(account.Id) data, err := h.accountManager.GetUsersFromAccount(account.Id)
if err != nil { if err != nil {
log.Error(err) log.Error(err)
http.Redirect(w, r, "/", http.StatusInternalServerError) http.Redirect(w, r, "/", http.StatusInternalServerError)
return return
} }
users := []*UserResponse{} users := []*api.User{}
for _, r := range data { for _, r := range data {
users = append(users, toUserResponse(r)) users = append(users, toUserResponse(r))
} }
@ -69,9 +51,9 @@ func (u *UserHandler) GetUsers(w http.ResponseWriter, r *http.Request) {
writeJSONObject(w, users) writeJSONObject(w, users)
} }
func toUserResponse(user *server.UserInfo) *UserResponse { func toUserResponse(user *server.UserInfo) *api.User {
return &UserResponse{ return &api.User{
ID: user.ID, Id: user.ID,
Name: user.Name, Name: user.Name,
Email: user.Email, Email: user.Email,
Role: user.Role, Role: user.Role,

View File

@ -3,6 +3,9 @@ package handler
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"github.com/netbirdio/netbird/management/server"
"github.com/netbirdio/netbird/management/server/jwtclaims"
"net/http" "net/http"
"time" "time"
) )
@ -47,3 +50,17 @@ func (d *Duration) UnmarshalJSON(b []byte) error {
return errors.New("invalid duration") 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
}

View File

@ -103,13 +103,12 @@ func (s *Server) Start() error {
rulesHandler := handler.NewRules(s.accountManager, s.config.AuthAudience) rulesHandler := handler.NewRules(s.accountManager, s.config.AuthAudience)
peersHandler := handler.NewPeers(s.accountManager, s.config.AuthAudience) peersHandler := handler.NewPeers(s.accountManager, s.config.AuthAudience)
keysHandler := handler.NewSetupKeysHandler(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", peersHandler.GetPeers).Methods("GET", "OPTIONS")
r.HandleFunc("/api/peers/{id}", peersHandler.HandlePeer). r.HandleFunc("/api/peers/{id}", peersHandler.HandlePeer).
Methods("GET", "PUT", "DELETE", "OPTIONS") 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/users", userHandler.GetUsers).Methods("GET", "OPTIONS")
r.HandleFunc("/api/setup-keys", keysHandler.GetKeys).Methods("GET", "POST", "OPTIONS") r.HandleFunc("/api/setup-keys", keysHandler.GetKeys).Methods("GET", "POST", "OPTIONS")
r.HandleFunc("/api/setup-keys/{id}", keysHandler.HandleKey).Methods("GET", "PUT", "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") Methods("GET", "PUT", "DELETE", "OPTIONS")
r.HandleFunc("/api/rules", rulesHandler.GetAllRulesHandler).Methods("GET", "OPTIONS") r.HandleFunc("/api/rules", rulesHandler.GetAllRulesHandler).Methods("GET", "OPTIONS")
r.HandleFunc("/api/rules", rulesHandler.CreateOrUpdateRuleHandler). r.HandleFunc("/api/rules", rulesHandler.CreateRuleHandler).Methods("POST", "OPTIONS")
Methods("POST", "PUT", "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.GetRuleHandler).Methods("GET", "OPTIONS")
r.HandleFunc("/api/rules/{id}", rulesHandler.DeleteRuleHandler).Methods("DELETE", "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.GetAllGroupsHandler).Methods("GET", "OPTIONS")
r.HandleFunc("/api/groups", groupsHandler.CreateOrUpdateGroupHandler). r.HandleFunc("/api/groups", groupsHandler.CreateGroupHandler).Methods("POST", "OPTIONS")
Methods("POST", "PUT", "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.GetGroupHandler).Methods("GET", "OPTIONS")
r.HandleFunc("/api/groups/{id}", groupsHandler.DeleteGroupHandler).Methods("DELETE", "OPTIONS") r.HandleFunc("/api/groups/{id}", groupsHandler.DeleteGroupHandler).Methods("DELETE", "OPTIONS")
http.Handle("/", r) http.Handle("/", r)

View File

@ -36,6 +36,9 @@ func NewClaimsExtractor(e ExtractClaims) *ClaimsExtractor {
// ExtractClaimsFromRequestContext extracts claims from the request context previously filled by the JWT token (after auth) // ExtractClaimsFromRequestContext extracts claims from the request context previously filled by the JWT token (after auth)
func ExtractClaimsFromRequestContext(r *http.Request, authAudience string) AuthorizationClaims { func ExtractClaimsFromRequestContext(r *http.Request, authAudience string) AuthorizationClaims {
if r.Context().Value(TokenUserProperty) == nil {
return AuthorizationClaims{}
}
token := r.Context().Value(TokenUserProperty).(*jwt.Token) token := r.Context().Value(TokenUserProperty).(*jwt.Token)
return ExtractClaimsWithToken(token, authAudience) return ExtractClaimsWithToken(token, authAudience)
} }

View File

@ -3,15 +3,15 @@ package mock_server
import ( import (
"github.com/netbirdio/netbird/management/server" "github.com/netbirdio/netbird/management/server"
"github.com/netbirdio/netbird/management/server/jwtclaims" "github.com/netbirdio/netbird/management/server/jwtclaims"
"github.com/netbirdio/netbird/util"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
"time"
) )
type MockAccountManager struct { type MockAccountManager struct {
GetOrCreateAccountByUserFunc func(userId, domain string) (*server.Account, error) GetOrCreateAccountByUserFunc func(userId, domain string) (*server.Account, error)
GetAccountByUserFunc func(userId 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) RevokeSetupKeyFunc func(accountId string, keyId string) (*server.SetupKey, error)
RenameSetupKeyFunc func(accountId string, keyId string, newName string) (*server.SetupKey, error) RenameSetupKeyFunc func(accountId string, keyId string, newName string) (*server.SetupKey, error)
GetAccountByIdFunc func(accountId string) (*server.Account, 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) AddPeerFunc func(setupKey string, userId string, peer *server.Peer) (*server.Peer, error)
GetGroupFunc func(accountID, groupID string) (*server.Group, error) GetGroupFunc func(accountID, groupID string) (*server.Group, error)
SaveGroupFunc func(accountID string, group *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 DeleteGroupFunc func(accountID, groupID string) error
ListGroupsFunc func(accountID string) ([]*server.Group, error) ListGroupsFunc func(accountID string) ([]*server.Group, error)
GroupAddPeerFunc func(accountID, groupID, peerKey string) error GroupAddPeerFunc func(accountID, groupID, peerKey string) error
@ -35,12 +36,14 @@ type MockAccountManager struct {
GroupListPeersFunc func(accountID, groupID string) ([]*server.Peer, error) GroupListPeersFunc func(accountID, groupID string) ([]*server.Peer, error)
GetRuleFunc func(accountID, ruleID string) (*server.Rule, error) GetRuleFunc func(accountID, ruleID string) (*server.Rule, error)
SaveRuleFunc func(accountID string, rule *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 DeleteRuleFunc func(accountID, ruleID string) error
ListRulesFunc func(accountID string) ([]*server.Rule, error) ListRulesFunc func(accountID string) ([]*server.Rule, error)
GetUsersFromAccountFunc func(accountID string) ([]*server.UserInfo, error) GetUsersFromAccountFunc func(accountID string) ([]*server.UserInfo, error)
UpdatePeerMetaFunc func(peerKey string, meta server.PeerSystemMeta) 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) { func (am *MockAccountManager) GetUsersFromAccount(accountID string) ([]*server.UserInfo, error) {
if am.GetUsersFromAccountFunc != nil { if am.GetUsersFromAccountFunc != nil {
return am.GetUsersFromAccountFunc(accountID) 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") return nil, status.Errorf(codes.Unimplemented, "method GetUsersFromAccount not implemented")
} }
// GetOrCreateAccountByUser mock implementation of GetOrCreateAccountByUser from server.AccountManager interface
func (am *MockAccountManager) GetOrCreateAccountByUser( func (am *MockAccountManager) GetOrCreateAccountByUser(
userId, domain string, userId, domain string,
) (*server.Account, error) { ) (*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) { func (am *MockAccountManager) GetAccountByUser(userId string) (*server.Account, error) {
if am.GetAccountByUserFunc != nil { if am.GetAccountByUserFunc != nil {
return am.GetAccountByUserFunc(userId) 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") return nil, status.Errorf(codes.Unimplemented, "method GetAccountByUser not implemented")
} }
// AddSetupKey mock implementation of AddSetupKey from server.AccountManager interface
func (am *MockAccountManager) AddSetupKey( func (am *MockAccountManager) AddSetupKey(
accountId string, accountId string,
keyName string, keyName string,
keyType server.SetupKeyType, keyType server.SetupKeyType,
expiresIn *util.Duration, expiresIn time.Duration,
) (*server.SetupKey, error) { ) (*server.SetupKey, error) {
if am.AddSetupKeyFunc != nil { if am.AddSetupKeyFunc != nil {
return am.AddSetupKeyFunc(accountId, keyName, keyType, expiresIn) 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") return nil, status.Errorf(codes.Unimplemented, "method AddSetupKey not implemented")
} }
// RevokeSetupKey mock implementation of RevokeSetupKey from server.AccountManager interface
func (am *MockAccountManager) RevokeSetupKey( func (am *MockAccountManager) RevokeSetupKey(
accountId string, accountId string,
keyId string, keyId string,
@ -89,6 +96,7 @@ func (am *MockAccountManager) RevokeSetupKey(
return nil, status.Errorf(codes.Unimplemented, "method RevokeSetupKey not implemented") return nil, status.Errorf(codes.Unimplemented, "method RevokeSetupKey not implemented")
} }
// RenameSetupKey mock implementation of RenameSetupKey from server.AccountManager interface
func (am *MockAccountManager) RenameSetupKey( func (am *MockAccountManager) RenameSetupKey(
accountId string, accountId string,
keyId string, keyId string,
@ -100,6 +108,7 @@ func (am *MockAccountManager) RenameSetupKey(
return nil, status.Errorf(codes.Unimplemented, "method RenameSetupKey not implemented") 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) { func (am *MockAccountManager) GetAccountById(accountId string) (*server.Account, error) {
if am.GetAccountByIdFunc != nil { if am.GetAccountByIdFunc != nil {
return am.GetAccountByIdFunc(accountId) 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") return nil, status.Errorf(codes.Unimplemented, "method GetAccountById not implemented")
} }
// GetAccountByUserOrAccountId mock implementation of GetAccountByUserOrAccountId from server.AccountManager interface
func (am *MockAccountManager) GetAccountByUserOrAccountId( func (am *MockAccountManager) GetAccountByUserOrAccountId(
userId, accountId, domain string, userId, accountId, domain string,
) (*server.Account, error) { ) (*server.Account, error) {
@ -119,6 +129,7 @@ func (am *MockAccountManager) GetAccountByUserOrAccountId(
) )
} }
// GetAccountWithAuthorizationClaims mock implementation of GetAccountWithAuthorizationClaims from server.AccountManager interface
func (am *MockAccountManager) GetAccountWithAuthorizationClaims( func (am *MockAccountManager) GetAccountWithAuthorizationClaims(
claims jwtclaims.AuthorizationClaims, claims jwtclaims.AuthorizationClaims,
) (*server.Account, error) { ) (*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) { func (am *MockAccountManager) AccountExists(accountId string) (*bool, error) {
if am.AccountExistsFunc != nil { if am.AccountExistsFunc != nil {
return am.AccountExistsFunc(accountId) 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") 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) { func (am *MockAccountManager) GetPeer(peerKey string) (*server.Peer, error) {
if am.GetPeerFunc != nil { if am.GetPeerFunc != nil {
return am.GetPeerFunc(peerKey) 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") 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 { func (am *MockAccountManager) MarkPeerConnected(peerKey string, connected bool) error {
if am.MarkPeerConnectedFunc != nil { if am.MarkPeerConnectedFunc != nil {
return am.MarkPeerConnectedFunc(peerKey, connected) 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") return status.Errorf(codes.Unimplemented, "method MarkPeerConnected not implemented")
} }
// RenamePeer mock implementation of RenamePeer from server.AccountManager interface
func (am *MockAccountManager) RenamePeer( func (am *MockAccountManager) RenamePeer(
accountId string, accountId string,
peerKey string, peerKey string,
@ -163,6 +178,7 @@ func (am *MockAccountManager) RenamePeer(
return nil, status.Errorf(codes.Unimplemented, "method RenamePeer not implemented") 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) { func (am *MockAccountManager) DeletePeer(accountId string, peerKey string) (*server.Peer, error) {
if am.DeletePeerFunc != nil { if am.DeletePeerFunc != nil {
return am.DeletePeerFunc(accountId, peerKey) 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") 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) { func (am *MockAccountManager) GetPeerByIP(accountId string, peerIP string) (*server.Peer, error) {
if am.GetPeerByIPFunc != nil { if am.GetPeerByIPFunc != nil {
return am.GetPeerByIPFunc(accountId, peerIP) 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") 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) { func (am *MockAccountManager) GetNetworkMap(peerKey string) (*server.NetworkMap, error) {
if am.GetNetworkMapFunc != nil { if am.GetNetworkMapFunc != nil {
return am.GetNetworkMapFunc(peerKey) 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") return nil, status.Errorf(codes.Unimplemented, "method GetNetworkMap not implemented")
} }
// AddPeer mock implementation of AddPeer from server.AccountManager interface
func (am *MockAccountManager) AddPeer( func (am *MockAccountManager) AddPeer(
setupKey string, setupKey string,
userId string, userId string,
@ -195,6 +214,7 @@ func (am *MockAccountManager) AddPeer(
return nil, status.Errorf(codes.Unimplemented, "method AddPeer not implemented") 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) { func (am *MockAccountManager) GetGroup(accountID, groupID string) (*server.Group, error) {
if am.GetGroupFunc != nil { if am.GetGroupFunc != nil {
return am.GetGroupFunc(accountID, groupID) 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") 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 { func (am *MockAccountManager) SaveGroup(accountID string, group *server.Group) error {
if am.SaveGroupFunc != nil { if am.SaveGroupFunc != nil {
return am.SaveGroupFunc(accountID, group) 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") 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 { func (am *MockAccountManager) DeleteGroup(accountID, groupID string) error {
if am.DeleteGroupFunc != nil { if am.DeleteGroupFunc != nil {
return am.DeleteGroupFunc(accountID, groupID) 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") 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) { func (am *MockAccountManager) ListGroups(accountID string) ([]*server.Group, error) {
if am.ListGroupsFunc != nil { if am.ListGroupsFunc != nil {
return am.ListGroupsFunc(accountID) 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") 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 { func (am *MockAccountManager) GroupAddPeer(accountID, groupID, peerKey string) error {
if am.GroupAddPeerFunc != nil { if am.GroupAddPeerFunc != nil {
return am.GroupAddPeerFunc(accountID, groupID, peerKey) 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") 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 { func (am *MockAccountManager) GroupDeletePeer(accountID, groupID, peerKey string) error {
if am.GroupDeletePeerFunc != nil { if am.GroupDeletePeerFunc != nil {
return am.GroupDeletePeerFunc(accountID, groupID, peerKey) 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") 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) { func (am *MockAccountManager) GroupListPeers(accountID, groupID string) ([]*server.Peer, error) {
if am.GroupListPeersFunc != nil { if am.GroupListPeersFunc != nil {
return am.GroupListPeersFunc(accountID, groupID) 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") 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) { func (am *MockAccountManager) GetRule(accountID, ruleID string) (*server.Rule, error) {
if am.GetRuleFunc != nil { if am.GetRuleFunc != nil {
return am.GetRuleFunc(accountID, ruleID) 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") 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 { func (am *MockAccountManager) SaveRule(accountID string, rule *server.Rule) error {
if am.SaveRuleFunc != nil { if am.SaveRuleFunc != nil {
return am.SaveRuleFunc(accountID, rule) 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") 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 { func (am *MockAccountManager) DeleteRule(accountID, ruleID string) error {
if am.DeleteRuleFunc != nil { if am.DeleteRuleFunc != nil {
return am.DeleteRuleFunc(accountID, ruleID) 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") 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) { func (am *MockAccountManager) ListRules(accountID string) ([]*server.Rule, error) {
if am.ListRulesFunc != nil { if am.ListRulesFunc != nil {
return am.ListRulesFunc(accountID) 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") 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 { func (am *MockAccountManager) UpdatePeerMeta(peerKey string, meta server.PeerSystemMeta) error {
if am.UpdatePeerMetaFunc != nil { if am.UpdatePeerMetaFunc != nil {
return am.UpdatePeerMetaFunc(peerKey, meta) 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") 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) { func (am *MockAccountManager) IsUserAdmin(claims jwtclaims.AuthorizationClaims) (bool, error) {
if am.IsUserAdminFunc != nil { if am.IsUserAdminFunc != nil {
return am.IsUserAdminFunc(claims) return am.IsUserAdminFunc(claims)

View File

@ -360,6 +360,9 @@ func (am *DefaultAccountManager) getPeersByACL(account *Account, peerKey string)
groups := map[string]*Group{} groups := map[string]*Group{}
for _, r := range srcRules { for _, r := range srcRules {
if r.Disabled {
continue
}
if r.Flow == TrafficFlowBidirect { if r.Flow == TrafficFlowBidirect {
for _, gid := range r.Destination { for _, gid := range r.Destination {
if group, ok := account.Groups[gid]; ok { if group, ok := account.Groups[gid]; ok {
@ -370,6 +373,9 @@ func (am *DefaultAccountManager) getPeersByACL(account *Account, peerKey string)
} }
for _, r := range dstRules { for _, r := range dstRules {
if r.Disabled {
continue
}
if r.Flow == TrafficFlowBidirect { if r.Flow == TrafficFlowBidirect {
for _, gid := range r.Source { for _, gid := range r.Source {
if group, ok := account.Groups[gid]; ok { if group, ok := account.Groups[gid]; ok {

View File

@ -220,4 +220,36 @@ func TestAccountManager_GetNetworkMapWithRule(t *testing.T) {
networkMap2.Peers[0].Key, 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))
}
} }

View File

@ -3,6 +3,7 @@ package server
import ( import (
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
"strings"
) )
// TrafficFlowType defines allowed direction of the traffic in the rule // TrafficFlowType defines allowed direction of the traffic in the rule
@ -11,6 +12,12 @@ type TrafficFlowType int
const ( const (
// TrafficFlowBidirect allows traffic to both direction // TrafficFlowBidirect allows traffic to both direction
TrafficFlowBidirect TrafficFlowType = iota 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 // Rule of ACL for groups
@ -21,6 +28,12 @@ type Rule struct {
// Name of the rule visible in the UI // Name of the rule visible in the UI
Name string 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 list of groups IDs of peers
Source []string Source []string
@ -31,10 +44,44 @@ type Rule struct {
Flow TrafficFlowType 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 { func (r *Rule) Copy() *Rule {
return &Rule{ return &Rule{
ID: r.ID, ID: r.ID,
Name: r.Name, Name: r.Name,
Description: r.Description,
Disabled: r.Disabled,
Source: r.Source[:], Source: r.Source[:],
Destination: r.Destination[:], Destination: r.Destination[:],
Flow: r.Flow, Flow: r.Flow,
@ -79,6 +126,81 @@ func (am *DefaultAccountManager) SaveRule(accountID string, rule *Rule) error {
return am.updateAccountPeers(account) 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 // DeleteRule of ACL from the store
func (am *DefaultAccountManager) DeleteRule(accountID, ruleID string) error { func (am *DefaultAccountManager) DeleteRule(accountID, ruleID string) error {
am.mux.Lock() am.mux.Lock()