diff --git a/Dockerfile b/Dockerfile index 0b5e338..93cf2d6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,6 +13,6 @@ COPY start.sh . COPY wait-for.sh . COPY db/migration ./db/migration -EXPOSE 8080 +EXPOSE 8080 9090 CMD [ "/app/main" ] ENTRYPOINT [ "/app/start.sh" ] \ No newline at end of file diff --git a/README.md b/README.md index 77610d6..4c08495 100644 --- a/README.md +++ b/README.md @@ -11,21 +11,3 @@ To run gRPC client Test redis server docker exec -it redis redis-cli ping - -docker run --name simplebank --network bank-network -p 8080:8080 -e GIN_MODE=release -e DB_SOURCE="postgresql://root:Prince2024@postgres12:5432/simple_bank?sslmode=disable" simplebank:latest - - To get external API - kubectl get services simple-bank-api-service - - proto: - rm -f pb/*.go - Remove-Item pb/*.go -Force - protoc --proto_path=proto --go_out=pb --go_opt=paths=source_relative \ - --go-grpc_out=pb --go-grpc_opt=paths=source_relative \ - --openapiv2_out=doc/swagger \ - proto/*.proto - - - https://medium.com/@rafaellevissa/microservices-architecture-in-golang-a-transformative-experience-596d1427a29a - - diff --git a/api/account_test.go b/api/account_test.go index ce49f60..a43a580 100644 --- a/api/account_test.go +++ b/api/account_test.go @@ -36,7 +36,7 @@ func TestGetAccountAPI(t *testing.T) { name: "OK", accountID: account.ID, setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, time.Minute) + addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, user.Role, time.Minute) }, buildStubs: func(store *mockdb.MockStore) { store.EXPECT(). @@ -53,7 +53,7 @@ func TestGetAccountAPI(t *testing.T) { name: "UnauthorizedUser", accountID: account.ID, setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, authorizationTypeBearer, "unauthorized_user", time.Minute) + addAuthorization(t, request, tokenMaker, authorizationTypeBearer, "unauthorized_user", util.DepositorRole, time.Minute) }, buildStubs: func(store *mockdb.MockStore) { store.EXPECT(). @@ -83,7 +83,7 @@ func TestGetAccountAPI(t *testing.T) { name: "NotFound", accountID: account.ID, setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, time.Minute) + addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, user.Role, time.Minute) }, buildStubs: func(store *mockdb.MockStore) { @@ -100,7 +100,7 @@ func TestGetAccountAPI(t *testing.T) { name: "InternalError", accountID: account.ID, setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, time.Minute) + addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, user.Role, time.Minute) }, buildStubs: func(store *mockdb.MockStore) { store.EXPECT(). @@ -116,7 +116,7 @@ func TestGetAccountAPI(t *testing.T) { name: "InvalidID", accountID: 0, setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, time.Minute) + addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, user.Role, time.Minute) }, buildStubs: func(store *mockdb.MockStore) { store.EXPECT(). @@ -170,7 +170,7 @@ func TestCreateAccountAPI(t *testing.T) { "currency": account.Currency, }, setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, time.Minute) + addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, user.Role, time.Minute) }, buildStubs: func(store *mockdb.MockStore) { arg := db.CreateAccountParams{ @@ -211,7 +211,7 @@ func TestCreateAccountAPI(t *testing.T) { "currency": account.Currency, }, setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, time.Minute) + addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, user.Role, time.Minute) }, buildStubs: func(store *mockdb.MockStore) { store.EXPECT(). @@ -229,7 +229,7 @@ func TestCreateAccountAPI(t *testing.T) { "currency": "invalid", }, setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, time.Minute) + addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, user.Role, time.Minute) }, buildStubs: func(store *mockdb.MockStore) { store.EXPECT(). @@ -298,7 +298,7 @@ func TestListAccountsAPI(t *testing.T) { pageSize: n, }, setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, time.Minute) + addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, user.Role, time.Minute) }, buildStubs: func(store *mockdb.MockStore) { arg := db.ListAccountsParams{ @@ -341,7 +341,7 @@ func TestListAccountsAPI(t *testing.T) { pageSize: n, }, setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, time.Minute) + addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, user.Role, time.Minute) }, buildStubs: func(store *mockdb.MockStore) { store.EXPECT(). @@ -360,7 +360,7 @@ func TestListAccountsAPI(t *testing.T) { pageSize: n, }, setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, time.Minute) + addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, user.Role, time.Minute) }, buildStubs: func(store *mockdb.MockStore) { store.EXPECT(). @@ -378,7 +378,7 @@ func TestListAccountsAPI(t *testing.T) { pageSize: 100000, }, setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, time.Minute) + addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user.Username, user.Role, time.Minute) }, buildStubs: func(store *mockdb.MockStore) { store.EXPECT(). diff --git a/api/middleware_test.go b/api/middleware_test.go index ecc8380..8b63e89 100644 --- a/api/middleware_test.go +++ b/api/middleware_test.go @@ -9,6 +9,7 @@ import ( "github.com/gin-gonic/gin" "github.com/imrishuroy/simplebank/token" + "github.com/imrishuroy/simplebank/util" "github.com/stretchr/testify/require" ) @@ -18,9 +19,10 @@ func addAuthorization( tokenMaker token.Maker, authorizationType string, username string, + role string, duration time.Duration, ) { - token, payload, err := tokenMaker.CreateToken(username, duration) + token, payload, err := tokenMaker.CreateToken(username, role, duration) require.NoError(t, err) require.NotEmpty(t, payload) @@ -29,6 +31,9 @@ func addAuthorization( } func TestAuthMiddleware(t *testing.T) { + username := util.RandomOwner() + role := util.DepositorRole + testCases := []struct { name string setupAuth func(t *testing.T, request *http.Request, tokenMaker token.Maker) @@ -37,7 +42,7 @@ func TestAuthMiddleware(t *testing.T) { { name: "OK", setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, authorizationTypeBearer, "user", time.Minute) + addAuthorization(t, request, tokenMaker, authorizationTypeBearer, username, role, time.Minute) }, checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) { require.Equal(t, http.StatusOK, recorder.Code) @@ -54,7 +59,7 @@ func TestAuthMiddleware(t *testing.T) { { name: "UnsupportedAuthorization", setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, "unsupported", "user", time.Minute) + addAuthorization(t, request, tokenMaker, "unsupported", username, role, time.Minute) }, checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) { require.Equal(t, http.StatusUnauthorized, recorder.Code) @@ -63,7 +68,7 @@ func TestAuthMiddleware(t *testing.T) { { name: "InvalidAuthorizationFormat", setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, "", "user", time.Minute) + addAuthorization(t, request, tokenMaker, "", username, role, time.Minute) }, checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) { require.Equal(t, http.StatusUnauthorized, recorder.Code) @@ -72,7 +77,7 @@ func TestAuthMiddleware(t *testing.T) { { name: "ExpiredToken", setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, authorizationTypeBearer, "user", -time.Minute) + addAuthorization(t, request, tokenMaker, authorizationTypeBearer, username, role, -time.Minute) }, checkResponse: func(t *testing.T, recorder *httptest.ResponseRecorder) { require.Equal(t, http.StatusUnauthorized, recorder.Code) diff --git a/api/token.go b/api/token.go index 0f14899..1ee1d97 100644 --- a/api/token.go +++ b/api/token.go @@ -68,6 +68,7 @@ func (server *Server) renewAccessToken(ctx *gin.Context) { accessToken, accessPayload, err := server.tokenMaker.CreateToken( refreshPayload.Username, + refreshPayload.Role, server.config.AccessTokenDuration, ) if err != nil { diff --git a/api/transfer_test.go b/api/transfer_test.go index 03fbd71..a6c9ba8 100644 --- a/api/transfer_test.go +++ b/api/transfer_test.go @@ -49,7 +49,7 @@ func TestTransferAPI(t *testing.T) { "currency": util.USD, }, setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user1.Username, time.Minute) + addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user1.Username, user1.Role, time.Minute) }, buildStubs: func(store *mockdb.MockStore) { store.EXPECT().GetAccount(gomock.Any(), gomock.Eq(account1.ID)).Times(1).Return(account1, nil) @@ -75,7 +75,7 @@ func TestTransferAPI(t *testing.T) { "currency": util.USD, }, setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user2.Username, time.Minute) + addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user2.Username, user2.Role, time.Minute) }, buildStubs: func(store *mockdb.MockStore) { store.EXPECT().GetAccount(gomock.Any(), gomock.Eq(account1.ID)).Times(1).Return(account1, nil) @@ -113,7 +113,7 @@ func TestTransferAPI(t *testing.T) { "currency": util.USD, }, setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user1.Username, time.Minute) + addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user1.Username, user1.Role, time.Minute) }, buildStubs: func(store *mockdb.MockStore) { store.EXPECT().GetAccount(gomock.Any(), gomock.Eq(account1.ID)).Times(1).Return(db.Account{}, db.ErrRecordNotFound) @@ -133,7 +133,7 @@ func TestTransferAPI(t *testing.T) { "currency": util.USD, }, setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user1.Username, time.Minute) + addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user1.Username, user1.Role, time.Minute) }, buildStubs: func(store *mockdb.MockStore) { store.EXPECT().GetAccount(gomock.Any(), gomock.Eq(account1.ID)).Times(1).Return(account1, nil) @@ -153,7 +153,7 @@ func TestTransferAPI(t *testing.T) { "currency": util.USD, }, setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user3.Username, time.Minute) + addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user3.Username, user3.Role, time.Minute) }, buildStubs: func(store *mockdb.MockStore) { store.EXPECT().GetAccount(gomock.Any(), gomock.Eq(account3.ID)).Times(1).Return(account3, nil) @@ -173,7 +173,7 @@ func TestTransferAPI(t *testing.T) { "currency": util.USD, }, setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user1.Username, time.Minute) + addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user1.Username, user1.Role, time.Minute) }, buildStubs: func(store *mockdb.MockStore) { store.EXPECT().GetAccount(gomock.Any(), gomock.Eq(account1.ID)).Times(1).Return(account1, nil) @@ -193,7 +193,7 @@ func TestTransferAPI(t *testing.T) { "currency": "XYZ", }, setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user1.Username, time.Minute) + addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user1.Username, user1.Role, time.Minute) }, buildStubs: func(store *mockdb.MockStore) { store.EXPECT().GetAccount(gomock.Any(), gomock.Any()).Times(0) @@ -212,7 +212,7 @@ func TestTransferAPI(t *testing.T) { "currency": util.USD, }, setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user1.Username, time.Minute) + addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user1.Username, user1.Role, time.Minute) }, buildStubs: func(store *mockdb.MockStore) { store.EXPECT().GetAccount(gomock.Any(), gomock.Any()).Times(0) @@ -231,7 +231,7 @@ func TestTransferAPI(t *testing.T) { "currency": util.USD, }, setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user1.Username, time.Minute) + addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user1.Username, user1.Role, time.Minute) }, buildStubs: func(store *mockdb.MockStore) { store.EXPECT().GetAccount(gomock.Any(), gomock.Any()).Times(1).Return(db.Account{}, sql.ErrConnDone) @@ -250,7 +250,7 @@ func TestTransferAPI(t *testing.T) { "currency": util.USD, }, setupAuth: func(t *testing.T, request *http.Request, tokenMaker token.Maker) { - addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user1.Username, time.Minute) + addAuthorization(t, request, tokenMaker, authorizationTypeBearer, user1.Username, user1.Role, time.Minute) }, buildStubs: func(store *mockdb.MockStore) { store.EXPECT().GetAccount(gomock.Any(), gomock.Eq(account1.ID)).Times(1).Return(account1, nil) @@ -289,5 +289,4 @@ func TestTransferAPI(t *testing.T) { tc.checkResponse(recorder) }) } - } diff --git a/api/user.go b/api/user.go index a7574e2..cd474e2 100644 --- a/api/user.go +++ b/api/user.go @@ -107,6 +107,7 @@ func (server *Server) loginUser(ctx *gin.Context) { accessToken, accessPayload, err := server.tokenMaker.CreateToken( user.Username, + user.Role, server.config.AccessTokenDuration, ) @@ -116,7 +117,7 @@ func (server *Server) loginUser(ctx *gin.Context) { } refreshToken, refreshPayload, err := server.tokenMaker.CreateToken( - req.Username, server.config.RefreshTokenDuration, + req.Username, user.Role, server.config.RefreshTokenDuration, ) if err != nil { diff --git a/db/migration/000005_add_role_to_users.down.sql b/db/migration/000005_add_role_to_users.down.sql new file mode 100644 index 0000000..44cf1e9 --- /dev/null +++ b/db/migration/000005_add_role_to_users.down.sql @@ -0,0 +1 @@ +ALTER TABLE "users" DROP COLUMN "role" varchar NOT NULL DEFAULT 'depositor'; \ No newline at end of file diff --git a/db/migration/000005_add_role_to_users.up.sql b/db/migration/000005_add_role_to_users.up.sql new file mode 100644 index 0000000..d93b626 --- /dev/null +++ b/db/migration/000005_add_role_to_users.up.sql @@ -0,0 +1 @@ +ALTER TABLE "users" ADD COLUMN "role" varchar NOT NULL DEFAULT 'depositor'; \ No newline at end of file diff --git a/db/sqlc/models.go b/db/sqlc/models.go index 004722f..13df4c9 100644 --- a/db/sqlc/models.go +++ b/db/sqlc/models.go @@ -54,6 +54,7 @@ type User struct { PasswordChangedAt time.Time `json:"password_changed_at"` CreatedAt time.Time `json:"created_at"` IsEmailVerified bool `json:"is_email_verified"` + Role string `json:"role"` } type VerifyEmail struct { diff --git a/db/sqlc/user.sql.go b/db/sqlc/user.sql.go index 17f7c4e..9e93d88 100644 --- a/db/sqlc/user.sql.go +++ b/db/sqlc/user.sql.go @@ -19,7 +19,7 @@ INSERT INTO users ( email ) VALUES ( $1, $2, $3, $4 -) RETURNING username, hashed_password, full_name, email, password_changed_at, created_at, is_email_verified +) RETURNING username, hashed_password, full_name, email, password_changed_at, created_at, is_email_verified, role ` type CreateUserParams struct { @@ -45,12 +45,13 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, e &i.PasswordChangedAt, &i.CreatedAt, &i.IsEmailVerified, + &i.Role, ) return i, err } const getUser = `-- name: GetUser :one -SELECT username, hashed_password, full_name, email, password_changed_at, created_at, is_email_verified FROM users +SELECT username, hashed_password, full_name, email, password_changed_at, created_at, is_email_verified, role FROM users WHERE username = $1 LIMIT 1 ` @@ -65,6 +66,7 @@ func (q *Queries) GetUser(ctx context.Context, username string) (User, error) { &i.PasswordChangedAt, &i.CreatedAt, &i.IsEmailVerified, + &i.Role, ) return i, err } @@ -79,7 +81,7 @@ SET is_email_verified = COALESCE($5, is_email_verified) WHERE username = $6 -RETURNING username, hashed_password, full_name, email, password_changed_at, created_at, is_email_verified +RETURNING username, hashed_password, full_name, email, password_changed_at, created_at, is_email_verified, role ` type UpdateUserParams struct { @@ -109,6 +111,7 @@ func (q *Queries) UpdateUser(ctx context.Context, arg UpdateUserParams) (User, e &i.PasswordChangedAt, &i.CreatedAt, &i.IsEmailVerified, + &i.Role, ) return i, err } diff --git a/doc/db.dbml b/doc/db.dbml index b8f4087..74e3d38 100644 --- a/doc/db.dbml +++ b/doc/db.dbml @@ -7,6 +7,7 @@ Project simple_bank { Table users as U { username varchar [pk] + role varchar [not null, default: 'depositor'] hashed_password varchar [not null] full_name varchar [not null] email varchar [unique, not null] diff --git a/doc/schema.sql b/doc/schema.sql index dd4c4b1..895d5ca 100644 --- a/doc/schema.sql +++ b/doc/schema.sql @@ -1,9 +1,10 @@ -- SQL dump generated using DBML (dbml-lang.org) -- Database: PostgreSQL --- Generated at: 2023-12-05T16:21:40.496Z +-- Generated at: 2023-12-09T06:13:38.638Z CREATE TABLE "users" ( "username" varchar PRIMARY KEY, + "role" varchar NOT NULL DEFAULT 'depositor', "hashed_password" varchar NOT NULL, "full_name" varchar NOT NULL, "email" varchar UNIQUE NOT NULL, diff --git a/gapi/authorization.go b/gapi/authorization.go index 4dad183..24dc170 100644 --- a/gapi/authorization.go +++ b/gapi/authorization.go @@ -14,7 +14,7 @@ const ( authorizationBearer = "bearer" ) -func (server *Server) authorizeUser(ctx context.Context) (*token.Payload, error) { +func (server *Server) authorizeUser(ctx context.Context, accessibleRoles []string) (*token.Payload, error) { md, ok := metadata.FromIncomingContext(ctx) if !ok { return nil, fmt.Errorf("missing metadata") @@ -42,5 +42,18 @@ func (server *Server) authorizeUser(ctx context.Context) (*token.Payload, error) return nil, fmt.Errorf("invalid access token: %s", err) } + if !hasPermission(payload.Role, accessibleRoles) { + return nil, fmt.Errorf("permission denied") + } + return payload, nil } + +func hasPermission(userRole string, accessibleRoles []string) bool { + for _, role := range accessibleRoles { + if userRole == role { + return true + } + } + return false +} diff --git a/gapi/main_test.go b/gapi/main_test.go index 53e0a79..970c617 100644 --- a/gapi/main_test.go +++ b/gapi/main_test.go @@ -26,8 +26,8 @@ func newTestServer(t *testing.T, store db.Store, taskDistributor worker.TaskDist return server } -func newContextWithBearerToken(t *testing.T, tokenMaker token.Maker, username string, duration time.Duration) context.Context { - accessToken, _, err := tokenMaker.CreateToken(username, duration) +func newContextWithBearerToken(t *testing.T, tokenMaker token.Maker, username string, role string, duration time.Duration) context.Context { + accessToken, _, err := tokenMaker.CreateToken(username, role, duration) require.NoError(t, err) bearerToken := fmt.Sprintf("%s %s", authorizationBearer, accessToken) diff --git a/gapi/rpc_create_user_test.go b/gapi/rpc_create_user_test.go index ee0f149..f32d625 100644 --- a/gapi/rpc_create_user_test.go +++ b/gapi/rpc_create_user_test.go @@ -61,6 +61,7 @@ func randomUser(t *testing.T) (user db.User, password string) { user = db.User{ Username: util.RandomOwner(), + Role: util.DepositorRole, HashedPassword: hashedPassword, FullName: util.RandomOwner(), Email: util.RandomEmail(), diff --git a/gapi/rpc_login_user.go b/gapi/rpc_login_user.go index 243d193..9c03078 100644 --- a/gapi/rpc_login_user.go +++ b/gapi/rpc_login_user.go @@ -35,6 +35,7 @@ func (server *Server) LoginUser(ctx context.Context, req *pb.LoginUserRequest) ( accessToken, accessPayload, err := server.tokenMaker.CreateToken( user.Username, + user.Role, server.config.AccessTokenDuration, ) if err != nil { @@ -43,6 +44,7 @@ func (server *Server) LoginUser(ctx context.Context, req *pb.LoginUserRequest) ( refreshToken, refreshPayload, err := server.tokenMaker.CreateToken( user.Username, + user.Role, server.config.RefreshTokenDuration, ) if err != nil { diff --git a/gapi/rpc_update_user.go b/gapi/rpc_update_user.go index 1b9ded4..8228064 100644 --- a/gapi/rpc_update_user.go +++ b/gapi/rpc_update_user.go @@ -17,7 +17,7 @@ import ( func (server *Server) UpdateUser(ctx context.Context, req *pb.UpdateUserRequest) (*pb.UpdateUserResponse, error) { - authPayload, err := server.authorizeUser(ctx) + authPayload, err := server.authorizeUser(ctx, []string{util.BankerRole, util.DepositorRole}) if err != nil { return nil, unauthenticatedError(err) } @@ -27,7 +27,8 @@ func (server *Server) UpdateUser(ctx context.Context, req *pb.UpdateUserRequest) return nil, invalidArgumentError(violations) } - if authPayload.Username != req.GetUsername() { + // allowing bankers to update other users account also + if authPayload.Role != util.BankerRole && authPayload.Username != req.GetUsername() { return nil, status.Errorf(codes.PermissionDenied, "cannot update other user's info") } diff --git a/gapi/rpc_update_user_test.go b/gapi/rpc_update_user_test.go index c9eed13..48c8ccc 100644 --- a/gapi/rpc_update_user_test.go +++ b/gapi/rpc_update_user_test.go @@ -65,7 +65,7 @@ func TestUpdateUserAPI(t *testing.T) { Return(updatedUser, nil) }, buildContext: func(t *testing.T, tokenMaker token.Maker) context.Context { - return newContextWithBearerToken(t, tokenMaker, user.Username, time.Minute) + return newContextWithBearerToken(t, tokenMaker, user.Username, user.Role, time.Minute) }, checkResponse: func(t *testing.T, res *pb.UpdateUserResponse, err error) { require.NoError(t, err) @@ -90,7 +90,7 @@ func TestUpdateUserAPI(t *testing.T) { Return(db.User{}, db.ErrRecordNotFound) }, buildContext: func(t *testing.T, tokenMaker token.Maker) context.Context { - return newContextWithBearerToken(t, tokenMaker, user.Username, time.Minute) + return newContextWithBearerToken(t, tokenMaker, user.Username, user.Role, time.Minute) }, checkResponse: func(t *testing.T, res *pb.UpdateUserResponse, err error) { require.Error(t, err) @@ -112,7 +112,7 @@ func TestUpdateUserAPI(t *testing.T) { Times(0) }, buildContext: func(t *testing.T, tokenMaker token.Maker) context.Context { - return newContextWithBearerToken(t, tokenMaker, user.Username, time.Minute) + return newContextWithBearerToken(t, tokenMaker, user.Username, user.Role, time.Minute) }, checkResponse: func(t *testing.T, res *pb.UpdateUserResponse, err error) { require.Error(t, err) @@ -134,7 +134,7 @@ func TestUpdateUserAPI(t *testing.T) { Times(0) }, buildContext: func(t *testing.T, tokenMaker token.Maker) context.Context { - return newContextWithBearerToken(t, tokenMaker, user.Username, -time.Minute) + return newContextWithBearerToken(t, tokenMaker, user.Username, user.Role, -time.Minute) }, checkResponse: func(t *testing.T, res *pb.UpdateUserResponse, err error) { require.Error(t, err) diff --git a/res.txt b/res.txt index df8bb41..9db957a 100644 --- a/res.txt +++ b/res.txt @@ -34,4 +34,7 @@ PASETO - A jwt alternative ( https://developer.okta.com/blog/2019/10/17/a-thorou https://kubernetes.io/docs/tutorials/stateless-application/expose-external-ip-address/ Developing Go Apps With Docker -https://www.docker.com/blog/developing-go-apps-docker/ \ No newline at end of file +https://www.docker.com/blog/developing-go-apps-docker/ + +microservices architecture in Go +https://medium.com/@rafaellevissa/microservices-architecture-in-golang-a-transformative-experience-596d1427a29a diff --git a/token/jwt_maker.go b/token/jwt_maker.go index 9e86cc0..8ffd1cb 100644 --- a/token/jwt_maker.go +++ b/token/jwt_maker.go @@ -27,8 +27,8 @@ func NewJWTMaker(secretKey string) (Maker, error) { } // CreateToken creates a new token for a specific username and duration -func (maker *JWTMaker) CreateToken(username string, duration time.Duration) (string, *Payload, error) { - payload, err := NewPayload(username, duration) +func (maker *JWTMaker) CreateToken(username string, role string, duration time.Duration) (string, *Payload, error) { + payload, err := NewPayload(username, role, duration) if err != nil { return "", payload, err } diff --git a/token/jwt_maker_test.go b/token/jwt_maker_test.go index d8970b3..e42eb9b 100644 --- a/token/jwt_maker_test.go +++ b/token/jwt_maker_test.go @@ -14,12 +14,13 @@ func TestJWTMaker(t *testing.T) { require.NoError(t, err) username := util.RandomOwner() + role := util.DepositorRole duration := time.Minute issuedAt := time.Now() expiredAt := issuedAt.Add(duration) - token, payload, err := maker.CreateToken(username, duration) + token, payload, err := maker.CreateToken(username, role, duration) require.NoError(t, err) require.NotEmpty(t, token) @@ -30,6 +31,7 @@ func TestJWTMaker(t *testing.T) { require.NotZero(t, payload.ID) require.Equal(t, username, payload.Username) + require.Equal(t, role, payload.Role) require.WithinDuration(t, issuedAt, payload.IssuedAt, time.Second) require.WithinDuration(t, expiredAt, payload.ExpiredAt, time.Second) } @@ -38,7 +40,7 @@ func TestExpiredJWTToken(t *testing.T) { maker, err := NewJWTMaker(util.RandomString(32)) require.NoError(t, err) - token, payload, err := maker.CreateToken(util.RandomOwner(), -time.Minute) + token, payload, err := maker.CreateToken(util.RandomOwner(), util.DepositorRole, -time.Minute) require.NoError(t, err) require.NotEmpty(t, token) require.NotEmpty(t, payload) @@ -50,7 +52,7 @@ func TestExpiredJWTToken(t *testing.T) { } func TestInvalidJWTTokenAlgNone(t *testing.T) { - payload, err := NewPayload(util.RandomOwner(), time.Minute) + payload, err := NewPayload(util.RandomOwner(), util.DepositorRole, time.Minute) require.NoError(t, err) jwtToken := jwt.NewWithClaims(jwt.SigningMethodNone, payload) diff --git a/token/maker.go b/token/maker.go index 9454d7a..16cea0e 100644 --- a/token/maker.go +++ b/token/maker.go @@ -6,7 +6,7 @@ import "time" type Maker interface { //CreateToken creates a new token for a specific username and duration - CreateToken(username string, duration time.Duration) (string, *Payload, error) + CreateToken(username string, role string, duration time.Duration) (string, *Payload, error) // VerifyToken checks if the token is valid or not VerifyToken(token string) (*Payload, error) diff --git a/token/paseto_maker.go b/token/paseto_maker.go index ba8f6ef..29d19ac 100644 --- a/token/paseto_maker.go +++ b/token/paseto_maker.go @@ -27,9 +27,9 @@ func NewPasetoMaker(symmetricKey string) (Maker, error) { } // CreateToken creates a new token for a specific username and duration -func (maker *PasetoMaker) CreateToken(username string, duration time.Duration) (string, *Payload, error) { +func (maker *PasetoMaker) CreateToken(username string, role string, duration time.Duration) (string, *Payload, error) { - payload, err := NewPayload(username, duration) + payload, err := NewPayload(username, role, duration) if err != nil { return "", payload, err } diff --git a/token/paseto_maker_test.go b/token/paseto_maker_test.go index 6de5ce0..2da66c7 100644 --- a/token/paseto_maker_test.go +++ b/token/paseto_maker_test.go @@ -13,12 +13,13 @@ func TestPasetoMaker(t *testing.T) { require.NoError(t, err) username := util.RandomOwner() + role := util.DepositorRole duration := time.Minute issuedAt := time.Now() expiredAt := issuedAt.Add(duration) - token, payload, err := maker.CreateToken(username, duration) + token, payload, err := maker.CreateToken(username, role, duration) require.NoError(t, err) require.NotEmpty(t, token) @@ -29,6 +30,7 @@ func TestPasetoMaker(t *testing.T) { require.NotZero(t, payload.ID) require.Equal(t, username, payload.Username) + require.Equal(t, role, payload.Role) require.WithinDuration(t, issuedAt, payload.IssuedAt, time.Second) require.WithinDuration(t, expiredAt, payload.ExpiredAt, time.Second) } @@ -37,7 +39,7 @@ func TestExpiredPasetoToken(t *testing.T) { maker, err := NewPasetoMaker(util.RandomString(32)) require.NoError(t, err) - token, payload, err := maker.CreateToken(util.RandomOwner(), -time.Minute) + token, payload, err := maker.CreateToken(util.RandomOwner(), util.DepositorRole, -time.Minute) require.NoError(t, err) require.NotEmpty(t, token) require.NotEmpty(t, payload) diff --git a/token/payload.go b/token/payload.go index 61db72a..b879ae5 100644 --- a/token/payload.go +++ b/token/payload.go @@ -17,12 +17,13 @@ var ( type Payload struct { ID uuid.UUID `json:"id"` Username string `json:"username"` + Role string `json:"role"` IssuedAt time.Time `json:"issued_at"` ExpiredAt time.Time `json:"expired_at"` } // NewPayload creates a new token payload with a specific username and duration -func NewPayload(username string, duration time.Duration) (*Payload, error) { +func NewPayload(username string, role string, duration time.Duration) (*Payload, error) { tokenID, err := uuid.NewRandom() if err != nil { return nil, err @@ -31,6 +32,7 @@ func NewPayload(username string, duration time.Duration) (*Payload, error) { payload := &Payload{ ID: tokenID, Username: username, + Role: role, IssuedAt: time.Now(), ExpiredAt: time.Now().Add(duration), } diff --git a/util/role.go b/util/role.go new file mode 100644 index 0000000..de67cde --- /dev/null +++ b/util/role.go @@ -0,0 +1,6 @@ +package util + +const ( + DepositorRole = "depositor" + BankerRole = "banker" +)