بررسی مجوزها
اکنون که PermissionModel ما راهاندازی شده است، بیایید بررسی کنیم چگونه میتوانیم از آن برای محدود کردن دسترسی به endpointهای API خود استفاده کنیم.
از نظر مفهومی، کاری که اینجا باید انجام دهیم خیلی پیچیده نیست.
- یک middleware جدید به نام
requirePermission()میسازیم که یک کد مجوز خاص مانند"movies:read"را به عنوان آرگومان دریافت میکند. - در این middleware، کاربر فعلی را از context درخواست بازیابی کرده و متد
app.models.Permissions.GetAllForUser()(که همین الان ساختیم) را فراخوانی میکنیم تا لیست مجوزهای او را دریافت کنیم. - سپس بررسی میکنیم آیا لیست حاوی کد مجوز مورد نیاز است یا خیر. اگر نباشد، باید پاسخ
403 Forbiddenبه client ارسال کنیم.
برای پیادهسازی این مورد، ابتدا یک تابع کمکی جدید notPermittedResponse() برای ارسال پاسخ 403 Forbidden میسازیم. به این صورت:
package main ... func (app *application) notPermittedResponse(w http.ResponseWriter, r *http.Request) { message := "your user account doesn't have the necessary permissions to access this resource" app.errorResponse(w, r, http.StatusForbidden, message) }
سپس به فایل cmd/api/middleware.go میرویم و middleware جدید requirePermission() را ایجاد میکنیم.
این را به صورتی تنظیم میکنیم که middleware requirePermission() به طور خودکار middleware موجود requireActivatedUser() ما را در بر بگیرد، که به نوبه خود — فراموش نکنید — middleware requireAuthenticatedUser() ما را در بر میگیرد.
این مهم است — یعنی وقتی از middleware requirePermission() استفاده میکنیم، در واقع سه بررسی انجام میدهیم که مجموعاً تضمین میکنند درخواست از یک کاربر احراز هویت شده (غیرناشناس) و فعال است، که یک مجوز خاص دارد.
بیایید این را در فایل cmd/api/middleware.go به این صورت ایجاد کنیم:
package main ... // Note that the first parameter for the middleware function is the permission code that // we require the user to have. func (app *application) requirePermission(code string, next http.HandlerFunc) http.HandlerFunc { fn := func(w http.ResponseWriter, r *http.Request) { // Retrieve the user from the request context. user := app.contextGetUser(r) // Get the slice of permissions for the user. permissions, err := app.models.Permissions.GetAllForUser(user.ID) if err != nil { app.serverErrorResponse(w, r, err) return } // Check if the slice includes the required permission. If it doesn't, then // return a 403 Forbidden response. if !permissions.Include(code) { app.notPermittedResponse(w, r) return } // Otherwise they have the required permission so we call the next handler in // the chain. next.ServeHTTP(w, r) } // Wrap this with the requireActivatedUser() middleware before returning it. return app.requireActivatedUser(fn) }
پس از اتمام کار، مرحله نهایی بهروزرسانی فایل cmd/api/routes.go برای استفاده از middleware جدید روی endpointهای مورد نیاز است.
مسیرها را بهروزرسانی کنید به طوری که API ما مجوز "movies:read" را برای endpointهایی که داده فیلم را واکشی میکنند و مجوز "movies:write" را برای endpointهایی که فیلم ایجاد، ویرایش یا حذف میکنند، الزامی کند.
package main ... func (app *application) routes() http.Handler { router := httprouter.New() router.NotFound = http.HandlerFunc(app.notFoundResponse) router.MethodNotAllowed = http.HandlerFunc(app.methodNotAllowedResponse) router.HandlerFunc(http.MethodGet, "/v1/healthcheck", app.healthcheckHandler) // Use the requirePermission() middleware on each of the /v1/movies** endpoints, // passing in the required permission code as the first parameter. router.HandlerFunc(http.MethodGet, "/v1/movies", app.requirePermission("movies:read", app.listMoviesHandler)) router.HandlerFunc(http.MethodPost, "/v1/movies", app.requirePermission("movies:write", app.createMovieHandler)) router.HandlerFunc(http.MethodGet, "/v1/movies/:id", app.requirePermission("movies:read", app.showMovieHandler)) router.HandlerFunc(http.MethodPatch, "/v1/movies/:id", app.requirePermission("movies:write", app.updateMovieHandler)) router.HandlerFunc(http.MethodDelete, "/v1/movies/:id", app.requirePermission("movies:write", app.deleteMovieHandler)) router.HandlerFunc(http.MethodPost, "/v1/users", app.registerUserHandler) router.HandlerFunc(http.MethodPut, "/v1/users/activated", app.activateUserHandler) router.HandlerFunc(http.MethodPost, "/v1/tokens/authentication", app.createAuthenticationTokenHandler) return app.recoverPanic(app.rateLimit(app.authenticate(router))) }
نمایش عملی
نمایش این قابلیت در عمل کمی دشوار است زیرا اگر از ابتدا همراه بوده باشید، هیچیک از کاربران فعلی پایگاه داده ما مجوزی برای آنها تنظیم نشده است.
برای کمک به نمایش این قابلیت جدید، psql را باز کرده و برخی مجوزها را اضافه میکنیم. به طور خاص، ما:
- کاربر
alice@example.comرا فعال میکنیم. - به همه کاربران مجوز
"movies:read"را میدهیم. - به کاربر
faith@example.comمجوز"movies:write"را میدهیم.
اگر همراه ما هستید، پرامپت psql خود را باز کنید:
$ psql $GREENLIGHT_DB_DSN Password for user greenlight: psql (15.4 (Ubuntu 15.4-1.pgdg22.04+1)) SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off) Type "help" for help. greenlight=>
و دستورات زیر را اجرا کنید:
-- Set the activated field for alice@example.com to true. UPDATE users SET activated = true WHERE email = 'alice@example.com'; -- Give all users the 'movies:read' permission INSERT INTO users_permissions SELECT id, (SELECT id FROM permissions WHERE code = 'movies:read') FROM users; -- Give faith@example.com the 'movies:write' permission INSERT INTO users_permissions VALUES ( (SELECT id FROM users WHERE email = 'faith@example.com'), (SELECT id FROM permissions WHERE code = 'movies:write') ); -- List all activated users and their permissions. SELECT email, array_agg(permissions.code) as permissions FROM permissions INNER JOIN users_permissions ON users_permissions.permission_id = permissions.id INNER JOIN users ON users_permissions.user_id = users.id WHERE users.activated = true GROUP BY email;
پس از اتمام، باید لیستی از کاربران فعال فعلی و مجوزهایشان را ببینید، مشابه این:
email | permissions
-------------------+----------------------------
alice@example.com | {movies:read}
faith@example.com | {movies:read,movies:write}
(2 rows)
اکنون که کاربران ما مجوزهایی دریافت کردهاند، آماده آزمایش هستیم.
برای شروع، بیایید چند درخواست به عنوان alice@example.com به endpointهای GET /v1/movies/1 و DELETE /v1/movies/1 ارسال کنیم. درخواست اول باید به درستی کار کند، اما درخواست دوم باید ناموفق باشد زیرا کاربر مجوز لازم movies:write را ندارد.
$ BODY='{"email": "alice@example.com", "password": "pa55word"}'
$ curl -d "$BODY" localhost:4000/v1/tokens/authentication
{
"authentication_token": {
"token": "OPFXEPOYZWMGNWXWKMYIMEGATU",
"expiry": "2021-04-17T20:49:39.963768416+02:00"
}
}
$ curl -H "Authorization: Bearer OPFXEPOYZWMGNWXWKMYIMEGATU" localhost:4000/v1/movies/1
{
"movie": {
"id": 1,
"title": "Moana",
"year": 2016,
"runtime": "107 mins",
"genres": [
"animation",
"adventure"
],
"version": 1
}
}
$ curl -X DELETE -H "Authorization: Bearer OPFXEPOYZWMGNWXWKMYIMEGATU" localhost:4000/v1/movies/1
{
"error": "your user account doesn't have the necessary permissions to access this resource"
}
عالی است، دقیقاً مطابق انتظار کار میکند — عملیات DELETE مسدود شده است زیرا alice@example.com مجوز لازم movies:write را ندارد.
در مقابل، بیایید همان عملیات را اما با کاربر faith@example.com امتحان کنیم. این بار عملیات DELETE باید به درستی کار کند، به این صورت:
$ BODY='{"email": "faith@example.com", "password": "pa55word"}'
$ curl -d "$BODY" localhost:4000/v1/tokens/authentication
{
"authentication_token": {
"token": "E42XD5OBBBO4MPUPYGLLY2GURE",
"expiry": "2021-04-17T20:51:14.924813208+02:00"
}
}
$ curl -X DELETE -H "Authorization: Bearer E42XD5OBBBO4MPUPYGLLY2GURE" localhost:4000/v1/movies/1
{
"message": "movie successfully deleted"
}