DDD cara memisahkan izin dan peran dengan benar

Katakanlah saya memiliki aplikasi yang memungkinkan pengguna membuat, menetapkan, dan mengedit tugas. Aplikasi ini dibangun untuk mendukung banyak akun pelanggan. Aplikasi ini memiliki berbagai peran, namun kami akan fokus pada tiga peran dan izinnya relevan dengan tindakan dan sumber daya tertentu (memperbarui teks pada tugas) - admin (memiliki izin untuk memperbarui teks pada semua tugas di semua akun), admin akun (memiliki izin untuk memperbarui teks pada tugas apa pun yang termasuk dalam akun mereka, namun mereka dapat tergabung dalam beberapa akun) dan pengguna akun (memiliki izin untuk memperbarui teks pada tugas hanya jika tugas tersebut diberikan kepada mereka).

Contohnya agak dibuat-buat dan nama perannya agak terlalu umum, tapi bersabarlah.

Tujuannya di sini adalah untuk mencoba menemukan cara yang tepat untuk memisahkan peran dan izin, namun tampaknya peran tersebut pasti terkait dengan izin (lihat kode di bawah).

Mungkin izinnya seharusnya task:updateText, tapi lalu bagaimana cara memeriksa perannya? Apakah saya akan memindahkan blok switch (actor.type) saya dalam model domain ke layanan domain dan memeriksa di sana apakah pengguna dikaitkan dengan admin, admin akun, atau pengguna akun pada akun tersebut? Data dapat di-cache, namun admin akun (dan kemungkinan pengguna lain) dapat dikaitkan dengan beberapa akun yang berarti pramuat data ini mungkin memerlukan terlalu banyak data dalam konteksnya dan dapat menimbulkan masalah karena data ini diteruskan antar layanan.

Pemeriksaan kepemilikan/penugasan dilakukan sebagai bagian dari domain karena bergantung pada status model saat ini. Tidak dibahas di sini, namun mekanisme pembuatan versi sederhana digunakan untuk memastikan model tidak berubah antara waktu pengambilan dan saat pembaruan telah diterapkan. Tampaknya kebijakan setidaknya dapat membuat logika ini lebih bersih, tetapi jika saya memindahkan logika ini ke dalam kebijakan, saya tidak yakin bagaimana saya akan terus menjaminnya kecuali kebijakan dan metode layanan memiliki cara untuk menjamin keduanya berbagi versi yang sama. sumber daya.

Apa pilihan saya di sini? Bimbingan apa pun akan sangat dihargai.

class TaskApplicationService {
  constructor(private taskRepository: TaskRepository) { }
  async updateText({ taskId, text, accountId, context }: { taskId: string, text: string, accountId?: string, context: Context }) {
    let actor: Actor;
    const userId = context.user.id;
    // permissions follow pattern resource:action:qualifier
    if (await hasPermission('task:updateText:all')) {
      actor = await anAdmin({ userId });
    } else if (await hasPermission('task:updateText:account')) {
      actor = await anAccountAdmin({ accountId, userId });
    } else if (await hasPermission('task.updateText:assigned')) {
      actor = await anAccountUser({ accountId, userId });
    } else {
      throw new Error('not authorized');
    }

    const task = await this.taskRepository.findOne({ taskId });
    task.updateText({ text, actor });
    await this.taskRepository.save(task);
    // return TaskMapper.toDto(task);
  }
}

class TaskDomainModel {
  private props: {
    text: string,
    accountId: string,
    assignedAccountUserId: string;
  };

  get text(): string {
    return this.props.text;
  }

  updateText({ text, actor }: { text: string, actor: Actor }) {
    switch (actor.type) {
      case ActorType.ADMIN:
        break;
      case ActorType.ACCOUNT_ADMIN:
        assert(this.props.accountId === actor.tenantId);
        break;
      case ActorType.ACCOUNT_USER:
        assert(this.props.accountId === actor.tenantId);
        assert(this.props.assignedAccountUserId === actor.tenantUserId);
        break;
      default:
        // note assertions and throwing errors are here for brevity,
        // but normally would use something similar to this:
        // https://khalilstemmler.com/articles/enterprise-typescript-nodejs/handling-errors-result-class/
        throw new Error('unknown actor type');
    }

    this.props.text = text;
  }
}

// supporting cast

interface User {
  id: string;
}

interface Context {
  user: User;
}

enum ActorType {
  ADMIN,
  ACCOUNT_ADMIN,
  ACCOUNT_USER
}

interface Admin {
  type: ActorType.ADMIN,
  userId: string
}

interface AccountAdmin {
  type: ActorType.ACCOUNT_ADMIN,
  tenantId: string,
  userId: string
}

interface AccountUser {
  type: ActorType.ACCOUNT_USER,
  tenantUserId: string,
  tenantId: string,
  userId: string
}

async function anAdmin({ userId }: { userId: string }): Promise<Admin> {
  // gets an admin
}

async function anAccountAdmin({ accountId, userId }: { accountId: string, userId: string }): Promise<AccountAdmin> {
  // gets an account admin
}

async function anAccountUser({ accountId, userId }: { accountId: string, userId: string }): Promise<AccountUser> {
  // gets an account user
}

async function hasPermission(permission: string) {
  // checks permissions in cache or calls to external service
}

type Actor = Admin | AccountAdmin | AccountUser;

interface TaskRepository {
  findOne({ taskId }: { taskId: string }): Promise<TaskModel>;
  save(task: TaskModel): Promise<TaskModel>;
}

person Jordan    schedule 01.12.2019    source sumber
comment
Jadikan pertanyaan Anda lebih ringkas mungkin sudah terjawab lebih awal.   -  person xiemeilong    schedule 04.12.2019


Jawaban (1)


Saya pikir tidak perlu memasangkan peran pada izin sesulit ini. Sebaliknya, Anda mungkin memiliki kelas peran yang berisi daftar izin umum seperti di bawah ini.

class Permission {
    name: String;
}

class Role {
    name: String;
    permissions: Permission[];
}

class User {
    id: String;
    role: Role;
}

Dengan cara ini Anda dapat mengubah peran izin apa yang sebenarnya dimiliki.

Hal lain yang menjadi perhatian saya adalah logika pemeriksaan akses Anda tersebar ke mana-mana sehingga mengganggu pemeliharaan kode. Sebagai gantinya, Anda dapat mengekstraknya menjadi satu layanan. Berikut sketsa kasarnya.

class PermissionService {
    userHasRightToUpdateText = (task : Task, user: User) => {
        return (user.role.permissions.some(p => p.name == 'task:updateText:all')
            || (user.role.permissions.some(p => p.name == 'task:updateText:account') && task.accountId == user.id));
            //or whatever you auth logic is
    }
}

class TaskApplicationService {
    constructor(private taskRepository: TaskRepository,
        private userRepository: UserRepository,
        private permissionService : PermissionService) { }
    async updateText({ taskId, text, accountId, context }: { taskId: string, text: string, accountId?: string, context: Context }) {
      const userId = context.user.id;
      const user = await this.userRepository.findOne({ userId });

      const task = await this.taskRepository.findOne({ taskId });

      if (this.permissionService.userHasRightToUpdateText(task, user)) {
        task.updateText({ text });
        await this.taskRepository.save(task);
      }
      else {
        throw new Error('not authorized');
      }
      // return TaskMapper.toDto(task);
    }
  }

Semoga ini membantu.

person Bohdan Stupak    schedule 10.12.2019
comment
Saya berpikir untuk menggunakan kebijakan dengan cara ini dan ini memecahkan masalah transaksional dan versi saya meskipun saya tertarik dengan bagaimana .NET melakukan hal-hal seperti ini (docs.microsoft.com/en-us/aspnet/core /security/authorization/) dan apakah masih dapat menawarkan jaminan yang sama. Mengekstraksi logika ke dalam kebijakan adalah bagian yang mudah bagi saya. Bagian tersulitnya adalah menentukan tipe pengguna yang saya hadapi. Saya memiliki tiga jenis pengguna dalam contoh saya - pengguna admin yang dapat memperbarui teks pada tugas apa pun... - person Jordan; 11.12.2019
comment
admin akun yang bisa memperbarui teks hanya untuk tugas di bawah akun mereka dan kemudian pengguna akun yang hanya bisa memperbarui tugas di akun mereka dan yang telah ditugaskan kepada mereka. Dalam model saya, dan saya tidak yakin apakah ini pendekatan yang benar, entitas pengguna akun (bukan pengguna) adalah entitas yang akan ditugaskan untuk tugas tersebut. Mungkin saya harus menggunakan userId untuk penugasannya, tetapi jika saya menghapus pengguna itu dari peran pengguna akun (tetapi mereka masih dalam peran lain) bagaimana saya tahu entitas mana yang terkait dengan mereka dalam kapasitas sebagai pengguna akun / apa yang perlu saya bersihkan? - person Jordan; 11.12.2019
comment
Untuk kejelasan - tugas dalam pendekatan saya saat ini adalah task.accountUserId = accountUser.id. - person Jordan; 11.12.2019
comment
ide dibalik contoh saya adalah Anda tidak perlu mengetahui pengguna mana yang Anda hadapi. Di PermissionsService cukup mengetahui izin mana yang dimiliki pengguna dan memeriksa batasan tambahan seperti apakah tugas milik pengguna yang dimaksud. Peran pengguna dalam contoh ini hanyalah wadah untuk izin. - person Bohdan Stupak; 11.12.2019