Let's Go Further مجوز مبتنی بر نقش › بررسی مجوزها
قبلی · فهرست مطالب · بعدی
فصل ۱۶.۴.

بررسی مجوزها

اکنون که PermissionModel ما راه‌اندازی شده است، بیایید بررسی کنیم چگونه می‌توانیم از آن برای محدود کردن دسترسی به endpointهای API خود استفاده کنیم.

از نظر مفهومی، کاری که اینجا باید انجام دهیم خیلی پیچیده نیست.

برای پیاده‌سازی این مورد، ابتدا یک تابع کمکی جدید notPermittedResponse() برای ارسال پاسخ 403 Forbidden می‌سازیم. به این صورت:

فایل: cmd/api/errors.go
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 به این صورت ایجاد کنیم:

فایل: 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هایی که فیلم ایجاد، ویرایش یا حذف می‌کنند، الزامی کند.

فایل: cmd/api/routes.go
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 را باز کرده و برخی مجوزها را اضافه می‌کنیم. به طور خاص، ما:

اگر همراه ما هستید، پرامپت 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"
}