Skip to content

Commit

Permalink
feat(user): add uuid to user information (#110)
Browse files Browse the repository at this point in the history
* feat: add uuid field in user

* chore: update proton commit and docker-compose image

* fix(asset): fix star repository getstargazers
  • Loading branch information
mabdh committed Apr 19, 2022
1 parent cccc0f8 commit 279e43a
Show file tree
Hide file tree
Showing 50 changed files with 1,240 additions and 981 deletions.
14 changes: 7 additions & 7 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
NAME="github.com/odpf/columbus"
VERSION=$(shell git describe --always --tags 2>/dev/null)
COVERFILE="/tmp/columbus.coverprofile"
PROTON_COMMIT := "712a5a1ae39c6dbbd5a1e152c2c082dde18b5e79"
PROTON_COMMIT := "2481c008a1eb2525eca058b0729abc036ddcbe6a"

.PHONY: all build test clean install proto

Expand Down Expand Up @@ -40,11 +40,11 @@ proto: ## Generate the protobuf files
install: ## install required dependencies
@echo "> installing dependencies"
go mod tidy
go get -d google.golang.org/protobuf/cmd/protoc-gen-go@v1.27.1
go get google.golang.org/protobuf/cmd/protoc-gen-go@v1.27.1
go get google.golang.org/protobuf/proto@v1.27.1
go get google.golang.org/grpc@v1.45.0
go get -d google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2.0
go get -d github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@v2.8.0
go get -d github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@v2.8.0
go get -d github.com/bufbuild/buf/cmd/buf@v1.1.0
go get -d github.com/envoyproxy/protoc-gen-validate@v0.6.7
go get google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2.0
go get github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@v2.8.0
go get github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@v2.8.0
go get github.com/bufbuild/buf/cmd/buf@v1.3.1
go get github.com/envoyproxy/protoc-gen-validate@v0.6.7
4 changes: 2 additions & 2 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,14 +94,14 @@ func RegisterHTTPRoutes(cfg Config, mux *runtime.ServeMux, deps *Dependencies, h
if err := mux.HandlePath(http.MethodGet, "/v1beta1/types/{name}/records",
middleware.NewRelic(deps.NRApp, http.MethodGet, "/v1beta1/types/{name}/records",
middleware.StatsD(deps.StatsdMonitor,
middleware.ValidateUser(cfg.IdentityHeaderKey, deps.UserService, handlerCollection.Record.GetByType)))); err != nil {
middleware.ValidateUser(cfg.IdentityUUIDHeaderKey, cfg.IdentityEmailHeaderKey, deps.UserService, handlerCollection.Record.GetByType)))); err != nil {
return err
}

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

Expand Down
3 changes: 2 additions & 1 deletion api/config.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package api

type Config struct {
IdentityHeaderKey string
IdentityUUIDHeaderKey string
IdentityEmailHeaderKey string
}
19 changes: 13 additions & 6 deletions api/grpc_interceptor/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,31 @@ import (
// ValidateUser middleware will propagate a valid user ID as string
// within request context
// use `user.FromContext` function to get the user ID string
func ValidateUser(identityHeaderKey string, userSvc *user.Service) grpc.UnaryServerInterceptor {
func ValidateUser(identityUUIDHeaderKey, identityEmailHeaderKey string, userSvc *user.Service) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return "", fmt.Errorf("metadata in grpc doesn't exist")
}

metadataValues := md.Get(identityHeaderKey)
metadataValues := md.Get(identityUUIDHeaderKey)
if len(metadataValues) < 1 {
return nil, status.Errorf(codes.InvalidArgument, "identity header is empty")
return nil, status.Errorf(codes.InvalidArgument, "identity header uuid is empty")
}
userEmail := metadataValues[0]
userID, err := userSvc.ValidateUser(ctx, userEmail)
userUUID := metadataValues[0]

var userEmail = ""
metadataValues = md.Get(identityEmailHeaderKey)
if len(metadataValues) > 0 {
userEmail = metadataValues[0]
}

userID, err := userSvc.ValidateUser(ctx, userUUID, userEmail)
if err != nil {
if errors.Is(err, user.ErrNoUserInformation) {
return nil, status.Errorf(codes.InvalidArgument, err.Error())
}
return nil, status.Errorf(codes.Internal, err.Error())
return nil, status.Errorf(codes.Internal, codes.Internal.String())
}
newCtx := user.NewContext(ctx, userID)
return handler(newCtx, req)
Expand Down
28 changes: 15 additions & 13 deletions api/grpc_interceptor/user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
pb_testproto "github.com/grpc-ecosystem/go-grpc-middleware/testing/testproto"
"github.com/odpf/columbus/lib/mocks"
"github.com/odpf/columbus/user"
"github.com/odpf/salt/log"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
Expand All @@ -20,8 +21,9 @@ import (
)

const (
identityHeaderKey = "Columbus-User-ID"
defaultProvider = "shield"
identityUUIDHeaderKey = "Columbus-User-ID"
identityEmailHeaderKey = "Columbus-User-Email"
userID = "user-id"
)

type UserTestSuite struct {
Expand All @@ -30,16 +32,15 @@ type UserTestSuite struct {
}

func TestUserSuite(t *testing.T) {
logger := log.NewNoop()
mockUserRepo := new(mocks.UserRepository)
userSvc := user.NewService(mockUserRepo, user.Config{
IdentityProviderDefaultName: defaultProvider,
})
userSvc := user.NewService(logger, mockUserRepo)
s := &UserTestSuite{
InterceptorTestSuite: &grpc_testing.InterceptorTestSuite{
TestService: &dummyService{TestServiceServer: &grpc_testing.TestPingService{T: t}},
ServerOpts: []grpc.ServerOption{
grpc_middleware.WithUnaryServerChain(
ValidateUser(identityHeaderKey, userSvc)),
ValidateUser(identityUUIDHeaderKey, identityEmailHeaderKey, userSvc)),
},
},
userRepo: mockUserRepo,
Expand All @@ -51,16 +52,17 @@ func (s *UserTestSuite) TestUnary_IdentityHeaderNotPresent() {
_, err := s.Client.Ping(s.SimpleCtx(), &pb_testproto.PingRequest{Value: "something", SleepTimeMs: 9999})
code := status.Code(err)
require.Equal(s.T(), codes.InvalidArgument, code)
require.EqualError(s.T(), err, "rpc error: code = InvalidArgument desc = identity header is empty")
require.EqualError(s.T(), err, "rpc error: code = InvalidArgument desc = identity header uuid is empty")
}

func (s *UserTestSuite) TestUnary_UserServiceError() {
userEmail := "user-email-error"
userUUID := "user-uuid-error"
customError := errors.New("some error")
s.userRepo.EXPECT().GetID(mock.Anything, userEmail).Return("", customError)
s.userRepo.EXPECT().Create(mock.Anything, mock.Anything).Return("", customError)
s.userRepo.EXPECT().GetByUUID(mock.Anything, userUUID).Return(user.User{}, customError)
s.userRepo.EXPECT().UpsertByEmail(mock.Anything, mock.Anything).Return("", customError)

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

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

ctx := metadata.AppendToOutgoingContext(s.SimpleCtx(), identityHeaderKey, userEmail)
ctx := metadata.AppendToOutgoingContext(s.SimpleCtx(), identityUUIDHeaderKey, userUUID, identityEmailHeaderKey, userEmail)
_, err := s.Client.Ping(ctx, &pb_testproto.PingRequest{Value: "something", SleepTimeMs: 9999})
code := status.Code(err)
require.Equal(s.T(), codes.OK, code)
Expand Down
11 changes: 6 additions & 5 deletions api/httpapi/middleware/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@ import (
// ValidateUser middleware will propagate a valid user ID as string
// within request context
// use `user.FromContext` function to get the user ID string
func ValidateUser(identityHeaderKey string, userSvc *user.Service, h runtime.HandlerFunc) runtime.HandlerFunc {
func ValidateUser(identityUUIDHeaderKey, identityEmailHeaderKey string, userSvc *user.Service, h runtime.HandlerFunc) runtime.HandlerFunc {
return runtime.HandlerFunc(func(rw http.ResponseWriter, r *http.Request, pathParams map[string]string) {
userEmail := r.Header.Get(identityHeaderKey)
if userEmail == "" {
handlers.WriteJSONError(rw, http.StatusBadRequest, "identity header is empty")
userUUID := r.Header.Get(identityUUIDHeaderKey)
if userUUID == "" {
handlers.WriteJSONError(rw, http.StatusBadRequest, "identity header uuid is empty")
return
}
userID, err := userSvc.ValidateUser(r.Context(), userEmail)
userEmail := r.Header.Get(identityEmailHeaderKey)
userID, err := userSvc.ValidateUser(r.Context(), userUUID, userEmail)
if err != nil {
if errors.Is(err, user.ErrNoUserInformation) {
handlers.WriteJSONError(rw, http.StatusBadRequest, err.Error())
Expand Down
164 changes: 76 additions & 88 deletions api/httpapi/middleware/user_test.go
Original file line number Diff line number Diff line change
@@ -1,115 +1,103 @@
package middleware

import (
"encoding/json"
"context"
"errors"
"net/http"
"net/http/httptest"
"testing"

"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"github.com/odpf/columbus/api/httpapi/handlers"
"github.com/odpf/columbus/lib/mocks"
"github.com/odpf/columbus/user"
"github.com/odpf/salt/log"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)

const (
dummyRoute = "/v1beta1/dummy"
identityHeaderKey = "Columbus-User-ID"
dummyRoute = "/v1beta1/dummy"
identityUUIDHeaderKey = "Columbus-User-ID"
identityEmailHeaderKey = "Columbus-User-Email"
userUUID = "user-uuid"
userID = "user-id"
userEmail = "some-email"
)

var userCfg = user.Config{IdentityProviderDefaultName: "shield"}

func TestValidateUser(t *testing.T) {

t.Run("should return HTTP 400 when identity header not present", func(t *testing.T) {
userSvc := user.NewService(nil, userCfg)

r := runtime.NewServeMux()
err := r.HandlePath(http.MethodGet, dummyRoute,
ValidateUser(identityHeaderKey, userSvc, nil))
if err != nil {
t.Fatal(err)
}

req, _ := http.NewRequest("GET", dummyRoute, nil)

rr := httptest.NewRecorder()

r.ServeHTTP(rr, req)

assert.Equal(t, http.StatusBadRequest, rr.Code)
response := &handlers.ErrorResponse{}
err = json.Unmarshal(rr.Body.Bytes(), &response)
if err != nil {
t.Fatal(err)
}

assert.Equal(t, "identity header is empty", response.Reason)
})

t.Run("should return HTTP 500 when something error with user service", func(t *testing.T) {
customError := errors.New("some error")
mockUserRepository := &mocks.UserRepository{}
mockUserRepository.On("GetID", mock.Anything, mock.Anything).Return("", customError)
mockUserRepository.On("Create", mock.Anything, mock.Anything).Return("", customError)

userSvc := user.NewService(mockUserRepository, userCfg)

r := runtime.NewServeMux()
err := r.HandlePath(http.MethodGet, dummyRoute,
ValidateUser(identityHeaderKey, userSvc, nil))
if err != nil {
t.Fatal(err)
}

req, _ := http.NewRequest("GET", dummyRoute, nil)
req.Header.Set(identityHeaderKey, "some-email")
rr := httptest.NewRecorder()

r.ServeHTTP(rr, req)

assert.Equal(t, http.StatusInternalServerError, rr.Code)
response := &handlers.ErrorResponse{}
err = json.Unmarshal(rr.Body.Bytes(), &response)
if err != nil {
t.Fatal(err)
}

assert.Equal(t, customError.Error(), response.Reason)
})

t.Run("should return HTTP 200 with propagated user ID when user validation success", func(t *testing.T) {
userID := "user-id"
userEmail := "some-email"
mockUserRepository := &mocks.UserRepository{}
mockUserRepository.On("GetID", mock.Anything, mock.Anything).Return(userID, nil)
mockUserRepository.On("Create", mock.Anything, mock.Anything).Return(userID, nil)

userSvc := user.NewService(mockUserRepository, userCfg)

r := runtime.NewServeMux()
if err := r.HandlePath(http.MethodGet, dummyRoute,
ValidateUser(identityHeaderKey, userSvc, runtime.HandlerFunc(func(rw http.ResponseWriter, r *http.Request, pathParams map[string]string) {
type testCase struct {
Description string
Setup func(ctx context.Context, userRepo *mocks.UserRepository, req *http.Request)
Handler runtime.HandlerFunc
ExpectStatus int
}

var testCases = []testCase{
{
Description: "should return HTTP 400 when identity header not present",
ExpectStatus: http.StatusBadRequest,
},
{
Description: "should return HTTP 500 when something error with user service",
Setup: func(ctx context.Context, userRepo *mocks.UserRepository, req *http.Request) {
req.Header.Set(identityUUIDHeaderKey, userUUID)
req.Header.Set(identityEmailHeaderKey, userEmail)

customError := errors.New("some error")
userRepo.EXPECT().GetByUUID(mock.Anything, mock.Anything).Return(user.User{}, customError)
userRepo.EXPECT().UpsertByEmail(mock.Anything, mock.Anything).Return("", customError)
},
ExpectStatus: http.StatusInternalServerError,
},
{
Description: "should return HTTP 200 with propagated user ID when user validation success",
Handler: func(rw http.ResponseWriter, r *http.Request, pathParams map[string]string) {
propagatedUserID := user.FromContext(r.Context())
_, err := rw.Write([]byte(propagatedUserID))
if err != nil {
t.Fatal(err)
}
rw.WriteHeader(http.StatusOK)
}))); err != nil {
t.Fatal(err)
}

req, _ := http.NewRequest("GET", dummyRoute, nil)
req.Header.Set(identityHeaderKey, userEmail)

rr := httptest.NewRecorder()

r.ServeHTTP(rr, req)

assert.Equal(t, userID, rr.Body.String())
})
},
Setup: func(ctx context.Context, userRepo *mocks.UserRepository, req *http.Request) {
req.Header.Set(identityUUIDHeaderKey, userUUID)
req.Header.Set(identityEmailHeaderKey, userEmail)

userRepo.EXPECT().GetByUUID(mock.Anything, mock.Anything).Return(user.User{
ID: userID,
UUID: userUUID,
Email: userEmail,
}, nil)
},
ExpectStatus: http.StatusOK,
},
}

for _, tc := range testCases {
t.Run(tc.Description, func(t *testing.T) {
ctx := context.Background()
logger := log.NewNoop()
userRepo := new(mocks.UserRepository)
userSvc := user.NewService(logger, userRepo)

r := runtime.NewServeMux()
err := r.HandlePath(http.MethodGet, dummyRoute,
ValidateUser(identityUUIDHeaderKey, identityEmailHeaderKey, userSvc, tc.Handler))
if err != nil {
t.Fatal(err)
}

req, _ := http.NewRequest("GET", dummyRoute, nil)
rr := httptest.NewRecorder()

if tc.Setup != nil {
tc.Setup(ctx, userRepo, req)
}

r.ServeHTTP(rr, req)

assert.Equal(t, tc.ExpectStatus, rr.Code)
})
}
}
Loading

0 comments on commit 279e43a

Please sign in to comment.