انتقل إلى المحتوى الرئيسي

TP3 - إدارة الأدوار والأذونات

أهداف التمرين​

في هذا التمرين، سوف تقوم بـ:

  1. ✅ تنفيذ نظام RBAC (التحكم في الوصول المستند إلى الأدوار)
  2. ✅ إنشاء أذونات دقيقة
  3. ✅ استخدام @PreAuthorize مع تعبيرات متقدمة
  4. ✅ إدارة تسلسل هرمي للأدوار
  5. ✅ اختبار الوصول مع مستخدمين مختلفين

المدة المقدرة: 1 ساعة و30 دقيقة


السياق​

أنت تدير تطبيقًا للشركات مع:

  • ADMIN : وصول كامل
  • MANAGER : يدير مستخدمي فريقه
  • USER : وصول محدود إلى بياناته الخاصة

الجزء 1: نموذج البيانات​

كيان الإذن​

@Entity
@Table(name = "permissions")
@Data
@NoArgsConstructor
public class Permission {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(unique = true, nullable = false)
private String name;

private String description;

public Permission(String name, String description) {
this.name = name;
this.description = description;
}
}

كيان الدور مع الأذونات​

@Entity
@Table(name = "roles")
@Data
@NoArgsConstructor
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(unique = true, nullable = false)
private String name;

@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "role_permissions",
joinColumns = @JoinColumn(name = "role_id"),
inverseJoinColumns = @JoinColumn(name = "permission_id"))
private Set<Permission> permissions = new HashSet<>();

public Role(String name) {
this.name = name;
}
}

كيان المستخدم​

@Entity
@Table(name = "users")
@Data
@NoArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(unique = true, nullable = false)
private String username;

@Column(nullable = false)
private String password;

private String email;
private boolean enabled = true;

@ManyToOne
@JoinColumn(name = "manager_id")
private User manager; // للتسلسل الهرمي

@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id"))
private Set<Role> roles = new HashSet<>();
}

الجزء 2: UserDetails مع الأذونات​

public class CustomUserDetails implements UserDetails {
private final User user;

public CustomUserDetails(User user) {
this.user = user;
}

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Set<GrantedAuthority> authorities = new HashSet<>();

for (Role role : user.getRoles()) {
// إضافة الدور (مثال: ROLE_ADMIN)
authorities.add(new SimpleGrantedAuthority(role.getName()));

// إضافة أذونات الدور
for (Permission permission : role.getPermissions()) {
authorities.add(new SimpleGrantedAuthority(permission.getName()));
}
}

return authorities;
}

@Override
public String getPassword() { return user.getPassword(); }

@Override
public String getUsername() { return user.getUsername(); }

@Override
public boolean isEnabled() { return user.isEnabled(); }

@Override
public boolean isAccountNonExpired() { return true; }

@Override
public boolean isAccountNonLocked() { return true; }

@Override
public boolean isCredentialsNonExpired() { return true; }

public User getUser() { return user; }

public Long getId() { return user.getId(); }
}

الجزء 3: تهيئة البيانات​

@Component
@RequiredArgsConstructor
public class DataInitializer implements CommandLineRunner {

private final PermissionRepository permissionRepository;
private final RoleRepository roleRepository;
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;

@Override
@Transactional
public void run(String... args) {
// الأذونات
Permission readUsers = createPermission("READ_USERS", "عرض المستخدمين");
Permission writeUsers = createPermission("WRITE_USERS", "تعديل المستخدمين");
Permission deleteUsers = createPermission("DELETE_USERS", "حذف المستخدمين");
Permission readReports = createPermission("READ_REPORTS", "عرض التقارير");
Permission writeReports = createPermission("WRITE_REPORTS", "إنشاء التقارير");

// الأدوار مع الأذونات
Role adminRole = createRole("ROLE_ADMIN",
Set.of(readUsers, writeUsers, deleteUsers, readReports, writeReports));
Role managerRole = createRole("ROLE_MANAGER",
Set.of(readUsers, writeUsers, readReports, writeReports));
Role userRole = createRole("ROLE_USER",
Set.of(readReports));

// المستخدمون
User admin = createUser("admin", "admin@test.com", "admin123", Set.of(adminRole));
User manager = createUser("manager", "manager@test.com", "manager123", Set.of(managerRole));
User user1 = createUser("user1", "user1@test.com", "user123", Set.of(userRole));
User user2 = createUser("user2", "user2@test.com", "user123", Set.of(userRole));

// التسلسل الهرمي: المدير يدير user1 و user2
user1.setManager(manager);
user2.setManager(manager);
userRepository.saveAll(List.of(user1, user2));
}

private Permission createPermission(String name, String description) {
return permissionRepository.findByName(name)
.orElseGet(() -> permissionRepository.save(new Permission(name, description)));
}

private Role createRole(String name, Set<Permission> permissions) {
return roleRepository.findByName(name)
.orElseGet(() -> {
Role role = new Role(name);
role.setPermissions(permissions);
return roleRepository.save(role);
});
}

private User createUser(String username, String email, String password, Set<Role> roles) {
return userRepository.findByUsername(username)
.orElseGet(() -> {
User user = new User();
user.setUsername(username);
user.setEmail(email);
user.setPassword(passwordEncoder.encode(password));
user.setRoles(roles);
return userRepository.save(user);
});
}
}

الجزء 4: Controllers مع @PreAuthorize​

UserController​

@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {

private final UserRepository userRepository;

// فقط للمسؤول
@GetMapping
@PreAuthorize("hasRole('ADMIN')")
public List<User> getAllUsers() {
return userRepository.findAll();
}

// المسؤول أو المدير الذي يدير هذا المستخدم
@GetMapping("/{id}")
@PreAuthorize("hasRole('ADMIN') or @userSecurity.isManagerOf(#id)")
public User getUser(@PathVariable Long id) {
return userRepository.findById(id).orElseThrow();
}

// إذن محدد
@PostMapping
@PreAuthorize("hasAuthority('WRITE_USERS')")
public User createUser(@RequestBody User user) {
return userRepository.save(user);
}

// فقط للمسؤول
@DeleteMapping("/{id}")
@PreAuthorize("hasAuthority('DELETE_USERS')")
public void deleteUser(@PathVariable Long id) {
userRepository.deleteById(id);
}

// المستخدم نفسه أو مديره أو المسؤول
@PutMapping("/{id}")
@PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id or @userSecurity.isManagerOf(#id)")
public User updateUser(@PathVariable Long id, @RequestBody User user) {
User existing = userRepository.findById(id).orElseThrow();
existing.setEmail(user.getEmail());
return userRepository.save(existing);
}
}

ReportController​

@RestController
@RequestMapping("/api/reports")
@RequiredArgsConstructor
public class ReportController {

@GetMapping
@PreAuthorize("hasAuthority('READ_REPORTS')")
public List<String> getReports() {
return List.of("تقرير 1", "تقرير 2", "تقرير 3");
}

@PostMapping
@PreAuthorize("hasAuthority('WRITE_REPORTS')")
public String createReport(@RequestBody String report) {
return "تم إنشاء التقرير: " + report;
}

// مزيج: الدور والإذن
@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN') and hasAuthority('DELETE_REPORTS')")
public void deleteReport(@PathVariable Long id) {
// ...
}
}

الجزء 5: خدمة أمان مخصصة​

@Service("userSecurity")
@RequiredArgsConstructor
public class UserSecurityService {

private final UserRepository userRepository;

public boolean isManagerOf(Long userId) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth == null || !(auth.getPrincipal() instanceof CustomUserDetails)) {
return false;
}

CustomUserDetails currentUser = (CustomUserDetails) auth.getPrincipal();
User targetUser = userRepository.findById(userId).orElse(null);

if (targetUser == null || targetUser.getManager() == null) {
return false;
}

return targetUser.getManager().getId().equals(currentUser.getId());
}

public boolean isOwner(Long resourceOwnerId) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth == null || !(auth.getPrincipal() instanceof CustomUserDetails)) {
return false;
}

CustomUserDetails currentUser = (CustomUserDetails) auth.getPrincipal();
return currentUser.getId().equals(resourceOwnerId);
}
}

الجزء 6: الاختبارات​

اختبار مع مستخدمين مختلفين​

# تسجيل دخول المسؤول
curl -X POST http://localhost:8080/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123"}'
# → استرجاع ADMIN_TOKEN

# تسجيل دخول المدير
curl -X POST http://localhost:8080/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"manager","password":"manager123"}'
# → استرجاع MANAGER_TOKEN

# تسجيل دخول user1
curl -X POST http://localhost:8080/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"user1","password":"user123"}'
# → استرجاع USER_TOKEN

اختبارات الوصول​

# المسؤول: GET جميع المستخدمين → OK
curl http://localhost:8080/api/users -H "Authorization: Bearer ADMIN_TOKEN"

# المدير: GET جميع المستخدمين → 403
curl http://localhost:8080/api/users -H "Authorization: Bearer MANAGER_TOKEN"

# المدير: GET مستخدم من فريقه → OK
curl http://localhost:8080/api/users/3 -H "Authorization: Bearer MANAGER_TOKEN"

# المستخدم: GET التقارير → OK
curl http://localhost:8080/api/reports -H "Authorization: Bearer USER_TOKEN"

# المستخدم: POST التقارير → 403
curl -X POST http://localhost:8080/api/reports \
-H "Authorization: Bearer USER_TOKEN" \
-H "Content-Type: application/json" \
-d '"تقريري"'

مصفوفة الوصول​

EndpointADMINMANAGERUSER
GET /api/users✅❌❌
GET /api/users/{id}✅⚠️ فريقه❌
POST /api/users✅✅❌
DELETE /api/users/{id}✅❌❌
GET /api/reports✅✅✅
POST /api/reports✅✅❌

قائمة التحقق​

  • يمكن للمسؤول فعل كل شيء
  • يمكن للمدير رؤية/تعديل فريقه فقط
  • يمكن للمستخدم فقط قراءة التقارير
  • يتم التحقق من الأذونات بشكل صحيح
  • @userSecurity.isManagerOf يعمل

🎯 الخطوة التالية​

➡️ TP8.4: تسجيل الدخول الاجتماعي مع Google


⬅️ العودة إلى الوحدة 8