Skip to content

Commit 983f3f3

Browse files
authored
Syncer supports to enable rbac (#1487)
1 parent b133e42 commit 983f3f3

14 files changed

+404
-41
lines changed

.github/workflows/eventbase-ci.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
uses: actions/checkout@v1
1414
- name: UT test
1515
run: |
16-
sudo docker-compose -f ./scripts/docker-compose.yaml up -d
16+
sudo docker compose -f ./scripts/docker-compose.yaml up -d
1717
sleep 20
1818
export TEST_DB_MODE=mongo
1919
export TEST_DB_URI=mongodb://127.0.0.1:27017
@@ -31,7 +31,7 @@ jobs:
3131
uses: actions/checkout@v1
3232
- name: UT for etcd
3333
run: |
34-
time docker run -d -p 2379:2379 --name etcd quay.io/coreos/etcd etcd -name etcd --advertise-client-urls http://0.0.0.0:2379 --listen-client-urls http://0.0.0.0:2379
34+
time docker run -d -p 2379:2379 --name etcd quay.io/coreos/etcd:v3.5.15 etcd -name etcd --advertise-client-urls http://0.0.0.0:2379 --listen-client-urls http://0.0.0.0:2379
3535
while ! nc -z 127.0.0.1 2379; do
3636
sleep 1
3737
done

.github/workflows/static_check.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ jobs:
4040
uses: actions/checkout@v1
4141
- name: UT-MONGO
4242
run: |
43-
sudo docker-compose -f ./scripts/docker-compose.yaml up -d
43+
sudo docker compose -f ./scripts/docker-compose.yaml up -d
4444
sleep 20
4545
bash -x scripts/ut_test_in_docker.sh mongo
4646
integration-test:

docker-compose.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
version: '3'
1818
services:
1919
etcd:
20-
image: 'quay.io/coreos/etcd:latest'
20+
image: 'quay.io/coreos/etcd:v3.5.15'
2121
# restart: always
2222
#ports:
2323
# - "2379:2379"

etc/conf/syncer.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
sync:
22
enableOnStart: false
3+
rbacEnabled: false
34
peers:
45
- name: dc
56
kind: servicecomb
67
endpoints: ["127.0.0.1:30105"]
78
# only allow mode implemented in incremental approach like push, watch(such as pub/sub, long polling)
89
mode: [push]
10+
token:
911
tombstone:
1012
retire:
1113
# use linux crontab not Quartz cron

scripts/integration_test.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ set +e
4141
docker rm -f etcd
4242
kill -9 $(ps aux | grep 'service-center' | awk '{print $2}')
4343
set -e
44-
sudo docker run -d -v /usr/share/ca-certificates/:/etc/ssl/certs -p 40010:40010 -p 23800:23800 -p 2379:2379 --name etcd quay.io/coreos/etcd etcd -name etcd0 -advertise-client-urls http://127.0.0.1:2379,http://127.0.0.1:40010 -listen-client-urls http://0.0.0.0:2379,http://0.0.0.0:40010 -initial-advertise-peer-urls http://127.0.0.1:23800 -listen-peer-urls http://0.0.0.0:23800 -initial-cluster-token etcd-cluster-1 -initial-cluster etcd0=http://127.0.0.1:23800 -initial-cluster-state new
44+
sudo docker run -d -v /usr/share/ca-certificates/:/etc/ssl/certs -p 40010:40010 -p 23800:23800 -p 2379:2379 --name etcd quay.io/coreos/etcd:v3.5.15 etcd -name etcd0 -advertise-client-urls http://127.0.0.1:2379,http://127.0.0.1:40010 -listen-client-urls http://0.0.0.0:2379,http://0.0.0.0:40010 -initial-advertise-peer-urls http://127.0.0.1:23800 -listen-peer-urls http://0.0.0.0:23800 -initial-cluster-token etcd-cluster-1 -initial-cluster etcd0=http://127.0.0.1:23800 -initial-cluster-state new
4545
while ! nc -z 127.0.0.1 2379; do
4646
echo "Waiting Etcd to launch on 2379..."
4747
sleep 1

scripts/ut_test_in_docker.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ echo "${green}Starting Unit Testing for Service Center${reset}"
3131

3232
if [ "${db_name}" == "etcd" ];then
3333
echo "${green}Starting etcd in docker${reset}"
34-
docker run -d -v /usr/share/ca-certificates/:/etc/ssl/certs -p 40010:40010 -p 23800:23800 -p 2379:2379 --name etcd quay.io/coreos/etcd etcd -name etcd0 -advertise-client-urls http://127.0.0.1:2379,http://127.0.0.1:40010 -listen-client-urls http://0.0.0.0:2379,http://0.0.0.0:40010 -initial-advertise-peer-urls http://127.0.0.1:23800 -listen-peer-urls http://0.0.0.0:23800 -initial-cluster-token etcd-cluster-1 -initial-cluster etcd0=http://127.0.0.1:23800 -initial-cluster-state new
34+
docker run -d -v /usr/share/ca-certificates/:/etc/ssl/certs -p 40010:40010 -p 23800:23800 -p 2379:2379 --name etcd quay.io/coreos/etcd:v3.5.15 etcd -name etcd0 -advertise-client-urls http://127.0.0.1:2379,http://127.0.0.1:40010 -listen-client-urls http://0.0.0.0:2379,http://0.0.0.0:40010 -initial-advertise-peer-urls http://127.0.0.1:23800 -listen-peer-urls http://0.0.0.0:23800 -initial-cluster-token etcd-cluster-1 -initial-cluster etcd0=http://127.0.0.1:23800 -initial-cluster-state new
3535
while ! nc -z 127.0.0.1 2379; do
3636
echo "Waiting Etcd to launch on 2379..."
3737
sleep 1

syncer/config/config.go

+10-3
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ import (
2121
"fmt"
2222
"path/filepath"
2323

24+
"github.com/go-chassis/go-archaius"
25+
2426
"github.com/apache/servicecomb-service-center/pkg/log"
2527
"github.com/apache/servicecomb-service-center/pkg/util"
26-
"github.com/go-chassis/go-archaius"
2728
)
2829

2930
var config Config
@@ -33,15 +34,21 @@ type Config struct {
3334
}
3435

3536
type Sync struct {
36-
EnableOnStart bool `yaml:"enableOnStart"`
37-
Peers []*Peer `yaml:"peers"`
37+
EnableOnStart bool `yaml:"enableOnStart"`
38+
// When RbacEnabled is true, syncer's API requires the rbac token,
39+
// and service-center also provides the rbac token to communicate with peer.
40+
// At the same time, service-center rbac must be enabled.
41+
RbacEnabled bool `yaml:"rbacEnabled"`
42+
Peers []*Peer `yaml:"peers"`
3843
}
3944

4045
type Peer struct {
4146
Name string `yaml:"name"`
4247
Kind string `yaml:"kind"`
4348
Endpoints []string `yaml:"endpoints"`
4449
Mode []string `yaml:"mode"`
50+
// The token to communicate with peer, this takes effect only when RbacEnabled is true
51+
Token string `yaml:"token"`
4552
}
4653

4754
func Init() error {

syncer/rpc/auth.go

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package rpc
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
8+
"github.com/go-chassis/cari/rbac"
9+
"github.com/go-chassis/go-chassis/v2/security/authr"
10+
"github.com/go-chassis/go-chassis/v2/server/restful"
11+
"google.golang.org/grpc/metadata"
12+
13+
"github.com/apache/servicecomb-service-center/pkg/log"
14+
"github.com/apache/servicecomb-service-center/syncer/config"
15+
)
16+
17+
var errWrongAccountNorRole = fmt.Errorf("account should be %s, and roles should contain %s", RbacAllowedAccountName, RbacAllowedRoleName)
18+
19+
func auth(ctx context.Context) error {
20+
if !config.GetConfig().Sync.RbacEnabled {
21+
return nil
22+
}
23+
md, ok := metadata.FromIncomingContext(ctx)
24+
if !ok {
25+
return rbac.NewError(rbac.ErrNoAuthHeader, "")
26+
}
27+
28+
authHeader := md.Get(restful.HeaderAuth)
29+
if len(authHeader) == 0 {
30+
return rbac.NewError(rbac.ErrNoAuthHeader, fmt.Sprintf("header %s not found nor content empty", restful.HeaderAuth))
31+
}
32+
33+
s := strings.Split(authHeader[0], " ")
34+
if len(s) != 2 {
35+
return rbac.ErrInvalidHeader
36+
}
37+
to := s[1]
38+
39+
claims, err := authr.Authenticate(ctx, to)
40+
if err != nil {
41+
return err
42+
}
43+
m, ok := claims.(map[string]interface{})
44+
if !ok {
45+
log.Error("claims convert failed", rbac.ErrConvert)
46+
return rbac.ErrConvert
47+
}
48+
account, err := rbac.GetAccount(m)
49+
if err != nil {
50+
log.Error("get account from token failed", err)
51+
return err
52+
}
53+
54+
if account.Name != RbacAllowedAccountName {
55+
return errWrongAccountNorRole
56+
}
57+
for _, role := range account.Roles {
58+
if role == RbacAllowedRoleName {
59+
return nil
60+
}
61+
}
62+
return errWrongAccountNorRole
63+
}

syncer/rpc/auth_test.go

+130
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package rpc
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"fmt"
8+
"testing"
9+
10+
"github.com/go-chassis/cari/pkg/errsvc"
11+
"github.com/go-chassis/cari/rbac"
12+
"github.com/go-chassis/go-chassis/v2/security/authr"
13+
"github.com/go-chassis/go-chassis/v2/server/restful"
14+
"github.com/stretchr/testify/assert"
15+
"google.golang.org/grpc/metadata"
16+
17+
"github.com/apache/servicecomb-service-center/syncer/config"
18+
)
19+
20+
type testAuth struct{}
21+
22+
func (testAuth) Login(ctx context.Context, user string, password string, opts ...authr.LoginOption) (string, error) {
23+
return "", nil
24+
}
25+
26+
func (testAuth) Authenticate(ctx context.Context, token string) (interface{}, error) {
27+
var claim map[string]interface{}
28+
return claim, json.Unmarshal([]byte(token), &claim)
29+
}
30+
31+
func Test_auth(t *testing.T) {
32+
// use the custom auth plugin
33+
authr.Install("test", func(opts *authr.Options) (authr.Authenticator, error) {
34+
return testAuth{}, nil
35+
})
36+
assert.NoError(t, authr.Init(authr.WithPlugin("test")))
37+
38+
type args struct {
39+
ctx context.Context
40+
}
41+
tests := []struct {
42+
name string
43+
preDo func()
44+
args args
45+
wantErr assert.ErrorAssertionFunc
46+
}{
47+
{
48+
name: "sync rbac disables",
49+
preDo: func() {
50+
config.SetConfig(config.Config{
51+
Sync: &config.Sync{
52+
RbacEnabled: false,
53+
}})
54+
},
55+
args: args{
56+
ctx: context.Background(), // rbac disabled, empty ctx should pass the auth
57+
},
58+
wantErr: assert.NoError,
59+
},
60+
{
61+
name: "no header",
62+
preDo: func() {
63+
config.SetConfig(config.Config{
64+
Sync: &config.Sync{
65+
RbacEnabled: true,
66+
}})
67+
},
68+
args: args{
69+
ctx: context.Background(), // rbac enabled, empty ctx should not pass the auth
70+
},
71+
wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
72+
var errSvcErr *errsvc.Error
73+
ok := errors.As(err, &errSvcErr)
74+
assert.True(t, ok)
75+
76+
return assert.Equal(t, rbac.ErrNoAuthHeader, errSvcErr.Code)
77+
},
78+
},
79+
{
80+
name: "with header but no auth header",
81+
args: args{
82+
ctx: metadata.NewIncomingContext(context.Background(), metadata.New(map[string]string{"fake": "fake"})),
83+
},
84+
wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
85+
var errSvcErr *errsvc.Error
86+
ok := errors.As(err, &errSvcErr)
87+
assert.True(t, ok)
88+
89+
return assert.Equal(t, rbac.ErrNoAuthHeader, errSvcErr.Code)
90+
},
91+
},
92+
{
93+
name: "auth header format error",
94+
args: args{
95+
ctx: metadata.NewIncomingContext(context.Background(), metadata.New(map[string]string{restful.HeaderAuth: "fake"})),
96+
},
97+
wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
98+
return assert.Equal(t, rbac.ErrInvalidHeader, err)
99+
},
100+
},
101+
{
102+
name: "wrong account nor role",
103+
args: args{
104+
ctx: metadata.NewIncomingContext(context.Background(),
105+
metadata.New(map[string]string{restful.HeaderAuth: `Bear {"account":"x","roles":["x"]}`})),
106+
},
107+
wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
108+
return assert.Equal(t, errWrongAccountNorRole, err)
109+
},
110+
},
111+
{
112+
name: "valid token",
113+
args: args{
114+
ctx: metadata.NewIncomingContext(context.Background(),
115+
metadata.New(map[string]string{restful.HeaderAuth: `Bear {"account":"sync-user","roles":["sync-admin"]}`})),
116+
},
117+
wantErr: func(t assert.TestingT, err error, i ...interface{}) bool {
118+
return assert.NoError(t, err)
119+
},
120+
},
121+
}
122+
for _, tt := range tests {
123+
t.Run(tt.name, func(t *testing.T) {
124+
if tt.preDo != nil {
125+
tt.preDo()
126+
}
127+
tt.wantErr(t, auth(tt.args.ctx), fmt.Sprintf("auth(%v)", tt.args.ctx))
128+
})
129+
}
130+
}

syncer/rpc/server.go

+34-4
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,21 @@ import (
2222
"fmt"
2323
"time"
2424

25-
"github.com/apache/servicecomb-service-center/syncer/service/replicator"
26-
"github.com/apache/servicecomb-service-center/syncer/service/replicator/resource"
27-
2825
"github.com/apache/servicecomb-service-center/pkg/log"
2926
v1sync "github.com/apache/servicecomb-service-center/syncer/api/v1"
3027
"github.com/apache/servicecomb-service-center/syncer/config"
28+
"github.com/apache/servicecomb-service-center/syncer/service/replicator"
29+
"github.com/apache/servicecomb-service-center/syncer/service/replicator/resource"
3130
)
3231

3332
const (
3433
HealthStatusConnected = "CONNECTED"
3534
HealthStatusAbnormal = "ABNORMAL"
3635
HealthStatusClose = "CLOSE"
36+
HealthStatusAuthFail = "AuthFail"
37+
38+
RbacAllowedAccountName = "sync-user"
39+
RbacAllowedRoleName = "sync-admin"
3740
)
3841

3942
func NewServer() *Server {
@@ -49,13 +52,33 @@ type Server struct {
4952
}
5053

5154
func (s *Server) Sync(ctx context.Context, events *v1sync.EventList) (*v1sync.Results, error) {
55+
err := auth(ctx)
56+
if err != nil {
57+
log.Error("auth failed", err)
58+
return generateFailedResults(events, err)
59+
}
60+
5261
log.Info(fmt.Sprintf("start sync: %s", events.Flag()))
5362

5463
res := s.replicator.Persist(ctx, events)
5564

5665
return s.toResults(res), nil
5766
}
5867

68+
func generateFailedResults(events *v1sync.EventList, err error) (*v1sync.Results, error) {
69+
if events == nil || len(events.Events) == 0 {
70+
return &v1sync.Results{Results: map[string]*v1sync.Result{}}, nil
71+
}
72+
rsts := make(map[string]*v1sync.Result, len(events.Events))
73+
for _, evt := range events.Events {
74+
rsts[evt.Id] = &v1sync.Result{
75+
Code: resource.Fail,
76+
Message: err.Error(),
77+
}
78+
}
79+
return &v1sync.Results{Results: rsts}, nil
80+
}
81+
5982
func (s *Server) toResults(results []*resource.Result) *v1sync.Results {
6083
syncResult := make(map[string]*v1sync.Result, len(results))
6184
for _, r := range results {
@@ -69,11 +92,18 @@ func (s *Server) toResults(results []*resource.Result) *v1sync.Results {
6992
}
7093
}
7194

72-
func (s *Server) Health(_ context.Context, _ *v1sync.HealthRequest) (*v1sync.HealthReply, error) {
95+
func (s *Server) Health(ctx context.Context, _ *v1sync.HealthRequest) (*v1sync.HealthReply, error) {
7396
resp := &v1sync.HealthReply{
7497
Status: HealthStatusConnected,
7598
LocalTimestamp: time.Now().UnixNano(),
7699
}
100+
err := auth(ctx)
101+
if err != nil {
102+
resp.Status = HealthStatusAuthFail
103+
log.Error("auth failed", err)
104+
return resp, nil
105+
}
106+
77107
// TODO enable to close syncer
78108
if !config.GetConfig().Sync.EnableOnStart {
79109
resp.Status = HealthStatusClose

0 commit comments

Comments
 (0)