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

اعطای مجوزها

مدل مجوزها و middleware احراز هویت ما اکنون به خوبی کار می‌کنند. اما — در حال حاضر — وقتی یک کاربر جدید حساب کاربری ثبت‌نام می‌کند، هیچ مجوزی ندارد. در این فصل قصد داریم این را تغییر دهیم تا کاربران جدید به طور خودکار مجوز "movies:read" را به صورت پیش‌فرض دریافت کنند.

به‌روزرسانی مدل مجوزها

برای اعطای مجوز به یک کاربر، باید PermissionModel خود را به‌روزرسانی کنیم تا شامل متد AddForUser() باشد، که یک یا چند کد مجوز برای یک کاربر مشخص را به دیتابیس اضافه می‌کند. ایده این است که بتوانیم آن را در handler‌های خود به این صورت استفاده کنیم:

// Add the "movies:read" and "movies:write" permissions for the user with ID = 2.
app.models.Permissions.AddForUser(2, "movies:read", "movies:write")

در پشت صحنه، دستور SQL که برای درج این داده نیاز داریم به این صورت است:

INSERT INTO users_permissions
SELECT $1, permissions.id FROM permissions WHERE permissions.code = ANY($2)

در این query، پارامتر $1 شناسه کاربر خواهد بود و پارامتر $2 یک آرایه PostgreSQL از کدهای مجوزی است که می‌خواهیم برای کاربر اضافه کنیم، مانند {'movies:read', 'movies:write'}.

بنابراین آنچه اینجا اتفاق می‌افتد این است که دستور SELECT ... در خط دوم یک جدول 'موقت' با سطرهایی تشکیل می‌دهد که شامل شناسه کاربر و شناسه‌های مربوط به کدهای مجوز در آرایه هستند. سپس محتوای این جدول موقت را در جدول user_permissions خود درج می‌کنیم.

بیایید متد AddForUser() را در فایل internal/data/permissions.go ایجاد کنیم:

فایل: internal/data/permissions.go
package data

import (
    "context"
    "database/sql"
    "time"

    "github.com/lib/pq" // New import
)

...

// Add the provided permission codes for a specific user. Notice that we're using a 
// variadic parameter for the codes so that we can assign multiple permissions in a 
// single call.
func (m PermissionModel) AddForUser(userID int64, codes ...string) error {
    query := `
        INSERT INTO users_permissions
        SELECT $1, permissions.id FROM permissions WHERE permissions.code = ANY($2)`

    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    _, err := m.DB.ExecContext(ctx, query, userID, pq.Array(codes))
    return err
}

به‌روزرسانی handler ثبت‌نام

اکنون که این بخش آماده است، بیایید registerUserHandler خود را به‌روزرسانی کنیم تا کاربران جدید به طور خودکار مجوز movies:read را هنگام ثبت‌نام دریافت کنند. به این صورت:

فایل: cmd/api/users.go
package main

...

func (app *application) registerUserHandler(w http.ResponseWriter, r *http.Request) {
    var input struct {
        Name     string `json:"name"`
        Email    string `json:"email"`
        Password string `json:"password"`
    }

    err := app.readJSON(w, r, &input)
    if err != nil {
        app.badRequestResponse(w, r, err)
        return
    }

    user := &data.User{
        Name:      input.Name,
        Email:     input.Email,
        Activated: false,
    }

    err = user.Password.Set(input.Password)
    if err != nil {
        app.serverErrorResponse(w, r, err)
        return
    }

    v := validator.New()

    if data.ValidateUser(v, user); !v.Valid() {
        app.failedValidationResponse(w, r, v.Errors)
        return
    }

    err = app.models.Users.Insert(user)
    if err != nil {
        switch {
        case errors.Is(err, data.ErrDuplicateEmail):
            v.AddError("email", "a user with this email address already exists")
            app.failedValidationResponse(w, r, v.Errors)
        default:
            app.serverErrorResponse(w, r, err)
        }
        return
    }

    // Add the "movies:read" permission for the new user.
    err = app.models.Permissions.AddForUser(user.ID, "movies:read")
    if err != nil {
        app.serverErrorResponse(w, r, err)
        return
    }

    token, err := app.models.Tokens.New(user.ID, 3*24*time.Hour, data.ScopeActivation)
    if err != nil {
        app.serverErrorResponse(w, r, err)
        return
    }

    app.background(func() {
        data := map[string]any{
            "activationToken": token.Plaintext,
            "userID":          user.ID,
        }

        err := app.mailer.Send(user.Email, "user_welcome.tmpl", data)
        if err != nil {
            app.logger.Error(err.Error())
        }
    })

    err = app.writeJSON(w, http.StatusAccepted, envelope{"user": user}, nil)
    if err != nil {
        app.serverErrorResponse(w, r, err)
    }
}

...

بیایید بررسی کنیم که این به درستی کار می‌کند با ثبت‌نام یک کاربر کاملاً جدید با آدرس ایمیل grace@example.com:

$ BODY='{"name": "Grace Smith", "email": "grace@example.com", "password": "pa55word"}'
$ curl -d "$BODY" localhost:4000/v1/users
{
    "user": {
        "id": 8,
        "created_at": "2021-04-16T21:32:56+02:00",
        "name": "Grace Smith",
        "email": "grace@example.com",
        "activated": false
    }
}

اگر psql را باز کنید، باید بتوانید ببینید که این کاربر مجوز movies:read را دارد با اجرای query SQL زیر:

SELECT email, code FROM users 
INNER JOIN users_permissions ON users.id = users_permissions.user_id
INNER JOIN permissions ON users_permissions.permission_id = permissions.id
WHERE users.email = 'grace@example.com';

به این صورت:

$ 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=> SELECT email, code FROM users 
greenlight-> INNER JOIN users_permissions ON users.id = users_permissions.user_id
greenlight-> INNER JOIN permissions ON users_permissions.permission_id = permissions.id
greenlight-> WHERE users.email = 'grace@example.com';
       email       |    code     
-------------------+-------------
 grace@example.com | movies:read
(1 row)