github.com/ngocphuongnb/tetua@v0.0.7-alpha/app/auth/auth_test.go (about) 1 package auth_test 2 3 import ( 4 "context" 5 "errors" 6 "math" 7 "net/http" 8 "strings" 9 "testing" 10 "time" 11 12 "github.com/golang-jwt/jwt/v4" 13 "github.com/ngocphuongnb/tetua/app/auth" 14 "github.com/ngocphuongnb/tetua/app/cache" 15 "github.com/ngocphuongnb/tetua/app/config" 16 "github.com/ngocphuongnb/tetua/app/entities" 17 "github.com/ngocphuongnb/tetua/app/mock" 18 mockrepository "github.com/ngocphuongnb/tetua/app/mock/repository" 19 "github.com/ngocphuongnb/tetua/app/repositories" 20 "github.com/ngocphuongnb/tetua/app/server" 21 "github.com/ngocphuongnb/tetua/app/test" 22 ga "github.com/ngocphuongnb/tetua/packages/auth" 23 "github.com/ngocphuongnb/tetua/packages/fiberserver" 24 fiber "github.com/ngocphuongnb/tetua/packages/fiberserver" 25 "github.com/stretchr/testify/assert" 26 ) 27 28 func init() { 29 mock.CreateRepositories() 30 repositories.User.Create(context.Background(), mock.RootUser) 31 cache.Roles = []*entities.Role{auth.ROLE_ADMIN, auth.ROLE_USER, auth.ROLE_GUEST} 32 } 33 34 func TestProvider(t *testing.T) { 35 providers := map[string]auth.NewProviderFn{ 36 "local": ga.NewLocal, 37 "mock1": mock.NewAuth, 38 "mock2": mock.NewAuth, 39 } 40 providerMap := map[string]map[string]string{ 41 "local": nil, 42 "mock1": nil, 43 "mock2": nil, 44 } 45 46 config.Auth = nil 47 auth.New(providers) 48 assert.Equal(t, 0, len(auth.Providers())) 49 50 config.Auth = &config.AuthConfig{ 51 EnabledProviders: []string{"local"}, 52 Providers: providerMap, 53 } 54 auth.New(providers) 55 assert.Equal(t, 1, len(auth.Providers())) 56 57 config.Auth = &config.AuthConfig{ 58 EnabledProviders: []string{"local", "mock1", "mock2"}, 59 Providers: providerMap, 60 } 61 auth.New(providers) 62 assert.Equal(t, 2, len(auth.Providers())) 63 assert.Equal(t, providers["mock1"](nil).Name(), auth.GetProvider("mock").Name()) 64 assert.Equal(t, nil, auth.GetProvider("invalid_provider")) 65 } 66 67 func TestActionConfigs(t *testing.T) { 68 testConfig := auth.Config(&server.AuthConfig{ 69 Action: "test", 70 Value: entities.PERM_ALL, 71 DefaultValue: entities.PERM_ALL, 72 }) 73 74 assert.Equal(t, testConfig, auth.GetAuthConfig("test")) 75 assert.Equal(t, (*server.AuthConfig)(nil), auth.GetAuthConfig("test2")) 76 77 defer test.RecoverPanic(t, "Duplicate action config: test", "duplicate action") 78 auth.Config(&server.AuthConfig{ 79 Action: "test", 80 Value: entities.PERM_ALL, 81 DefaultValue: entities.PERM_ALL, 82 }) 83 } 84 85 func TestHelpers(t *testing.T) { 86 ctx := &fiberserver.Context{} 87 role1Permissions := []*entities.PermissionValue{{ 88 Action: "test", 89 Value: entities.PERM_ALL, 90 }} 91 cache.RolesPermissions = []*entities.RolePermissions{{ 92 RoleID: 1, 93 Permissions: role1Permissions, 94 }} 95 96 assert.Equal(t, role1Permissions, auth.GetRolePermissions(1).Permissions) 97 assert.Equal(t, []*entities.PermissionValue{}, auth.GetRolePermissions(2).Permissions) 98 assert.Equal(t, role1Permissions[0], auth.GetRolePermission(1, "test")) 99 assert.Equal(t, &entities.PermissionValue{}, auth.GetRolePermission(1, "test2")) 100 assert.Equal(t, []*entities.Role{cache.Roles[0]}, auth.GetRolesFromIDs([]int{1})) 101 assert.Equal(t, []*entities.Role{}, auth.GetRolesFromIDs([]int{4})) 102 assert.Equal(t, true, auth.AllowAll(ctx)) 103 assert.Equal(t, false, auth.AllowNone(ctx)) 104 105 s := mock.CreateServer() 106 107 s.Get("/test", func(c server.Context) error { 108 assert.Equal(t, false, auth.AllowLoggedInUser(c)) 109 c.Locals("user", &entities.User{ID: 1}) 110 assert.Equal(t, true, auth.AllowLoggedInUser(c)) 111 err := auth.GetFile(c) 112 assert.Equal(t, true, entities.IsNotFound(err)) 113 return nil 114 }) 115 116 mock.GetRequest(s, "/test") 117 repositories.File.Create(context.Background(), &entities.File{ 118 ID: 1, 119 UserID: 1, 120 }) 121 122 s.Get("/files/:id", func(c server.Context) error { 123 err := auth.GetFile(c) 124 125 if c.Param("id") == "new" { 126 assert.Equal(t, true, auth.FileOwnerCheck(c)) 127 } 128 129 if c.ParamInt("id") > 1 { 130 assert.Equal(t, true, entities.IsNotFound(err)) 131 assert.Equal(t, nil, c.Locals("file")) 132 } else if c.ParamInt("id") == 1 { 133 assert.Equal(t, nil, err) 134 135 if f, ok := c.Locals("file").(*entities.File); ok { 136 assert.Equal(t, 1, f.ID) 137 } else { 138 assert.Fail(t, "local file is not a file") 139 } 140 141 c.Locals("user", nil) 142 assert.Equal(t, false, auth.FileOwnerCheck(c)) 143 144 c.Locals("user", &entities.User{ID: 2}) 145 assert.Equal(t, false, auth.FileOwnerCheck(c)) 146 147 c.Locals("user", &entities.User{ID: 1}) 148 assert.Equal(t, true, auth.FileOwnerCheck(c)) 149 150 c.Locals("file", 1) 151 assert.Equal(t, false, auth.FileOwnerCheck(c)) 152 } 153 154 return nil 155 }) 156 157 mock.GetRequest(s, "/files/new") 158 mock.GetRequest(s, "/files/1") 159 mock.GetRequest(s, "/files/2") 160 161 repositories.Post.Create(context.Background(), &entities.Post{ 162 ID: 1, 163 Name: "post 1", 164 UserID: 1, 165 }) 166 repositories.Post.Create(context.Background(), &entities.Post{ 167 ID: 2, 168 Name: "post 2", 169 UserID: 2, 170 }) 171 172 s.Get("/posts/:id", func(c server.Context) error { 173 err := auth.GetPost(c) 174 175 if c.Param("id") == "new" { 176 assert.Equal(t, nil, err) 177 assert.Equal(t, nil, c.Locals("post")) 178 assert.Equal(t, true, auth.PostOwnerCheck(c)) 179 } else if c.ParamInt("id") == 1 { 180 assert.Equal(t, nil, err) 181 182 if p, ok := c.Locals("post").(*entities.Post); ok { 183 assert.Equal(t, 1, p.ID) 184 } else { 185 assert.Fail(t, "local post is not a post") 186 } 187 188 c.Locals("user", &entities.User{ID: 1}) 189 assert.Equal(t, true, auth.PostOwnerCheck(c)) 190 191 c.Locals("user", &entities.User{ID: 2}) 192 assert.Equal(t, false, auth.PostOwnerCheck(c)) 193 194 c.Locals("user", nil) 195 assert.Equal(t, false, auth.PostOwnerCheck(c)) 196 } else if c.ParamInt("id") > 2 { 197 assert.Equal(t, true, entities.IsNotFound(err)) 198 assert.Equal(t, nil, c.Locals("post")) 199 } 200 201 return nil 202 }) 203 204 mock.GetRequest(s, "/posts/new") 205 mock.GetRequest(s, "/posts/1") 206 mock.GetRequest(s, "/posts/2") 207 mock.GetRequest(s, "/posts/3") 208 209 repositories.Comment.Create(context.Background(), &entities.Comment{ 210 ID: 1, 211 UserID: 1, 212 }) 213 214 s.Get("/comments/:id", func(c server.Context) error { 215 err := auth.GetComment(c) 216 217 if c.Param("id") == "new" { 218 assert.Equal(t, nil, err) 219 assert.Equal(t, nil, c.Locals("comment")) 220 assert.Equal(t, true, auth.CommentOwnerCheck(c)) 221 } else if c.ParamInt("id") == 1 { 222 assert.Equal(t, nil, err) 223 224 if c, ok := c.Locals("comment").(*entities.Comment); ok { 225 assert.Equal(t, 1, c.ID) 226 } else { 227 assert.Fail(t, "local comment is not a comment") 228 } 229 230 c.Locals("user", &entities.User{ID: 1}) 231 assert.Equal(t, true, auth.CommentOwnerCheck(c)) 232 233 c.Locals("user", &entities.User{ID: 2}) 234 assert.Equal(t, false, auth.CommentOwnerCheck(c)) 235 236 c.Locals("user", nil) 237 assert.Equal(t, false, auth.CommentOwnerCheck(c)) 238 239 c.Locals("comment", nil) 240 assert.Equal(t, false, auth.CommentOwnerCheck(c)) 241 242 } else { 243 assert.Equal(t, true, entities.IsNotFound(err)) 244 assert.Equal(t, nil, c.Locals("comment")) 245 } 246 247 return nil 248 }) 249 250 mock.GetRequest(s, "/comments/new") 251 mock.GetRequest(s, "/comments/0") 252 mock.GetRequest(s, "/comments/1") 253 mock.GetRequest(s, "/comments/2") 254 } 255 256 func TestRoutes(t *testing.T) { 257 logger := mock.CreateLogger() 258 s := mock.CreateServer() 259 auth.Routes(s) 260 261 body, resp := mock.GetRequest(s, "/auth/invalid_provider") 262 assert.Equal(t, http.StatusNotFound, resp.StatusCode) 263 assert.Equal(t, "Invalid provider", string(body)) 264 265 body, resp = mock.GetRequest(s, "/auth/mock") 266 267 assert.Equal(t, true, strings.HasPrefix(resp.Header["Location"][0], "https://auth.mock-service.local/auth")) 268 assert.Equal(t, http.StatusFound, resp.StatusCode) 269 270 body, _ = mock.GetRequest(s, "/auth/mock/callback") 271 assert.Equal(t, "Something went wrong", string(body)) 272 assert.Equal(t, []*mock.MockLoggerMessage{{ 273 Type: "Error", 274 Params: []interface{}{errors.New("code is empty")}, 275 }}, logger.Messages) 276 277 _, resp = mock.GetRequest(s, "/auth/mock/callback?code=mock_callback_code") 278 assert.Equal(t, http.StatusFound, resp.StatusCode) 279 assert.Equal(t, "/", resp.Header["Location"][0]) 280 281 header := http.Header{} 282 header.Add("Cookie", strings.Split(resp.Header["Set-Cookie"][0], ";")[0]) 283 request := http.Request{Header: header} 284 cookies := request.Cookies() 285 token, err := jwt.ParseWithClaims( 286 cookies[0].Value, 287 &entities.UserJwtClaims{}, 288 func(token *jwt.Token) (interface{}, error) { 289 return []byte(config.APP_KEY), nil 290 }, 291 ) 292 293 assert.Equal(t, nil, err) 294 claims, ok := token.Claims.(*entities.UserJwtClaims) 295 assert.Equal(t, true, ok) 296 assert.Equal(t, true, token.Valid) 297 assert.Equal(t, mock.NormalUser2.ID, claims.User.ID) 298 assert.Equal(t, mock.NormalUser2.Provider, claims.User.Provider) 299 assert.Equal(t, mock.NormalUser2.Username, claims.User.Username) 300 assert.Equal(t, mock.NormalUser2.Email, claims.User.Email) 301 assert.Equal(t, mock.NormalUser2.DisplayName, claims.User.DisplayName) 302 assert.Equal(t, mock.NormalUser2.RoleIDs, claims.User.RoleIDs) 303 assert.Equal(t, mock.NormalUser2.AvatarImageUrl, claims.User.AvatarImageUrl) 304 305 s2 := fiber.New(fiber.Config{JwtSigningKey: config.APP_KEY}) 306 s2.Use(func(c server.Context) error { 307 c.Locals("jwt_header", map[string]interface{}{ 308 "invalid_header": math.Inf(1), 309 }) 310 return c.Next() 311 }) 312 auth.Routes(s2) 313 314 _, resp = mock.GetRequest(s2, "/auth/mock/callback?code=mock_callback_code") 315 assert.Equal(t, "Error setting login info", logger.Messages[1].Params[0]) 316 assert.Equal(t, http.StatusBadGateway, resp.StatusCode) 317 318 mockrepository.ErrorCreateIfNotExistsByProvider = true 319 320 body, _ = mock.GetRequest(s, "/auth/mock/callback?code=mock_callback_code") 321 assert.Equal(t, "Something went wrong", string(body)) 322 assert.Equal(t, mock.MockLoggerMessage{ 323 Type: "Error", 324 Params: []interface{}{errors.New("CreateIfNotExistsByProvider error")}, 325 }, logger.Last()) 326 327 mockrepository.ErrorCreateIfNotExistsByProvider = false 328 } 329 330 func TestAssignUserInfo(t *testing.T) { 331 logger := mock.CreateLogger() 332 s := mock.CreateServer() 333 s.Use(auth.AssignUserInfo) 334 335 s.Get("/nocookie", func(c server.Context) error { 336 assert.Equal(t, auth.GUEST_USER, c.User()) 337 return nil 338 }) 339 mock.GetRequest(s, "/nocookie") 340 341 s.Get("/invalidcookie", func(c server.Context) error { 342 assert.Equal(t, auth.GUEST_USER, c.User()) 343 return nil 344 }) 345 346 mock.GetRequest(s, "/invalidcookie", map[string]string{ 347 "cookie": config.APP_TOKEN_KEY + "=aaaa", 348 }) 349 assert.Equal(t, jwt.NewValidationError("token contains an invalid number of segments", 0x1), logger.Messages[0].Params[0]) 350 351 s.Get("/validcookie", func(c server.Context) error { 352 assert.Equal(t, &entities.User{ 353 ID: mock.NormalUser2.ID, 354 Provider: mock.NormalUser2.Provider, 355 Username: mock.NormalUser2.Username, 356 Email: mock.NormalUser2.Email, 357 DisplayName: mock.NormalUser2.DisplayName, 358 RoleIDs: mock.NormalUser2.RoleIDs, 359 Active: mock.NormalUser2.Active, 360 Roles: auth.GetRolesFromIDs(mock.NormalUser2.RoleIDs), 361 AvatarImageUrl: mock.NormalUser2.Avatar(), 362 }, c.User()) 363 return nil 364 }) 365 366 exp := time.Now().Add(time.Hour * 100 * 365 * 24) 367 jwtToken, _ := mock.NormalUser2.JwtClaim(exp) 368 mock.GetRequest(s, "/validcookie", map[string]string{ 369 "cookie": config.APP_TOKEN_KEY + "=" + jwtToken, 370 }) 371 assert.Equal(t, jwt.NewValidationError("token contains an invalid number of segments", 0x1), logger.Messages[0].Params[0]) 372 } 373 374 func TestAuthCheck(t *testing.T) { 375 s := mock.CreateServer() 376 s.Use(auth.Check) 377 378 s.Get("/noauthconfig", func(c server.Context) error { 379 return c.SendString("noauthconfig") 380 }) 381 body, _ := mock.GetRequest(s, "/noauthconfig") 382 assert.Equal(t, "noauthconfig", string(body)) 383 384 s.Get("/withauthconfigprepareerror", func(c server.Context) error { 385 return c.SendString("withauthconfigprepareerror") 386 }, auth.Config(&server.AuthConfig{ 387 Action: "withauthconfigprepareerror", 388 DefaultValue: entities.PERM_ALL, 389 Prepare: func(c server.Context) error { 390 return errors.New("prepare error") 391 }, 392 })) 393 394 exp := time.Now().Add(time.Hour * 100 * 365 * 24) 395 jwtToken, _ := mock.NormalUser2.JwtClaim(exp) 396 validAuthHeader := map[string]string{"cookie": config.APP_TOKEN_KEY + "=" + jwtToken} 397 body, resp := mock.GetRequest(s, "/withauthconfigprepareerror", validAuthHeader) 398 assert.Equal(t, "prepare error", body) 399 assert.Equal(t, http.StatusInternalServerError, resp.StatusCode) 400 } 401 402 func createServerWithAuthConfig(action string) server.Server { 403 s := mock.CreateServer() 404 s.Get("/posts/:id", func(c server.Context) error { 405 return c.SendString("View post: " + c.Param("id")) 406 }, auth.Config(&server.AuthConfig{ 407 Action: action, 408 DefaultValue: entities.PERM_NONE, 409 Prepare: func(c server.Context) error { 410 return auth.GetPost(c) 411 }, 412 OwnCheckFN: auth.PostOwnerCheck, 413 })) 414 return s 415 } 416 417 func TestGuestViewPostAllActionConfigs(t *testing.T) { 418 var body = "" 419 var s server.Server 420 var resp *http.Response 421 422 s = createServerWithAuthConfig("guest.post.view.perm_none") 423 _, resp = mock.GetRequest(s, "/posts/3") 424 assert.Equal(t, http.StatusNotFound, resp.StatusCode) 425 426 _, resp = mock.GetRequest(s, "/posts/1") 427 assert.Equal(t, http.StatusFound, resp.StatusCode) 428 assert.Equal(t, "/login?back=%2Fposts%2F1", resp.Header["Location"][0]) 429 430 s = createServerWithAuthConfig("guest.post.view.perm_own") 431 _, resp = mock.GetRequest(s, "/posts/1") 432 assert.Equal(t, http.StatusFound, resp.StatusCode) 433 assert.Equal(t, "/login?back=%2Fposts%2F1", resp.Header["Location"][0]) 434 435 cache.RolesPermissions = []*entities.RolePermissions{{ 436 RoleID: 3, // Guest 437 Permissions: []*entities.PermissionValue{{ 438 Action: "guest.post.view.perm_all", 439 Value: entities.PERM_ALL, 440 }}, 441 }} 442 s = createServerWithAuthConfig("guest.post.view.perm_all") 443 body, resp = mock.GetRequest(s, "/posts/1") 444 assert.Equal(t, http.StatusOK, resp.StatusCode) 445 assert.Equal(t, "View post: 1", body) 446 } 447 448 func TestInactiveUser(t *testing.T) { 449 var s server.Server 450 var resp *http.Response 451 exp := time.Now().Add(time.Hour * 100 * 365 * 24) 452 453 mock.NormalUser2.Active = false 454 jwtTokenNormalUser2, _ := mock.NormalUser2.JwtClaim(exp) 455 authHeaderNormalUser2 := map[string]string{"cookie": config.APP_TOKEN_KEY + "=" + jwtTokenNormalUser2} 456 457 s = createServerWithAuthConfig("inactiveuser.post.view.perm_none") 458 cache.RolesPermissions = []*entities.RolePermissions{{ 459 RoleID: 2, // User 460 Permissions: []*entities.PermissionValue{{ 461 Action: "inactiveuser.post.view.perm_none", 462 Value: entities.PERM_NONE, 463 }}, 464 }} 465 466 _, resp = mock.GetRequest(s, "/posts/2", authHeaderNormalUser2) 467 assert.Equal(t, http.StatusFound, resp.StatusCode) 468 assert.Equal(t, "/inactive", resp.Header["Location"][0]) 469 mock.NormalUser2.Active = true 470 } 471 472 func TestUserViewPostAllActionConfigs(t *testing.T) { 473 var body = "" 474 var s server.Server 475 var resp *http.Response 476 exp := time.Now().Add(time.Hour * 100 * 365 * 24) 477 478 jwtTokenNormalUser2, _ := mock.NormalUser2.JwtClaim(exp) 479 authHeaderNormalUser2 := map[string]string{"cookie": config.APP_TOKEN_KEY + "=" + jwtTokenNormalUser2} 480 481 jwtTokenNormalUser3, _ := mock.NormalUser3.JwtClaim(exp) 482 authHeaderNormalUser3 := map[string]string{"cookie": config.APP_TOKEN_KEY + "=" + jwtTokenNormalUser3} 483 484 // Action that allow no one to access 485 s = createServerWithAuthConfig("user.post.view.perm_none") 486 cache.RolesPermissions = []*entities.RolePermissions{{ 487 RoleID: 2, // User 488 Permissions: []*entities.PermissionValue{{ 489 Action: "user.post.view.perm_none", 490 Value: entities.PERM_NONE, 491 }}, 492 }} 493 494 // Access from user who is the post owner 495 body, resp = mock.GetRequest(s, "/posts/2", authHeaderNormalUser2) 496 assert.Equal(t, http.StatusForbidden, resp.StatusCode) 497 assert.Equal(t, "Insufficient permission", body) 498 499 // Access from user who is not the post owner 500 body, resp = mock.GetRequest(s, "/posts/2", authHeaderNormalUser3) 501 assert.Equal(t, http.StatusForbidden, resp.StatusCode) 502 assert.Equal(t, "Insufficient permission", body) 503 504 // Action that only allow the owner to access 505 s = createServerWithAuthConfig("user.post.view.perm_own") 506 cache.RolesPermissions = []*entities.RolePermissions{{ 507 RoleID: 2, // User 508 Permissions: []*entities.PermissionValue{{ 509 Action: "user.post.view.perm_own", 510 Value: entities.PERM_OWN, 511 }}, 512 }} 513 514 // Access from user who is not the post owner 515 body, resp = mock.GetRequest(s, "/posts/2", authHeaderNormalUser3) 516 assert.Equal(t, http.StatusForbidden, resp.StatusCode) 517 assert.Equal(t, "Insufficient permission", body) 518 519 // Access from user who is the post owner 520 body, resp = mock.GetRequest(s, "/posts/2", authHeaderNormalUser2) 521 522 assert.Equal(t, http.StatusOK, resp.StatusCode) 523 assert.Equal(t, "View post: 2", body) 524 525 // Action that allow all users to access 526 s = createServerWithAuthConfig("user.post.view.perm_all") 527 cache.RolesPermissions = []*entities.RolePermissions{{ 528 RoleID: 2, // User 529 Permissions: []*entities.PermissionValue{{ 530 Action: "user.post.view.perm_all", 531 Value: entities.PERM_ALL, 532 }}, 533 }} 534 535 // Access from user who is not the post owner 536 body, resp = mock.GetRequest(s, "/posts/2", authHeaderNormalUser3) 537 assert.Equal(t, http.StatusOK, resp.StatusCode) 538 assert.Equal(t, "View post: 2", body) 539 540 // Access from user who is the post owner 541 body, resp = mock.GetRequest(s, "/posts/2", authHeaderNormalUser2) 542 assert.Equal(t, http.StatusOK, resp.StatusCode) 543 assert.Equal(t, "View post: 2", body) 544 } 545 546 func TestRootUserAllActionConfigs(t *testing.T) { 547 var body = "" 548 var s server.Server 549 var resp *http.Response 550 exp := time.Now().Add(time.Hour * 100 * 365 * 24) 551 552 jwtTokenRootUser, _ := mock.RootUser.JwtClaim(exp) 553 authHeaderRootUser := map[string]string{"cookie": config.APP_TOKEN_KEY + "=" + jwtTokenRootUser} 554 555 s = createServerWithAuthConfig("root.post.view.perm_none") 556 557 body, resp = mock.GetRequest(s, "/posts/2", authHeaderRootUser) 558 assert.Equal(t, http.StatusOK, resp.StatusCode) 559 assert.Equal(t, "View post: 2", body) 560 }