Here is an idea for managing the user permissions at my web app. In this design users have different privileges to perform basic crud operations on different app domains (Customer Management Domain, Stock Management Domain etc.). In this way, some specific areas or functions hiding/showing to the user also, and an admin user can grant these permissions.
User Entity:
@Entity
@Table(name = "Users")
public class Users {
@Id
@GeneratedValue
@Column(name = "id")
private Long id;
@Column(name="firstname")
private String firstname;
@Column(name="lastname")
private String lastname;
@OneToMany(cascade = CascadeType.ALL)
@JoinTable(name = "name_permission_mapping",
joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")},
inverseJoinColumns = {@JoinColumn(name = "permission_id", referencedColumnName = "id")})
@MapKey(name = "permission_name")
private Map<String, Permission> _roles = new HashMap<>();
public Users() {
super();
}
public Users(String firstname, String lastname, Map<String, Permission> roles) {
super();
this.firstname = firstname;
this.lastname = lastname;
this._roles = roles;
}
// getters + setters + toString
}
User Builder:
public class UsersBuilder {
private String firstname;
private String lastname;
Map<String, Permission> Permissions = new HashMap<>();
public UsersBuilder setFirstname(String firstname) {
this.firstname = firstname;
return this;
}
public UsersBuilder setLastname(String lastname) {
this.lastname = lastname;
return this;
}
public UsersBuilder setPermissions(Map<String, Permission> p) {
this.Permissions = p;
return this;
}
public Users build() {
return new Users(firstname, lastname, Permissions);
}
}
User Service:
public class UserPermissionService {
public boolean createPermissionProvider(Permission p, Users u) {
return u.get_roles().get(p.getClass().getSimpleName()) != null
? checkUserWritePermission(p, u)
: false;
}
public boolean readPermissionProvider(Permission p, Users u) {
return u.get_roles().get(p.getClass().getSimpleName()) != null
? checkUserReadPermission(p, u)
: false;
}
public boolean updatePermissionProvider(Permission p, Users u) {
return u.get_roles().get(p.getClass().getSimpleName()) != null
? checkUserUpdatePermission(p, u)
: false;
}
public boolean deletePermissionProvider(Permission p, Users u) {
return u.get_roles().get(p.getClass().getSimpleName()) != null
? checkUserDeletePermission(p, u)
: false;
}
public boolean checkUserWritePermission(Permission p, Users user) {
return user.get_roles().get(p.getClass().getSimpleName()).is_create();
}
public boolean checkUserReadPermission(Permission p, Users user) {
return user.get_roles().get(p.getClass().getSimpleName()).is_read();
}
public boolean checkUserUpdatePermission(Permission p, Users user) {
return user.get_roles().get(p.getClass().getSimpleName()).is_update();
}
public boolean checkUserDeletePermission(Permission p, Users user) {
return user.get_roles().get(p.getClass().getSimpleName()).is_delete();
}
// ...
public boolean grantPermission(Permission p, Users user) {
return user.get_roles().put(p.getClass().getSimpleName(), p) != null
? true : false;
}
public boolean removePermission(Permission p, Users user) {
return user.get_roles().remove(p.getClass().getSimpleName(), p);
}
public Permission findPermission(Permission p, Users user) {
return user.get_roles().get(p.getClass().getSimpleName());
}
}
Permission Base Class:
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class Permission {
@Id
@GeneratedValue
private Long id;
@Column(name="permission_name")
private String permission_name;
@Column(name = "_create")
private boolean _create;
@Column(name = "_read")
private boolean _read;
@Column(name = "_update")
private boolean _update;
@Column(name = "_delete")
private boolean _delete;
// getters + setters + toString
}
Permission Subclass 1:
@Entity
public class CustomerDomainPermission extends Permission {
public CustomerDomainPermission() {
super();
}
}
Permission Subclass 2:
@Entity
public class ProductDomainPermission extends Permission {
public ProductDomainPermission() {
super();
}
}
Test Class:
public class PermissionsTest {
public static void main(String[] args) {
Permission cd = new CustomerDomainPermission();
cd.set_read(true);
cd.set_create(true);
cd.set_delete(true);
cd.set_update(true);
cd.setPermission_name("customer_domain");
Permission pd = new ProductDomainPermission();
pd.set_read(true);
pd.set_create(true);
pd.set_delete(true);
pd.set_update(true);
pd.setPermission_name("product_domain");
Users user = new UsersBuilder()
.setFirstname("Walter")
.setLastname("White")
.build();
UserPermissionService us = new UserPermissionService();
us.grantPermission(pd, user);
us.grantPermission(cd, user);
new UserDao().addUser(user);
System.out.println(user);
}
}
Output:
Users [id=1, firstname=Walter, lastname=White, _roles={ProductDomainPermission=Permission [id=2, permission_name=product_domain, _create=true, _read=true, _update=true, _delete=true], CustomerDomainPermission=Permission [id=3, permission_name=customer_domain, _create=true, _read=true, _update=true, _delete=true]}]
Database Tables:
![](https://blog.gunlerveisler.gen.tr/wp-content/uploads/2020/08/table_06.jpg)
Pros of this design:
- When new requirements of application domains come (for example “Stock Domain” or “Invoice Management Domain” etc. ) only need to do is just create another subclass of Permission. (also obeyed Open-Closed Principle, SOLID) This can require significantly less programming effort because the base class contains many methods providing default behavior.
- The association between Users and Permission classes simply stored at “permissions” map collection attribute in the Users base class.
- It needs only one base service class and 8 methods for all of its subclasses. (Composition)
But I hesitate to use this, because the database is not in a good shape, there are some repetitive columns at the “ProductDomainPermission
” and “CustomerDomainPermission
” tables, such as “permission_name
“, this is an illegal situation at the context of First Normal Form database property. This may cause performance issues once project starts to grow. Also, Map collection attribute seems like misused and “Users
” entity is not loosely coupled to the permission code block. Consequently, It looks not very good to me, so I give up the idea.
But, Instead of this I thought up another solution:
I decided to use a “DomainPermissions
” type instead of a Map Collection attribute inside the “Users
” class, it consists of 4 primitive boolean attribute which represent crud operation permissions, so that "Users
” decouples from “Permission
“. Also, create OneToOne associations and constraints among the Permission Persistent Block and the Users explicitly, in this way I had more control over Hibernate to shaping database.
![](https://blog.gunlerveisler.gen.tr/wp-content/uploads/2020/08/umlDiagram-1024x530.jpg)
Users Class:
Entity
@Table(name = "Users")
public class Users {
@Id
@GeneratedValue
@Column(name = "user_id")
Long user_id;
@Column(name="firstname")
private String firstname;
@Column(name="lastname")
private String lastname;
@OneToOne(cascade = CascadeType.ALL,
orphanRemoval = true)
@JoinColumn(name = "_domainPermission_id", referencedColumnName = "domainPermissions_id")
private DomainPermissions domainPermissions;
public Users() {
super();
}
public Users(String firstname, String lastname, DomainPermissions d) {
super();
this.firstname = firstname;
this.lastname = lastname;
this.domainPermissions = d;
}
// getters + setters + toString()
As is seen, this class has a attribute with type of “DomainPermissions
” and there is an OneToOne Bidirectional association between “Users
” and “DomainPermissions
” classes.
DomainPermissions Class:
@Entity
public class DomainPermissions {
@Id
@GeneratedValue
private Long domainPermissions_id;
@OneToOne(cascade = CascadeType.ALL,
orphanRemoval = true)
@JoinColumn(name="customerDomain_permission_id", columnDefinition = "bigint default 1")
private Permission customerDomain;
@OneToOne(cascade = CascadeType.ALL,
orphanRemoval = true)
@JoinColumn(name="productDomain_permission_id", columnDefinition = "bigint default 1")
private Permission productDomain;
@OneToOne(mappedBy = "domainPermissions")
private Users user;
// getters + setters + toString()
And this “DomainPermission
” class has 2 “Permission
” super type attributes for available application domain types. Also, has OneToOne Unidirectinal association with “Permission
” super type. In future when a new domain comes I can simply create another subtype and add another attribute in here such as:
(...)
@OneToOne(cascade = CascadeType.ALL,
orphanRemoval = true)
@JoinColumn(name="blablaDomain_permission_id", columnDefinition = "bigint default 1")
private Permission blablaDomain;
(...)
But an important thing is if a new subclass (domain) creation occurs in the future, a new null column will add to the “DomainPermissions
” table and all of the before rows will mark at this column as null, this is an unwanted situation, so I had to pay attention for potential null pointer exceptions and Data Integrity. To prevent this I rearrange the DomainPermissions class and define a DEFAULT Constraint per domain such as:
@JoinColumn(name="customerDomain_permission_id", columnDefinition = "bigint default 1")
// private Permission some_domain_name;
In this notation the attribute with id 1 should be an all “false” responsive record placed on Permission
class table and it stand in for null object pattern. This can be creating at the deployment or coding in a Servlet which takes care of initialization stuff.
DomainPermissionsBuilder Class:
public class DomainPermissionsBuilder {
private Permission customerDomain;
private Permission productDomain;
public DomainPermissionsBuilder setCustomerDomain(Permission c) {
this.customerDomain = c != null ? c : new CustomerDomainPermission();
return this;
}
public DomainPermissionsBuilder setProductDomain(Permission p) {
this.productDomain = p != null ? p : new ProductDomainPermission();
return this;
}
public DomainPermissions build() {
if (this.customerDomain == null)
this.customerDomain = new CustomerDomainPermission();
if (this.productDomain == null)
this.productDomain = new ProductDomainPermission();
return new DomainPermissions(customerDomain, productDomain);
}
}
Permission Class:
@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class Permission {
@Id
@GeneratedValue
Long permission_id;
@Column(name = "_create")
private boolean _create;
@Column(name = "_read")
private boolean _read;
@Column(name = "_update")
private boolean _update;
@Column(name = "_delete")
private boolean _delete;
public Permission() {
super();
this._create = false;
this._read = false;
this._update = false;
this._delete = false;
}
public Permission(boolean _create, boolean _read, boolean _update, boolean _delete) {
super();
this._create = _create;
this._read = _read;
this._update = _update;
this._delete = _delete;
}
getters + setters + toString()
CustomerDomainPermission Class:
@Entity
public class CustomerDomainPermission extends Permission {
public CustomerDomainPermission() {
super();
}
}
ProductDomainPermission Class:
@Entity
public class ProductDomainPermission extends Permission {
public ProductDomainPermission() {
super();
}
}
JUnit Test:
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class PermissionTest {
Users user;
Permission cd;
Permission pd;
DomainPermissions d;
@BeforeAll
void createUser() {
cd = new CustomerDomainPermission();
cd.set_read(true);
cd.set_create(false);
cd.set_delete(false);
cd.set_update(true);
pd = new ProductDomainPermission();
d = new DomainPermissions();
d.setCustomerDomain(cd);
d.setProductDomain(pd);
user = new Users();
user.setFirstname("Alfred");
user.setLastname("Hitchcock");
user.setDomainPermissions(d);
}
@Test
@DisplayName("When creating Customer Domain Permissions")
void customerDomainPermissionsTest() {
assertAll(
() -> assertEquals(false, user.getDomainPermissions().getCustomerDomain().is_create()),
() -> assertEquals(true, user.getDomainPermissions().getCustomerDomain().is_read()),
() -> assertEquals(true, user.getDomainPermissions().getCustomerDomain().is_update()),
() -> assertEquals(false, user.getDomainPermissions().getCustomerDomain().is_delete())
);
}
@Test
@DisplayName("When creating Product Domain Permissions")
void productDomainPermissionsTest() {
assertAll(
() -> assertEquals(false, user.getDomainPermissions().getProductDomain().is_create()),
() -> assertEquals(false, user.getDomainPermissions().getProductDomain().is_read()),
() -> assertEquals(false, user.getDomainPermissions().getProductDomain().is_update()),
() -> assertEquals(false, user.getDomainPermissions().getProductDomain().is_delete())
);
}
@Test
@DisplayName("Is CustomerDomainPermission an instance of correct sub class")
void customerDomainPermissionInstantiationCheck() {
assertTrue(cd instanceof CustomerDomainPermission);
}
@Test
@DisplayName("Is ProductDomainPermission an instance of correct sub class")
void productDomainPermissionInstantiationCheck() {
assertTrue(pd instanceof ProductDomainPermission);
}
@Test
@DisplayName("Does DomainPermissions Type Include CustomerDomainPermission")
void domainPermissionsTypeCustomerDomainPermissionContentTest() {
assertEquals(cd, d.getCustomerDomain());
}
@Test
@DisplayName("Does DomainPermissions Type Include ProductDomainPermission")
void domainPermissionTypeProductDomainPermissionContentTest() {
assertEquals(pd, d.getProductDomain());
}
@Test
@DisplayName("Does the User have correct DomainPermissions")
void userDomainPermissionsTest() {
assertEquals(d, user.getDomainPermissions());
}
Output:
![](https://blog.gunlerveisler.gen.tr/wp-content/uploads/2020/08/junit01.jpg)
Database Tables:
![](https://blog.gunlerveisler.gen.tr/wp-content/uploads/2020/08/allTables.jpg)
![](https://blog.gunlerveisler.gen.tr/wp-content/uploads/2020/08/users.jpg)
![](https://blog.gunlerveisler.gen.tr/wp-content/uploads/2020/08/DomainPermissions.jpg)
![](https://blog.gunlerveisler.gen.tr/wp-content/uploads/2020/08/customerDomainPermission.jpg)
![](https://blog.gunlerveisler.gen.tr/wp-content/uploads/2020/08/productDomainPermission.jpg)