Skip to content

Commit 279e43a

Browse files
authored
feat(user): add uuid to user information (#110)
* feat: add uuid field in user * chore: update proton commit and docker-compose image * fix(asset): fix star repository getstargazers
1 parent cccc0f8 commit 279e43a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+1240
-981
lines changed

Makefile

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
NAME="github.com/odpf/columbus"
22
VERSION=$(shell git describe --always --tags 2>/dev/null)
33
COVERFILE="/tmp/columbus.coverprofile"
4-
PROTON_COMMIT := "712a5a1ae39c6dbbd5a1e152c2c082dde18b5e79"
4+
PROTON_COMMIT := "2481c008a1eb2525eca058b0729abc036ddcbe6a"
55

66
.PHONY: all build test clean install proto
77

@@ -40,11 +40,11 @@ proto: ## Generate the protobuf files
4040
install: ## install required dependencies
4141
@echo "> installing dependencies"
4242
go mod tidy
43-
go get -d google.golang.org/protobuf/cmd/[email protected]
43+
go get google.golang.org/protobuf/cmd/[email protected]
4444
go get google.golang.org/protobuf/[email protected]
4545
go get google.golang.org/[email protected]
46-
go get -d google.golang.org/grpc/cmd/[email protected]
47-
go get -d github.com/grpc-ecosystem/grpc-gateway/v2/[email protected]
48-
go get -d github.com/grpc-ecosystem/grpc-gateway/v2/[email protected]
49-
go get -d github.com/bufbuild/buf/cmd/buf@v1.1.0
50-
go get -d github.com/envoyproxy/[email protected]
46+
go get google.golang.org/grpc/cmd/[email protected]
47+
go get github.com/grpc-ecosystem/grpc-gateway/v2/[email protected]
48+
go get github.com/grpc-ecosystem/grpc-gateway/v2/[email protected]
49+
go get github.com/bufbuild/buf/cmd/buf@v1.3.1
50+
go get github.com/envoyproxy/[email protected]

api/api.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,14 +94,14 @@ func RegisterHTTPRoutes(cfg Config, mux *runtime.ServeMux, deps *Dependencies, h
9494
if err := mux.HandlePath(http.MethodGet, "/v1beta1/types/{name}/records",
9595
middleware.NewRelic(deps.NRApp, http.MethodGet, "/v1beta1/types/{name}/records",
9696
middleware.StatsD(deps.StatsdMonitor,
97-
middleware.ValidateUser(cfg.IdentityHeaderKey, deps.UserService, handlerCollection.Record.GetByType)))); err != nil {
97+
middleware.ValidateUser(cfg.IdentityUUIDHeaderKey, cfg.IdentityEmailHeaderKey, deps.UserService, handlerCollection.Record.GetByType)))); err != nil {
9898
return err
9999
}
100100

101101
if err := mux.HandlePath(http.MethodGet, "/v1beta1/types/{name}/records/{id}",
102102
middleware.NewRelic(deps.NRApp, http.MethodGet, "/v1beta1/types/{name}/records/{id}",
103103
middleware.StatsD(deps.StatsdMonitor,
104-
middleware.ValidateUser(cfg.IdentityHeaderKey, deps.UserService, handlerCollection.Record.GetOneByType)))); err != nil {
104+
middleware.ValidateUser(cfg.IdentityUUIDHeaderKey, cfg.IdentityEmailHeaderKey, deps.UserService, handlerCollection.Record.GetOneByType)))); err != nil {
105105
return err
106106
}
107107

api/config.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package api
22

33
type Config struct {
4-
IdentityHeaderKey string
4+
IdentityUUIDHeaderKey string
5+
IdentityEmailHeaderKey string
56
}

api/grpc_interceptor/user.go

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,31 @@ import (
1515
// ValidateUser middleware will propagate a valid user ID as string
1616
// within request context
1717
// use `user.FromContext` function to get the user ID string
18-
func ValidateUser(identityHeaderKey string, userSvc *user.Service) grpc.UnaryServerInterceptor {
18+
func ValidateUser(identityUUIDHeaderKey, identityEmailHeaderKey string, userSvc *user.Service) grpc.UnaryServerInterceptor {
1919
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
2020
md, ok := metadata.FromIncomingContext(ctx)
2121
if !ok {
2222
return "", fmt.Errorf("metadata in grpc doesn't exist")
2323
}
2424

25-
metadataValues := md.Get(identityHeaderKey)
25+
metadataValues := md.Get(identityUUIDHeaderKey)
2626
if len(metadataValues) < 1 {
27-
return nil, status.Errorf(codes.InvalidArgument, "identity header is empty")
27+
return nil, status.Errorf(codes.InvalidArgument, "identity header uuid is empty")
2828
}
29-
userEmail := metadataValues[0]
30-
userID, err := userSvc.ValidateUser(ctx, userEmail)
29+
userUUID := metadataValues[0]
30+
31+
var userEmail = ""
32+
metadataValues = md.Get(identityEmailHeaderKey)
33+
if len(metadataValues) > 0 {
34+
userEmail = metadataValues[0]
35+
}
36+
37+
userID, err := userSvc.ValidateUser(ctx, userUUID, userEmail)
3138
if err != nil {
3239
if errors.Is(err, user.ErrNoUserInformation) {
3340
return nil, status.Errorf(codes.InvalidArgument, err.Error())
3441
}
35-
return nil, status.Errorf(codes.Internal, err.Error())
42+
return nil, status.Errorf(codes.Internal, codes.Internal.String())
3643
}
3744
newCtx := user.NewContext(ctx, userID)
3845
return handler(newCtx, req)

api/grpc_interceptor/user_test.go

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
pb_testproto "github.com/grpc-ecosystem/go-grpc-middleware/testing/testproto"
1111
"github.com/odpf/columbus/lib/mocks"
1212
"github.com/odpf/columbus/user"
13+
"github.com/odpf/salt/log"
1314
"github.com/stretchr/testify/mock"
1415
"github.com/stretchr/testify/require"
1516
"github.com/stretchr/testify/suite"
@@ -20,8 +21,9 @@ import (
2021
)
2122

2223
const (
23-
identityHeaderKey = "Columbus-User-ID"
24-
defaultProvider = "shield"
24+
identityUUIDHeaderKey = "Columbus-User-ID"
25+
identityEmailHeaderKey = "Columbus-User-Email"
26+
userID = "user-id"
2527
)
2628

2729
type UserTestSuite struct {
@@ -30,16 +32,15 @@ type UserTestSuite struct {
3032
}
3133

3234
func TestUserSuite(t *testing.T) {
35+
logger := log.NewNoop()
3336
mockUserRepo := new(mocks.UserRepository)
34-
userSvc := user.NewService(mockUserRepo, user.Config{
35-
IdentityProviderDefaultName: defaultProvider,
36-
})
37+
userSvc := user.NewService(logger, mockUserRepo)
3738
s := &UserTestSuite{
3839
InterceptorTestSuite: &grpc_testing.InterceptorTestSuite{
3940
TestService: &dummyService{TestServiceServer: &grpc_testing.TestPingService{T: t}},
4041
ServerOpts: []grpc.ServerOption{
4142
grpc_middleware.WithUnaryServerChain(
42-
ValidateUser(identityHeaderKey, userSvc)),
43+
ValidateUser(identityUUIDHeaderKey, identityEmailHeaderKey, userSvc)),
4344
},
4445
},
4546
userRepo: mockUserRepo,
@@ -51,16 +52,17 @@ func (s *UserTestSuite) TestUnary_IdentityHeaderNotPresent() {
5152
_, err := s.Client.Ping(s.SimpleCtx(), &pb_testproto.PingRequest{Value: "something", SleepTimeMs: 9999})
5253
code := status.Code(err)
5354
require.Equal(s.T(), codes.InvalidArgument, code)
54-
require.EqualError(s.T(), err, "rpc error: code = InvalidArgument desc = identity header is empty")
55+
require.EqualError(s.T(), err, "rpc error: code = InvalidArgument desc = identity header uuid is empty")
5556
}
5657

5758
func (s *UserTestSuite) TestUnary_UserServiceError() {
5859
userEmail := "user-email-error"
60+
userUUID := "user-uuid-error"
5961
customError := errors.New("some error")
60-
s.userRepo.EXPECT().GetID(mock.Anything, userEmail).Return("", customError)
61-
s.userRepo.EXPECT().Create(mock.Anything, mock.Anything).Return("", customError)
62+
s.userRepo.EXPECT().GetByUUID(mock.Anything, userUUID).Return(user.User{}, customError)
63+
s.userRepo.EXPECT().UpsertByEmail(mock.Anything, mock.Anything).Return("", customError)
6264

63-
ctx := metadata.AppendToOutgoingContext(context.Background(), identityHeaderKey, userEmail)
65+
ctx := metadata.AppendToOutgoingContext(context.Background(), identityUUIDHeaderKey, userUUID, identityEmailHeaderKey, userEmail)
6466
_, err := s.Client.Ping(ctx, &pb_testproto.PingRequest{Value: "something", SleepTimeMs: 9999})
6567
code := status.Code(err)
6668
require.Equal(s.T(), codes.Internal, code)
@@ -70,10 +72,10 @@ func (s *UserTestSuite) TestUnary_UserServiceError() {
7072

7173
func (s *UserTestSuite) TestUnary_HeaderPassed() {
7274
userEmail := "user-email"
73-
userID := "user-id"
74-
s.userRepo.EXPECT().GetID(mock.Anything, userEmail).Return(userID, nil)
75+
userUUID := "user-uuid"
76+
s.userRepo.EXPECT().GetByUUID(mock.Anything, userUUID).Return(user.User{ID: userID, UUID: userUUID, Email: userEmail}, nil)
7577

76-
ctx := metadata.AppendToOutgoingContext(s.SimpleCtx(), identityHeaderKey, userEmail)
78+
ctx := metadata.AppendToOutgoingContext(s.SimpleCtx(), identityUUIDHeaderKey, userUUID, identityEmailHeaderKey, userEmail)
7779
_, err := s.Client.Ping(ctx, &pb_testproto.PingRequest{Value: "something", SleepTimeMs: 9999})
7880
code := status.Code(err)
7981
require.Equal(s.T(), codes.OK, code)

api/httpapi/middleware/user.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,15 @@ import (
1212
// ValidateUser middleware will propagate a valid user ID as string
1313
// within request context
1414
// use `user.FromContext` function to get the user ID string
15-
func ValidateUser(identityHeaderKey string, userSvc *user.Service, h runtime.HandlerFunc) runtime.HandlerFunc {
15+
func ValidateUser(identityUUIDHeaderKey, identityEmailHeaderKey string, userSvc *user.Service, h runtime.HandlerFunc) runtime.HandlerFunc {
1616
return runtime.HandlerFunc(func(rw http.ResponseWriter, r *http.Request, pathParams map[string]string) {
17-
userEmail := r.Header.Get(identityHeaderKey)
18-
if userEmail == "" {
19-
handlers.WriteJSONError(rw, http.StatusBadRequest, "identity header is empty")
17+
userUUID := r.Header.Get(identityUUIDHeaderKey)
18+
if userUUID == "" {
19+
handlers.WriteJSONError(rw, http.StatusBadRequest, "identity header uuid is empty")
2020
return
2121
}
22-
userID, err := userSvc.ValidateUser(r.Context(), userEmail)
22+
userEmail := r.Header.Get(identityEmailHeaderKey)
23+
userID, err := userSvc.ValidateUser(r.Context(), userUUID, userEmail)
2324
if err != nil {
2425
if errors.Is(err, user.ErrNoUserInformation) {
2526
handlers.WriteJSONError(rw, http.StatusBadRequest, err.Error())
Lines changed: 76 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,115 +1,103 @@
11
package middleware
22

33
import (
4-
"encoding/json"
4+
"context"
55
"errors"
66
"net/http"
77
"net/http/httptest"
88
"testing"
99

1010
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
11-
"github.com/odpf/columbus/api/httpapi/handlers"
1211
"github.com/odpf/columbus/lib/mocks"
1312
"github.com/odpf/columbus/user"
13+
"github.com/odpf/salt/log"
1414
"github.com/stretchr/testify/assert"
1515
"github.com/stretchr/testify/mock"
1616
)
1717

1818
const (
19-
dummyRoute = "/v1beta1/dummy"
20-
identityHeaderKey = "Columbus-User-ID"
19+
dummyRoute = "/v1beta1/dummy"
20+
identityUUIDHeaderKey = "Columbus-User-ID"
21+
identityEmailHeaderKey = "Columbus-User-Email"
22+
userUUID = "user-uuid"
23+
userID = "user-id"
24+
userEmail = "some-email"
2125
)
2226

23-
var userCfg = user.Config{IdentityProviderDefaultName: "shield"}
24-
2527
func TestValidateUser(t *testing.T) {
2628

27-
t.Run("should return HTTP 400 when identity header not present", func(t *testing.T) {
28-
userSvc := user.NewService(nil, userCfg)
29-
30-
r := runtime.NewServeMux()
31-
err := r.HandlePath(http.MethodGet, dummyRoute,
32-
ValidateUser(identityHeaderKey, userSvc, nil))
33-
if err != nil {
34-
t.Fatal(err)
35-
}
36-
37-
req, _ := http.NewRequest("GET", dummyRoute, nil)
38-
39-
rr := httptest.NewRecorder()
40-
41-
r.ServeHTTP(rr, req)
42-
43-
assert.Equal(t, http.StatusBadRequest, rr.Code)
44-
response := &handlers.ErrorResponse{}
45-
err = json.Unmarshal(rr.Body.Bytes(), &response)
46-
if err != nil {
47-
t.Fatal(err)
48-
}
49-
50-
assert.Equal(t, "identity header is empty", response.Reason)
51-
})
52-
53-
t.Run("should return HTTP 500 when something error with user service", func(t *testing.T) {
54-
customError := errors.New("some error")
55-
mockUserRepository := &mocks.UserRepository{}
56-
mockUserRepository.On("GetID", mock.Anything, mock.Anything).Return("", customError)
57-
mockUserRepository.On("Create", mock.Anything, mock.Anything).Return("", customError)
58-
59-
userSvc := user.NewService(mockUserRepository, userCfg)
60-
61-
r := runtime.NewServeMux()
62-
err := r.HandlePath(http.MethodGet, dummyRoute,
63-
ValidateUser(identityHeaderKey, userSvc, nil))
64-
if err != nil {
65-
t.Fatal(err)
66-
}
67-
68-
req, _ := http.NewRequest("GET", dummyRoute, nil)
69-
req.Header.Set(identityHeaderKey, "some-email")
70-
rr := httptest.NewRecorder()
71-
72-
r.ServeHTTP(rr, req)
73-
74-
assert.Equal(t, http.StatusInternalServerError, rr.Code)
75-
response := &handlers.ErrorResponse{}
76-
err = json.Unmarshal(rr.Body.Bytes(), &response)
77-
if err != nil {
78-
t.Fatal(err)
79-
}
80-
81-
assert.Equal(t, customError.Error(), response.Reason)
82-
})
83-
84-
t.Run("should return HTTP 200 with propagated user ID when user validation success", func(t *testing.T) {
85-
userID := "user-id"
86-
userEmail := "some-email"
87-
mockUserRepository := &mocks.UserRepository{}
88-
mockUserRepository.On("GetID", mock.Anything, mock.Anything).Return(userID, nil)
89-
mockUserRepository.On("Create", mock.Anything, mock.Anything).Return(userID, nil)
90-
91-
userSvc := user.NewService(mockUserRepository, userCfg)
92-
93-
r := runtime.NewServeMux()
94-
if err := r.HandlePath(http.MethodGet, dummyRoute,
95-
ValidateUser(identityHeaderKey, userSvc, runtime.HandlerFunc(func(rw http.ResponseWriter, r *http.Request, pathParams map[string]string) {
29+
type testCase struct {
30+
Description string
31+
Setup func(ctx context.Context, userRepo *mocks.UserRepository, req *http.Request)
32+
Handler runtime.HandlerFunc
33+
ExpectStatus int
34+
}
35+
36+
var testCases = []testCase{
37+
{
38+
Description: "should return HTTP 400 when identity header not present",
39+
ExpectStatus: http.StatusBadRequest,
40+
},
41+
{
42+
Description: "should return HTTP 500 when something error with user service",
43+
Setup: func(ctx context.Context, userRepo *mocks.UserRepository, req *http.Request) {
44+
req.Header.Set(identityUUIDHeaderKey, userUUID)
45+
req.Header.Set(identityEmailHeaderKey, userEmail)
46+
47+
customError := errors.New("some error")
48+
userRepo.EXPECT().GetByUUID(mock.Anything, mock.Anything).Return(user.User{}, customError)
49+
userRepo.EXPECT().UpsertByEmail(mock.Anything, mock.Anything).Return("", customError)
50+
},
51+
ExpectStatus: http.StatusInternalServerError,
52+
},
53+
{
54+
Description: "should return HTTP 200 with propagated user ID when user validation success",
55+
Handler: func(rw http.ResponseWriter, r *http.Request, pathParams map[string]string) {
9656
propagatedUserID := user.FromContext(r.Context())
9757
_, err := rw.Write([]byte(propagatedUserID))
9858
if err != nil {
9959
t.Fatal(err)
10060
}
10161
rw.WriteHeader(http.StatusOK)
102-
}))); err != nil {
103-
t.Fatal(err)
104-
}
105-
106-
req, _ := http.NewRequest("GET", dummyRoute, nil)
107-
req.Header.Set(identityHeaderKey, userEmail)
108-
109-
rr := httptest.NewRecorder()
110-
111-
r.ServeHTTP(rr, req)
112-
113-
assert.Equal(t, userID, rr.Body.String())
114-
})
62+
},
63+
Setup: func(ctx context.Context, userRepo *mocks.UserRepository, req *http.Request) {
64+
req.Header.Set(identityUUIDHeaderKey, userUUID)
65+
req.Header.Set(identityEmailHeaderKey, userEmail)
66+
67+
userRepo.EXPECT().GetByUUID(mock.Anything, mock.Anything).Return(user.User{
68+
ID: userID,
69+
UUID: userUUID,
70+
Email: userEmail,
71+
}, nil)
72+
},
73+
ExpectStatus: http.StatusOK,
74+
},
75+
}
76+
77+
for _, tc := range testCases {
78+
t.Run(tc.Description, func(t *testing.T) {
79+
ctx := context.Background()
80+
logger := log.NewNoop()
81+
userRepo := new(mocks.UserRepository)
82+
userSvc := user.NewService(logger, userRepo)
83+
84+
r := runtime.NewServeMux()
85+
err := r.HandlePath(http.MethodGet, dummyRoute,
86+
ValidateUser(identityUUIDHeaderKey, identityEmailHeaderKey, userSvc, tc.Handler))
87+
if err != nil {
88+
t.Fatal(err)
89+
}
90+
91+
req, _ := http.NewRequest("GET", dummyRoute, nil)
92+
rr := httptest.NewRecorder()
93+
94+
if tc.Setup != nil {
95+
tc.Setup(ctx, userRepo, req)
96+
}
97+
98+
r.ServeHTTP(rr, req)
99+
100+
assert.Equal(t, tc.ExpectStatus, rr.Code)
101+
})
102+
}
115103
}

0 commit comments

Comments
 (0)