Skip to content

Commit

Permalink
Merge pull request #12 from devondragon:issue-11-Enhance-Roles-and-Pr…
Browse files Browse the repository at this point in the history
…iveleges

Issue-11-Enhance-Roles-and-Priveleges
  • Loading branch information
devondragon authored Aug 6, 2022
2 parents a2b6d97 + 1880fdf commit 367151c
Show file tree
Hide file tree
Showing 14 changed files with 344 additions and 118 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -123,4 +123,6 @@ gradle-app.setting
!/config/README.md
!/config/*.example

application-local.yml


2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ CSRF is enabled by default and the example jQuery AJAX calls pass the CSRF token

An audit event and listener are implmented to allow for recording security events, or any type of event you like, and logging them to a seperate file. You can easily replace the logging listener with your own and store audit events in a database, publish them to a REST API, or anything else.

There is Role and Privilege setup service, which allows you to easily define Roles, associated Privileges, and Role inheritance hierachy in your application.yml. Check out the application.yml for the basic OOTB configuration, and look at the RolePrivilegeSetupService component. You can still create and leverage roles and privileges programatically, but this makes it easy to define and see the associations.


## How To Get Started

Expand Down
1 change: 0 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ plugins {
id 'org.springframework.boot' version '2.7.2'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
id 'eclipse'
}

group = 'com.digitalsanctuary.spring'
Expand Down
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
package com.digitalsanctuary.spring.user.persistence.model;

import java.util.Collection;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToMany;

import lombok.Data;

/**
Expand All @@ -17,6 +15,7 @@
@Entity
public class Privilege {


/** The id. */
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
Expand All @@ -25,7 +24,25 @@ public class Privilege {
/** The name. */
private String name;

/** The roles. */
/** The description of the role. */
private String description;

/** The roles which have this privilege. */
@ManyToMany(mappedBy = "privileges")
private Collection<Role> roles;

public Privilege() {
super();
}

public Privilege(final String name) {
super();
this.name = name;
}

public Privilege(final String name, final String description) {
super();
this.name = name;
this.description = description;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
@Data
@Entity
public class Role {

/** The id. */
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
Expand All @@ -30,9 +29,28 @@ public class Role {

/** The privileges. */
@ManyToMany
@JoinTable(name = "roles_privileges", joinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "privilege_id", referencedColumnName = "id"))
@JoinTable(name = "roles_privileges", joinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id"),
inverseJoinColumns = @JoinColumn(name = "privilege_id", referencedColumnName = "id"))
private Collection<Privilege> privileges;

/** The name. */
private String name;

private String description;


public Role() {
super();
}

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

public Role(final String name, final String description) {
super();
this.name = name;
this.description = description;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.digitalsanctuary.spring.user.service;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import javax.transaction.Transactional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;
import com.digitalsanctuary.spring.user.persistence.model.Privilege;
import com.digitalsanctuary.spring.user.persistence.model.Role;
import com.digitalsanctuary.spring.user.persistence.repository.PrivilegeRepository;
import com.digitalsanctuary.spring.user.persistence.repository.RoleRepository;
import com.digitalsanctuary.spring.user.util.RolesAndPrivilegesConfig;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Data
@Component
public class RolePrivilegeSetupService implements ApplicationListener<ContextRefreshedEvent> {
private boolean alreadySetup = false;

@Autowired
private RolesAndPrivilegesConfig rolesAndPrivilegesConfig;


@Autowired
private RoleRepository roleRepository;

@Autowired
private PrivilegeRepository privilegeRepository;


@Override
@Transactional
public void onApplicationEvent(final ContextRefreshedEvent event) {
if (alreadySetup) {
return;
}

log.debug("rolesAndPrivilegesConfig: " + rolesAndPrivilegesConfig);

for (Map.Entry<String, List<String>> entry : rolesAndPrivilegesConfig.getRolesAndPrivileges().entrySet()) {
final String roleName = entry.getKey();
final List<String> privileges = entry.getValue();
if (roleName != null && privileges != null) {
final List<Privilege> privilegesList = new ArrayList<Privilege>();
for (String privilegeName : privileges) {
privilegesList.add(createPrivilegeIfNotFound(privilegeName));
}
createRoleIfNotFound(roleName, privilegesList);
}

}
alreadySetup = true;
}

@Transactional
Privilege createPrivilegeIfNotFound(final String name) {
Privilege privilege = privilegeRepository.findByName(name);
if (privilege == null) {
privilege = new Privilege(name);
privilege = privilegeRepository.save(privilege);
}
return privilege;
}

@Transactional
Role createRoleIfNotFound(final String name, final Collection<Privilege> privileges) {
Role role = roleRepository.findByName(name);
if (role == null) {
role = new Role(name);
}
role.setPrivileges(privileges);
role = roleRepository.save(role);
return role;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package com.digitalsanctuary.spring.user.util;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import lombok.Data;

@Data
@Configuration
@ConfigurationProperties(prefix = "user")
public class RolesAndPrivilegesConfig {
private Map<String, List<String>> rolesAndPrivileges = new HashMap<String, List<String>>();

private List<String> roleHierarchy = new ArrayList<String>();

public String getRoleHierarchyString() {
StringBuffer roleHierarchyStringBuf = new StringBuffer();
if (roleHierarchy != null && !roleHierarchy.isEmpty()) {

for (String roleRelationship : roleHierarchy) {
roleHierarchyStringBuf.append(roleRelationship);
roleHierarchyStringBuf.append("\n");
}
}
// If we have built a list of hierarchy relationships, then return it.
if (roleHierarchyStringBuf.length() > 0) {
return roleHierarchyStringBuf.toString();
} else {
// otherwise, return null.
return null;
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

import java.util.ArrayList;
import java.util.Arrays;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.expression.SecurityExpressionHandler;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
Expand All @@ -21,13 +23,16 @@
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler;
import org.springframework.security.web.session.HttpSessionEventPublisher;
import com.digitalsanctuary.spring.user.service.LoginSuccessService;
import com.digitalsanctuary.spring.user.service.LogoutSuccessService;

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Data
@EqualsAndHashCode(callSuper = false)
@Configuration
Expand Down Expand Up @@ -97,6 +102,9 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private LogoutSuccessService logoutSuccessService;

@Autowired
private RolesAndPrivilegesConfig rolesAndPrivilegesConfig;

@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
Expand Down Expand Up @@ -134,20 +142,18 @@ protected void configure(HttpSecurity http) throws Exception {
disableCSRFURIs.removeAll(Arrays.asList("", null));

if (DEFAULT_ACTION_DENY.equals(getDefaultAction())) {
http.authorizeRequests().antMatchers(unprotectedURIs.toArray(new String[0])).permitAll().anyRequest()
.authenticated().and().formLogin().loginPage(loginPageURI).loginProcessingUrl(loginActionURI)
.successHandler(loginSuccessService).permitAll().and().logout().logoutUrl(logoutActionURI)
.invalidateHttpSession(true).logoutSuccessHandler(logoutSuccessService).deleteCookies("JSESSIONID")
http.authorizeRequests().antMatchers(unprotectedURIs.toArray(new String[0])).permitAll().anyRequest().authenticated().and().formLogin()
.loginPage(loginPageURI).loginProcessingUrl(loginActionURI).successHandler(loginSuccessService).permitAll().and().logout()
.logoutUrl(logoutActionURI).invalidateHttpSession(true).logoutSuccessHandler(logoutSuccessService).deleteCookies("JSESSIONID")
.permitAll();
if (disableCSRFURIs != null && disableCSRFURIs.size() > 0) {
http.csrf().ignoringAntMatchers(disableCSRFURIs.toArray(new String[0]));
}
} else if (DEFAULT_ACTION_ALLOW.equals(getDefaultAction())) {
http.authorizeRequests().antMatchers(protectedURIsArray).authenticated().antMatchers("/**").permitAll()
.and().formLogin().loginPage(loginPageURI).loginProcessingUrl(loginActionURI)
.successHandler(loginSuccessService).successHandler(loginSuccessService).and().logout()
.logoutUrl(logoutActionURI).invalidateHttpSession(true).logoutSuccessHandler(logoutSuccessService)
.deleteCookies("JSESSIONID").permitAll();
http.authorizeRequests().antMatchers(protectedURIsArray).authenticated().antMatchers("/**").permitAll().and().formLogin()
.loginPage(loginPageURI).loginProcessingUrl(loginActionURI).successHandler(loginSuccessService)
.successHandler(loginSuccessService).and().logout().logoutUrl(logoutActionURI).invalidateHttpSession(true)
.logoutSuccessHandler(logoutSuccessService).deleteCookies("JSESSIONID").permitAll();

if (disableCSRFURIs != null && disableCSRFURIs.size() > 0) {
http.csrf().ignoringAntMatchers(disableCSRFURIs.toArray(new String[0]));
Expand Down Expand Up @@ -178,4 +184,25 @@ public PasswordEncoder encoder() {
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
}

@Bean
public RoleHierarchy roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
roleHierarchy.setHierarchy(rolesAndPrivilegesConfig.getRoleHierarchyString());
log.debug("WebSecurityConfig.roleHierarchy:" + "roleHierarchy: {}", roleHierarchy.toString());
return roleHierarchy;
}

@Bean
public SecurityExpressionHandler<FilterInvocation> webExpressionHandler() {
DefaultWebSecurityExpressionHandler defaultWebSecurityExpressionHandler = new DefaultWebSecurityExpressionHandler();
defaultWebSecurityExpressionHandler.setRoleHierarchy(roleHierarchy());
return defaultWebSecurityExpressionHandler;
}

@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}

}
15 changes: 0 additions & 15 deletions src/main/resources/application-dev.properties

This file was deleted.

32 changes: 32 additions & 0 deletions src/main/resources/application-dev.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
debug: true

logging:
level:
com:
digitalsanctuary:
spring: DEBUG
org:
springframework:
security: DEBUG
web:
filter:
CommonsRequestLoggingFilter: DEBUG
nodeValue: DEBUG
spring:
mvc:
log-request-details: true
thymeleaf:
cache: false
devtools:
restart:
enabled: true

server:
servlet:
session:
cookie:
secure: false
user:
audit:
flushOnWrite: true

File renamed without changes.
Loading

0 comments on commit 367151c

Please sign in to comment.