feat(backend): Implement complete RBAC system (Day 5 Phase 2)
Implemented Role-Based Access Control (RBAC) with 5 tenant-level roles following Clean Architecture principles. Changes: - Created TenantRole enum (TenantOwner, TenantAdmin, TenantMember, TenantGuest, AIAgent) - Created UserTenantRole entity with repository pattern - Updated JWT service to include role claims (tenant_role, role) - Updated RegisterTenant to auto-assign TenantOwner role - Updated Login to query and include user role in JWT - Updated RefreshToken to preserve role claims - Added authorization policies in Program.cs (RequireTenantOwner, RequireTenantAdmin, etc.) - Updated /api/auth/me endpoint to return role information - Created EF Core migration for user_tenant_roles table - Applied database migration successfully Database: - New table: identity.user_tenant_roles - Columns: id, user_id, tenant_id, role, assigned_at, assigned_by_user_id - Indexes: user_id, tenant_id, role, unique(user_id, tenant_id) - Foreign keys: CASCADE on user and tenant deletion Testing: - Created test-rbac.ps1 PowerShell script - All RBAC tests passing - JWT tokens contain role claims - Role persists across login and token refresh Documentation: - DAY5-PHASE2-RBAC-IMPLEMENTATION-SUMMARY.md with complete implementation details 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,366 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using ColaFlow.Modules.Identity.Infrastructure.Persistence;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ColaFlow.Modules.Identity.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
[DbContext(typeof(IdentityDbContext))]
|
||||
[Migration("20251103135644_AddUserTenantRoles")]
|
||||
partial class AddUserTenantRoles
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "9.0.10")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("ColaFlow.Modules.Identity.Domain.Aggregates.Tenants.Tenant", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<int>("MaxProjects")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("max_projects");
|
||||
|
||||
b.Property<int>("MaxStorageGB")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("max_storage_gb");
|
||||
|
||||
b.Property<int>("MaxUsers")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("max_users");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("name");
|
||||
|
||||
b.Property<string>("Plan")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)")
|
||||
.HasColumnName("plan");
|
||||
|
||||
b.Property<string>("Slug")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)")
|
||||
.HasColumnName("slug");
|
||||
|
||||
b.Property<string>("SsoConfig")
|
||||
.HasColumnType("jsonb")
|
||||
.HasColumnName("sso_config");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<DateTime?>("SuspendedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("suspended_at");
|
||||
|
||||
b.Property<string>("SuspensionReason")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)")
|
||||
.HasColumnName("suspension_reason");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Slug")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_tenants_slug");
|
||||
|
||||
b.ToTable("tenants", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ColaFlow.Modules.Identity.Domain.Aggregates.Users.RefreshToken", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("DeviceInfo")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)")
|
||||
.HasColumnName("device_info");
|
||||
|
||||
b.Property<DateTime>("ExpiresAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expires_at");
|
||||
|
||||
b.Property<string>("IpAddress")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)")
|
||||
.HasColumnName("ip_address");
|
||||
|
||||
b.Property<string>("ReplacedByToken")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)")
|
||||
.HasColumnName("replaced_by_token");
|
||||
|
||||
b.Property<DateTime?>("RevokedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("revoked_at");
|
||||
|
||||
b.Property<string>("RevokedReason")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)")
|
||||
.HasColumnName("revoked_reason");
|
||||
|
||||
b.Property<Guid>("TenantId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("tenant_id");
|
||||
|
||||
b.Property<string>("TokenHash")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)")
|
||||
.HasColumnName("token_hash");
|
||||
|
||||
b.Property<string>("UserAgent")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)")
|
||||
.HasColumnName("user_agent");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ExpiresAt")
|
||||
.HasDatabaseName("ix_refresh_tokens_expires_at");
|
||||
|
||||
b.HasIndex("TenantId")
|
||||
.HasDatabaseName("ix_refresh_tokens_tenant_id");
|
||||
|
||||
b.HasIndex("TokenHash")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_refresh_tokens_token_hash");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_refresh_tokens_user_id");
|
||||
|
||||
b.ToTable("refresh_tokens", "identity");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ColaFlow.Modules.Identity.Domain.Aggregates.Users.User", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("AuthProvider")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)")
|
||||
.HasColumnName("auth_provider");
|
||||
|
||||
b.Property<string>("AvatarUrl")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)")
|
||||
.HasColumnName("avatar_url");
|
||||
|
||||
b.Property<DateTime>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("created_at");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)")
|
||||
.HasColumnName("email");
|
||||
|
||||
b.Property<string>("EmailVerificationToken")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)")
|
||||
.HasColumnName("email_verification_token");
|
||||
|
||||
b.Property<DateTime?>("EmailVerifiedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("email_verified_at");
|
||||
|
||||
b.Property<string>("ExternalEmail")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)")
|
||||
.HasColumnName("external_email");
|
||||
|
||||
b.Property<string>("ExternalUserId")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)")
|
||||
.HasColumnName("external_user_id");
|
||||
|
||||
b.Property<string>("FullName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("full_name");
|
||||
|
||||
b.Property<string>("JobTitle")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)")
|
||||
.HasColumnName("job_title");
|
||||
|
||||
b.Property<DateTime?>("LastLoginAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_login_at");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.IsRequired()
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)")
|
||||
.HasColumnName("password_hash");
|
||||
|
||||
b.Property<string>("PasswordResetToken")
|
||||
.HasMaxLength(255)
|
||||
.HasColumnType("character varying(255)")
|
||||
.HasColumnName("password_reset_token");
|
||||
|
||||
b.Property<DateTime?>("PasswordResetTokenExpiresAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("password_reset_token_expires_at");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)")
|
||||
.HasColumnName("phone_number");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)")
|
||||
.HasColumnName("status");
|
||||
|
||||
b.Property<Guid>("TenantId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("tenant_id");
|
||||
|
||||
b.Property<DateTime?>("UpdatedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("updated_at");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TenantId", "Email")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("ix_users_tenant_id_email");
|
||||
|
||||
b.ToTable("users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ColaFlow.Modules.Identity.Domain.Aggregates.Users.UserTenantRole", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTime>("AssignedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("assigned_at");
|
||||
|
||||
b.Property<Guid?>("AssignedByUserId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("assigned_by_user_id");
|
||||
|
||||
b.Property<string>("Role")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)")
|
||||
.HasColumnName("role");
|
||||
|
||||
b.Property<Guid>("TenantId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("tenant_id");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.Property<Guid>("tenant_id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("user_id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Role")
|
||||
.HasDatabaseName("ix_user_tenant_roles_role");
|
||||
|
||||
b.HasIndex("TenantId")
|
||||
.HasDatabaseName("ix_user_tenant_roles_tenant_id");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_user_tenant_roles_user_id");
|
||||
|
||||
b.HasIndex("tenant_id");
|
||||
|
||||
b.HasIndex("user_id");
|
||||
|
||||
b.HasIndex("UserId", "TenantId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("uq_user_tenant_roles_user_tenant");
|
||||
|
||||
b.ToTable("user_tenant_roles", "identity", t =>
|
||||
{
|
||||
t.Property("tenant_id")
|
||||
.HasColumnName("tenant_id1");
|
||||
|
||||
t.Property("user_id")
|
||||
.HasColumnName("user_id1");
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ColaFlow.Modules.Identity.Domain.Aggregates.Users.UserTenantRole", b =>
|
||||
{
|
||||
b.HasOne("ColaFlow.Modules.Identity.Domain.Aggregates.Tenants.Tenant", "Tenant")
|
||||
.WithMany()
|
||||
.HasForeignKey("tenant_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ColaFlow.Modules.Identity.Domain.Aggregates.Users.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("user_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Tenant");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ColaFlow.Modules.Identity.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddUserTenantRoles : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "user_tenant_roles",
|
||||
schema: "identity",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
user_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
tenant_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
role = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||
assigned_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
assigned_by_user_id = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
user_id1 = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
tenant_id1 = table.Column<Guid>(type: "uuid", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_user_tenant_roles", x => x.id);
|
||||
table.ForeignKey(
|
||||
name: "FK_user_tenant_roles_tenants_tenant_id1",
|
||||
column: x => x.tenant_id1,
|
||||
principalTable: "tenants",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_user_tenant_roles_users_user_id1",
|
||||
column: x => x.user_id1,
|
||||
principalTable: "users",
|
||||
principalColumn: "id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_user_tenant_roles_role",
|
||||
schema: "identity",
|
||||
table: "user_tenant_roles",
|
||||
column: "role");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_user_tenant_roles_tenant_id",
|
||||
schema: "identity",
|
||||
table: "user_tenant_roles",
|
||||
column: "tenant_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_user_tenant_roles_tenant_id1",
|
||||
schema: "identity",
|
||||
table: "user_tenant_roles",
|
||||
column: "tenant_id1");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "ix_user_tenant_roles_user_id",
|
||||
schema: "identity",
|
||||
table: "user_tenant_roles",
|
||||
column: "user_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_user_tenant_roles_user_id1",
|
||||
schema: "identity",
|
||||
table: "user_tenant_roles",
|
||||
column: "user_id1");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "uq_user_tenant_roles_user_tenant",
|
||||
schema: "identity",
|
||||
table: "user_tenant_roles",
|
||||
columns: new[] { "user_id", "tenant_id" },
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "user_tenant_roles",
|
||||
schema: "identity");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -274,6 +274,89 @@ namespace ColaFlow.Modules.Identity.Infrastructure.Persistence.Migrations
|
||||
|
||||
b.ToTable("users", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ColaFlow.Modules.Identity.Domain.Aggregates.Users.UserTenantRole", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<DateTime>("AssignedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("assigned_at");
|
||||
|
||||
b.Property<Guid?>("AssignedByUserId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("assigned_by_user_id");
|
||||
|
||||
b.Property<string>("Role")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)")
|
||||
.HasColumnName("role");
|
||||
|
||||
b.Property<Guid>("TenantId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("tenant_id");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("user_id");
|
||||
|
||||
b.Property<Guid>("tenant_id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("user_id")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Role")
|
||||
.HasDatabaseName("ix_user_tenant_roles_role");
|
||||
|
||||
b.HasIndex("TenantId")
|
||||
.HasDatabaseName("ix_user_tenant_roles_tenant_id");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.HasDatabaseName("ix_user_tenant_roles_user_id");
|
||||
|
||||
b.HasIndex("tenant_id");
|
||||
|
||||
b.HasIndex("user_id");
|
||||
|
||||
b.HasIndex("UserId", "TenantId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("uq_user_tenant_roles_user_tenant");
|
||||
|
||||
b.ToTable("user_tenant_roles", "identity", t =>
|
||||
{
|
||||
t.Property("tenant_id")
|
||||
.HasColumnName("tenant_id1");
|
||||
|
||||
t.Property("user_id")
|
||||
.HasColumnName("user_id1");
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("ColaFlow.Modules.Identity.Domain.Aggregates.Users.UserTenantRole", b =>
|
||||
{
|
||||
b.HasOne("ColaFlow.Modules.Identity.Domain.Aggregates.Tenants.Tenant", "Tenant")
|
||||
.WithMany()
|
||||
.HasForeignKey("tenant_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("ColaFlow.Modules.Identity.Domain.Aggregates.Users.User", "User")
|
||||
.WithMany()
|
||||
.HasForeignKey("user_id")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Tenant");
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user