Skip to content

Commit

Permalink
added more guards for dealing with 'special' authorities scopes
Browse files Browse the repository at this point in the history
  • Loading branch information
Wassim-Rached committed Oct 23, 2024
1 parent 979be89 commit 6e549d2
Show file tree
Hide file tree
Showing 20 changed files with 371 additions and 16 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.example.usermanagement.controllers;

import com.example.usermanagement.dto.permissions.CreatePermissionDTO;
import com.example.usermanagement.dto.permissions.DetailedPermissionDTO;
import com.example.usermanagement.dto.permissions.GeneralPermissionDTO;
import com.example.usermanagement.entities.Permission;
Expand Down Expand Up @@ -32,7 +33,8 @@ public ResponseEntity<Page<GeneralPermissionDTO>> searchPermissions(
}

@PostMapping
public ResponseEntity<UUID> createPermission(@RequestBody Permission permission) {
public ResponseEntity<UUID> createPermission(@RequestBody CreatePermissionDTO requestBody) {
Permission permission = requestBody.toEntity(null);
permissionService.savePermission(permission);
return ResponseEntity.status(201).body(permission.getId());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ public ResponseEntity<Page<GeneralRoleDTO>> searchAndSortRoles(
}

@PostMapping
public ResponseEntity<GeneralRoleDTO> createRole(@RequestBody CreateRoleDTO requestBody) {
public ResponseEntity<UUID> createRole(@RequestBody CreateRoleDTO requestBody) {
Role role = roleService.createRole(requestBody);
return ResponseEntity.ok(new GeneralRoleDTO(role));
return ResponseEntity.ok(role.getId());
}

@PostMapping("/edit")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.example.usermanagement.dto.accounts;

import com.example.usermanagement.entities.Permission;
import com.example.usermanagement.exceptions.InputValidationException;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
Expand All @@ -14,6 +16,15 @@ public class EditAuthoritiesRequest {
private RoleChanges roles;
private PermissionChanges permissions;

public void validate() {
if (email == null || email.isBlank()) {
throw new InputValidationException("Email is required");
}
if(this.permissions != null){
this.permissions.validate();
}
}

@Getter
@Setter
@NoArgsConstructor
Expand All @@ -29,6 +40,21 @@ public static class PermissionChanges {
private Set<String> grant;
private Set<String> revoke;

public void validate() {
if (this.grant != null) {
for (String permissionPublicName : grant){
if (Permission.isSpecial(permissionPublicName))
throw new InputValidationException("Cannot grant special permissions");
}
}

if (this.revoke != null) {
for (String permissionPublicName : revoke) {
if (Permission.isSpecial(permissionPublicName))
throw new InputValidationException("Cannot revoke special permissions");
}
}
}
}

public Set<String> getRolesTogrant() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.example.usermanagement.dto.permissions;

import com.example.usermanagement.entities.Permission;
import com.example.usermanagement.exceptions.InputValidationException;
import com.example.usermanagement.interfaces.dto.IEntityDTO;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
public class CreatePermissionDTO implements IEntityDTO<Permission,Void> {
private String name;
private String scope;
private String description;

@Override
public Permission toEntity(Void aVoid) {
if(name == null || name.isBlank())
throw new InputValidationException("Permission name is required");
if(scope == null || scope.isBlank())
throw new InputValidationException("Permission scope is required");
if(!name.matches("[a-zA-Z_]+"))
throw new InputValidationException("Permission name can only contain letters and _ : " + name);
if(!scope.matches("[a-zA-Z_]+"))
throw new InputValidationException("Permission scope can only contain letters and _ : " + scope);
if(scope.equals("special")){
throw new InputValidationException("Permission scope cannot be 'special'");
}

Permission permission = new Permission();
permission.setName(name);
permission.setScope(scope);
permission.setDescription(description);
return permission;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ public Role toEntity(Void aVoid) {
throw new InputValidationException("Role name can only contain letters and _ : " + name);
if(!scope.matches("[a-zA-Z_]+"))
throw new InputValidationException("Role scope can only contain letters and _ : " + scope);
if(scope.equals("special")){
throw new InputValidationException("Role scope cannot be 'special'");
}
Role role = new Role();
role.setName(name);
role.setScope(scope);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.example.usermanagement.entities.Account;
import com.example.usermanagement.entities.Permission;
import com.example.usermanagement.entities.Role;
import com.example.usermanagement.exceptions.InputValidationException;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
Expand All @@ -21,7 +22,7 @@ public class RoleEditRequest {

public void validate() {
Role.validatePublicName(publicName);
this.permissions.validate();
this.permissions.validate(publicName);
this.accounts.validate();
}

Expand All @@ -32,12 +33,24 @@ public static class PermissionChanges {
private List<String> grant;
private List<String> revoke;

public void validate() {
public void validate(String rolePublicName) {
if (grant != null) {
grant.forEach(Permission::validatePublicName);
for(String permissionPublicName : grant) {
Role.validateCanBeGrantedPermission(rolePublicName, permissionPublicName);
Permission.validatePublicName(permissionPublicName);
if(Permission.isSpecial(permissionPublicName)) {
throw new InputValidationException("Cannot grant special permissions");
}
}
}
if (revoke != null) {
revoke.forEach(Permission::validatePublicName);
for(String permissionPublicName : revoke) {
Role.validateCanBeRevokedPermission(rolePublicName, permissionPublicName);
Permission.validatePublicName(permissionPublicName);
if(Permission.isSpecial(permissionPublicName)) {
throw new InputValidationException("Cannot revoke special permissions");
}
}
}
}
}
Expand Down Expand Up @@ -74,4 +87,8 @@ public List<String> getAccountsToGrant() {
public List<String> getAccountsToRevoke() {
return accounts.getRevoke();
}

public String getRoleScope() {
return Role.getScopeFromPublicName(publicName);
}
}
12 changes: 12 additions & 0 deletions src/main/java/com/example/usermanagement/entities/Permission.java
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,16 @@ public static void validatePublicName(String publicName) {
throw new InputValidationException("Permission public name must be in the format 'scope.perm.name' : " + publicName);
}
}

public boolean isSpecial() {
return scope.equals("special");
}

public static boolean isSpecial(String publicName){
return Permission.getScopeFromPublicName(publicName).equals("special");
}

public static String getScopeFromPublicName(String publicName) {
return publicName.split("\\.")[0];
}
}
40 changes: 39 additions & 1 deletion src/main/java/com/example/usermanagement/entities/Role.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import com.example.usermanagement.exceptions.InputValidationException;
import jakarta.persistence.*;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
Expand Down Expand Up @@ -96,4 +95,43 @@ public static void validatePublicName(String publicName) {
throw new InputValidationException("Role public name must be in the format 'scope.role.name' : " + publicName);
}
}

public boolean isSpecial() {
return this.scope.equals("special");
}

public static boolean isSpecial(String publicName) {
return getScopeFromPublicName(publicName).equals("special");
}

public static String getScopeFromPublicName(String publicName) {
return publicName.split("\\.")[0];
}

public static boolean canBeGrantedPermission(String rolePublicName,String permissionPublicName) {
// they should either be at the same scope or the role is global
String roleScope = Role.getScopeFromPublicName(rolePublicName);
String permissionScope = Permission.getScopeFromPublicName(permissionPublicName);
return roleScope.equals(permissionScope) || roleScope.equals("global");
}

public static boolean canBeRevokedPermission(String rolePublicName, String permissionPublicName) {
// they should either be at the same scope or the role is global
String roleScope = Role.getScopeFromPublicName(rolePublicName);
String permissionScope = Permission.getScopeFromPublicName(permissionPublicName);
return roleScope.equals(permissionScope) || roleScope.equals("global");
}

public static void validateCanBeGrantedPermission(String rolePublicName, String permissionPublicName) {
if (!canBeGrantedPermission(rolePublicName, permissionPublicName)) {
throw new InputValidationException("Role " + rolePublicName + " cannot be granted permission " + permissionPublicName);
}
}

public static void validateCanBeRevokedPermission(String rolePublicName, String permissionPublicName) {
if (!canBeRevokedPermission(rolePublicName, permissionPublicName)) {
throw new InputValidationException("Role " + rolePublicName + " cannot be revoked permission " + permissionPublicName);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.example.usermanagement.events.listeners;

import com.example.usermanagement.entities.Account;
import com.example.usermanagement.entities.Permission;
import com.example.usermanagement.events.publishers.*;
import com.example.usermanagement.exceptions.IrregularBehaviourException;
import com.example.usermanagement.repositories.AccountRepository;
import com.example.usermanagement.repositories.PermissionRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class MainListener {

private final AccountRepository accountRepository;
private final PermissionRepository permissionRepository;

@EventListener(AccountEmailVerifiedEvent.class)
public void onAccountEmailVerifiedEvent(AccountEmailVerifiedEvent event) {
// this method is mainly for granting the permissions related to
// having a verified email to the account
String permissionPublicName = "special.perm.verified_email";
Account account = accountRepository.findByEmail(event.getEmail()).orElseThrow(() -> new IrregularBehaviourException("Account not found"));
Permission permission = permissionRepository.findByPublicName(permissionPublicName).orElseThrow(() -> new IrregularBehaviourException("Permission not found"));

account.getPermissions().add(permission);
accountRepository.save(account);
}

@EventListener(AccountBecomeMemberEvent.class)
public void onAccountBecomeMemberEvent(AccountBecomeMemberEvent event) {
// this method is mainly for granting the permissions related to
// being a member to the account
String permissionPublicName = "special.perm.membership";
Account account = accountRepository.findByEmail(event.getEmail()).orElseThrow(() -> new IrregularBehaviourException("Account not found"));
Permission permission = permissionRepository.findByPublicName(permissionPublicName).orElseThrow(() -> new IrregularBehaviourException("Permission not found"));

account.getPermissions().add(permission);
accountRepository.save(account);
}

@EventListener(AccountNoLongerMemberEvent.class)
public void onAccountNoLongerMemberEvent(AccountNoLongerMemberEvent event) {
// this method is mainly for revoking the permissions related to
// being a member from the account
String permissionPublicName = "special.perm.membership";
Account account = accountRepository.findByEmail(event.getEmail()).orElseThrow(() -> new IrregularBehaviourException("Account not found"));
Permission permission = permissionRepository.findByPublicName(permissionPublicName).orElseThrow(() -> new IrregularBehaviourException("Permission not found"));

account.getPermissions().remove(permission);
accountRepository.save(account);
}

@EventListener(AccountIdentityVerifiedEvent.class)
public void onAccountIdentityVerifiedEvent(AccountIdentityVerifiedEvent event) {
// this method is mainly for granting the permissions related to
// having a verified identity to the account
String permissionPublicName = "special.perm.verified_identity";
Account account = accountRepository.findByEmail(event.getEmail()).orElseThrow(() -> new IrregularBehaviourException("Account not found"));
Permission permission = permissionRepository.findByPublicName(permissionPublicName).orElseThrow(() -> new IrregularBehaviourException("Permission not found"));

account.getPermissions().add(permission);
accountRepository.save(account);
}

@EventListener(AccountIdentityUnverifiedEvent.class)
public void onAccountIdentityUnverifiedEvent(AccountIdentityUnverifiedEvent event) {
// this method is mainly for revoking the permissions related to
// having a verified identity from the account
String permissionPublicName = "special.perm.verified_identity";
Account account = accountRepository.findByEmail(event.getEmail()).orElseThrow(() -> new IrregularBehaviourException("Account not found"));
Permission permission = permissionRepository.findByPublicName(permissionPublicName).orElseThrow(() -> new IrregularBehaviourException("Permission not found"));

account.getPermissions().remove(permission);
accountRepository.save(account);
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.example.usermanagement.events.publishers;

import lombok.Getter;
import org.springframework.context.ApplicationEvent;

@Getter
public class AccountBecomeMemberEvent extends ApplicationEvent {
private final String email;

public AccountBecomeMemberEvent(Object source, String email) {
super(source);
this.email = email;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.example.usermanagement.events.publishers;

import lombok.Getter;
import org.springframework.context.ApplicationEvent;

@Getter
public class AccountEmailVerifiedEvent extends ApplicationEvent {
private final String email;

public AccountEmailVerifiedEvent(Object source, String email) {
super(source);
this.email = email;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.example.usermanagement.events.publishers;

import lombok.Getter;
import org.springframework.context.ApplicationEvent;

@Getter
public class AccountIdentityUnverifiedEvent extends ApplicationEvent {
private final String email;

public AccountIdentityUnverifiedEvent(Object source, String email) {
super(source);
this.email = email;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.example.usermanagement.events.publishers;

import lombok.Getter;
import org.springframework.context.ApplicationEvent;

@Getter
public class AccountIdentityVerifiedEvent extends ApplicationEvent {
private final String email;

public AccountIdentityVerifiedEvent(Object source, String email) {
super(source);
this.email = email;
}

}
Loading

0 comments on commit 6e549d2

Please sign in to comment.