Skip to content

Commit

Permalink
Security / Build permission filter based on user info.
Browse files Browse the repository at this point in the history
  • Loading branch information
fxprunayre committed Dec 30, 2020
1 parent 6bf1fe5 commit c12f5fa
Show file tree
Hide file tree
Showing 11 changed files with 147 additions and 31 deletions.
12 changes: 12 additions & 0 deletions modules/library/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@
Slf4j is used as a logging facade, and by default the logs will be redirected
onto the standard output.


Create a topic in the class and use the log:
```java
@Slf4j(topic = "org.fao.geonet.indexing.tasks")
public class IndexingService {
...
log.info(String.format(
"Index %s removed.",
index));
```


If one need to tweak the log level, it can be done by modifying the
`bootstrap.yml` file of the spring-boot application, e.g.:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -669,10 +669,18 @@ private UserInfo getUserInfo() {
GrantedAuthority authority = authentication.getAuthorities().stream().findFirst().get();
if (authority.getAuthority().equals("gn")) {
OAuth2UserAuthority userAuthority = (OAuth2UserAuthority) authority;
Map<String, Object> attributes = userAuthority.getAttributes();

String highestProfile = (String) attributes.get("highest_profile");
if (StringUtils.isNotEmpty(highestProfile)) {
userInfo.setHighestProfile(Profile.valueOf(highestProfile).name());
}

userInfo.setUserId((Integer) attributes.get("user_id"));
userInfo.setViewingGroups(
(List<Integer>) userAuthority.getAttributes().get(Profile.Reviewer.name()));
(List<Integer>) attributes.get(Profile.Reviewer.name()));
userInfo.setEditingGroups(
(List<Integer>) userAuthority.getAttributes().get(Profile.Editor.name()));
(List<Integer>) attributes.get(Profile.Editor.name()));
}

return userInfo;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public String buildQueryFilter(String type, UserInfo userInfo) {
*/
private String buildPermissionsFilter(UserInfo userInfo) {
// If admin you can see all
if (Profile.Administrator.equals(userInfo.getProfile())) {
if (Profile.Administrator.name().equals(userInfo.getHighestProfile())) {
return "*";
} else {
// op0 (ie. view operation) contains one of the ids of your groups
Expand All @@ -68,15 +68,15 @@ private String buildPermissionsFilter(UserInfo userInfo) {
.collect(Collectors.joining(" OR "));
String operationFilter = String.format("op%d:(%s)", ReservedOperation.view.getId(), ids);


String ownerFilter = "";
if (userInfo.isAuthenticated()) {
if (userInfo.isAuthenticated() && userInfo.getUserId() != null) {
// OR you are owner
ownerFilter = String.format("owner:%d", userInfo.getUserId());
String ownerFilter = String.format("owner:%d", userInfo.getUserId());
// OR member of groupOwner
// TODOES
return String.format("(%s %s)", operationFilter, ownerFilter);
} else {
return operationFilter;
}
return String.format("(%s %s)", operationFilter, ownerFilter).trim();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ public class UserInfo {
@Setter
private String userName;

@Getter
@Setter
private String highestProfile;

@Getter
@Setter
private List<Integer> viewingGroups = new ArrayList<>();
Expand All @@ -41,15 +45,6 @@ public boolean isAuthenticated() {
&& (!userName.equalsIgnoreCase("anonymousUser")));
}

/**
* Retrieve the user profile.
*
* @return User profile.
*/
public String getProfile() {
return "";
}

/**
* Retrieve the user groups.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ private boolean isOwner(UserInfo userInfo, Integer owner, Integer groupOwner) {
}

//--- check if the user is an administrator
final Profile profile = Profile.findProfileIgnoreCase(userInfo.getProfile());
final Profile profile = Profile.findProfileIgnoreCase(userInfo.getHighestProfile());
if (profile == Profile.Administrator) {
return true;
}
Expand Down
17 changes: 17 additions & 0 deletions modules/services/authorizing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ GeoNetwork does not need to be started, but it MUST have started once to create
1. Get an authentication token

```shell script
USERNAME=editor
PASSWORD=aaaaaa
USERNAME=admin
PASSWORD=admin
gn_token=$( \
Expand Down Expand Up @@ -42,8 +44,23 @@ echo $gn_token
3. The token is used to access services

```shell script
docker-compose up -d search

gn_auth_header=$(echo "Authorization: Bearer $gn_token")

curl 127.0.0.1:9900/search/secured -H "$gn_auth_header"
```

The server return user details:

```
curl 127.0.0.1:9900/search/secured -H "$gn_auth_header"
You are authenticated as editor
Authorities gn
* UserAdmin:[]
* highest_profile:Editor
* Editor:[100]
* Reviewer:[]
* RegisteredUser:[101]
```

Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,16 @@
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;

public class GnUserDetailsService implements UserDetailsService {
public static final String HIGHEST_PROFILE = "highest_profile";
public static final String USER_ID = "user_id";
public static final String GN_AUTHORITY = "gn";

@Autowired
private UserRepository userRepository;

@Autowired
private UserGroupRepository userGroupRepository;

@Autowired
private GroupRepository groupRepository;

private boolean checkUserNameOrEmail = true;

@Override
Expand All @@ -61,12 +61,13 @@ public UserDetails loadUserByUsername(String userNameOrEmail) throws UsernameNot
Collectors.mapping(t -> t.getValue(), Collectors.toList())
));
Map<String, Object> attributes = (Map<String, Object>) (Object) attributesToCast;
attributes.put("highest_profile", user.getProfile().name());
attributes.put(USER_ID, user.getId());
attributes.put(HIGHEST_PROFILE, user.getProfile().name());
attributes.putIfAbsent(Profile.UserAdmin.name(), Collections.emptyList());
attributes.putIfAbsent(Profile.Reviewer.name(), Collections.emptyList());
attributes.putIfAbsent(Profile.RegisteredUser.name(), Collections.emptyList());
attributes.putIfAbsent(Profile.Editor.name(), Collections.emptyList());
OAuth2UserAuthority authority = new OAuth2UserAuthority("gn", attributes);
OAuth2UserAuthority authority = new OAuth2UserAuthority(GN_AUTHORITY, attributes);

return org.springframework.security.core.userdetails.User
.withUsername(user.getUsername())
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.fao.geonet.authorizing;

import static org.fao.geonet.authorizing.GnUserDetailsService.HIGHEST_PROFILE;
import static org.fao.geonet.authorizing.GnUserDetailsService.USER_ID;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

Expand Down Expand Up @@ -45,10 +47,12 @@ public void nominal() {
Group group = new Group();
group.setName("csc");
groupRepository.save(group);

User user = new User();
user.setUsername("csc_editor");
user.setProfile(Profile.Editor);
userRepository.save(user);

UserGroup userGroup = new UserGroup();
userGroup.setGroup(group);
userGroup.setUser(user);
Expand All @@ -58,17 +62,20 @@ public void nominal() {
UserDetails userDetails = toTest.loadUserByUsername("csc_editor");

assertEquals("csc_editor", userDetails.getUsername());

assertEquals(1, userDetails.getAuthorities().size());
assertTrue(userDetails.getAuthorities().stream().findFirst().get() instanceof OAuth2UserAuthority);
OAuth2UserAuthority authority = (OAuth2UserAuthority) userDetails.getAuthorities().stream().findFirst().get();
assertEquals("gn", authority.getAuthority());

Map<String, Object> attributes = authority.getAttributes();
assertEquals(5, attributes.size());
assertEquals(6, attributes.size());
assertEquals(Arrays.asList(group.getId()), attributes.get(Profile.Editor.name()));
assertEquals(Collections.emptyList(), attributes.get(Profile.Reviewer.name()));
assertEquals(Collections.emptyList(), attributes.get(Profile.UserAdmin.name()));
assertEquals(Collections.emptyList(), attributes.get(Profile.RegisteredUser.name()));
assertEquals(Profile.Editor.name(), attributes.get("highest_profile"));
assertEquals(Profile.Editor.name(), attributes.get(HIGHEST_PROFILE));
assertEquals(100, attributes.get(USER_ID));
}

@Configuration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
package org.fao.geonet.searching.controller;

import java.security.Principal;
import java.util.Map;
import java.util.Optional;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.core.user.OAuth2UserAuthority;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
Expand All @@ -18,7 +20,7 @@
public class SecurityTesterController {

/**
* A simple secured endpoint returning username.
* A simple secured endpoint returning user details stored in JWT.
*/
@RequestMapping("/search/secured")
public String search(
Expand All @@ -30,9 +32,18 @@ public String search(
Optional<GrantedAuthority> authority =
oauth2Authentication.getAuthorities()
.stream().findFirst();
return String.format(
"Search service called. You are authenticated as %s. Authorities: %s",
name,
authority.isPresent() ? authority.get().getAuthority() : "");
if (authority.isPresent() && authority.get().getAuthority().equals("gn")) {
OAuth2UserAuthority oauthAuthority = (OAuth2UserAuthority) authority.get();
Map<String, Object> attributes = oauthAuthority.getAttributes();
StringBuilder message = new StringBuilder();
message.append(String.format("You are authenticated as %s\n", name));
message.append(String.format("Authorities %s\n", oauthAuthority.getAuthority()));
attributes.forEach((k, v) -> {
message.append(" * ").append(k).append(":").append(v).append("\n");
});
return message.toString();
} else {
return String.format("No authority found. User is %s", name);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ eureka:
logging:
level:
org.springframework.security: DEBUG

org.fao.geonet.searching: DEBUG

---
# Use this profile when running on a GN4 database on localhost and Elasticsearch index.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/**
* (c) 2020 Open Source Geospatial Foundation - all rights reserved This code is licensed under the
* GPL 2.0 license, available at the root application directory.
*/

package org.fao.geonet.common.search;

import org.fao.geonet.common.search.domain.Profile;
import org.fao.geonet.common.search.domain.UserInfo;
import org.junit.Before;
import org.junit.Test;

import java.util.Arrays;

import static org.junit.jupiter.api.Assertions.assertTrue;


public class FilterBuilderTest {

FilterBuilder filterBuilder;

@Before
public void setup() {
this.filterBuilder = new FilterBuilder();
}

@Test
public void testFilterType() {
UserInfo userInfo = new UserInfo();
String queryFilter = filterBuilder.buildQueryFilter("", userInfo);
assertTrue(queryFilter.contains("op0:(1) AND (draft:n OR draft:e)"));
queryFilter = filterBuilder.buildQueryFilter("metadata", userInfo);
assertTrue(queryFilter.contains("op0:(1) AND (isTemplate:n) AND (draft:n OR draft:e)"));
queryFilter = filterBuilder.buildQueryFilter("template", userInfo);
assertTrue(queryFilter.contains("op0:(1) AND (isTemplate:y) AND (draft:n OR draft:e)"));
queryFilter = filterBuilder.buildQueryFilter("subtemplate", userInfo);
assertTrue(queryFilter.contains("op0:(1) AND (isTemplate:s) AND (draft:n OR draft:e)"));
}

@Test
public void testFilterForAnonymousUser() {
UserInfo userInfo = new UserInfo();
String queryFilter = filterBuilder.buildQueryFilter("metadata", userInfo);
assertTrue(queryFilter.contains("op0:(1) AND (isTemplate:n) AND (draft:n OR draft:e)"));
}

@Test
public void testFilterForAdministrator() {
UserInfo userInfo = new UserInfo();
userInfo.setHighestProfile(Profile.Administrator.name());
String queryFilter = filterBuilder.buildQueryFilter("metadata", userInfo);
assertTrue(queryFilter.contains("* AND (isTemplate:n) AND (draft:n OR draft:e)"));
}

@Test
public void testFilterForEditor() {
UserInfo userInfo = new UserInfo();
userInfo.setUserName("Dad");
userInfo.setUserId(999);
userInfo.setHighestProfile(Profile.Editor.name());
userInfo.setEditingGroups(Arrays.asList(new Integer[]{10, 11, 12}));
String queryFilter = filterBuilder.buildQueryFilter("metadata", userInfo);
assertTrue(queryFilter.contains("(op0:(1 OR 10 OR 11 OR 12) owner:999) AND (isTemplate:n) AND (draft:n OR draft:e)"));
}
}

0 comments on commit c12f5fa

Please sign in to comment.