Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
5bb0f79
Anonymous user can read an MD given appropriate security context perm…
cmangeat Sep 11, 2025
0a450d0
Fix typo
cmangeat Sep 11, 2025
11a7239
migrate from uuid to id, keep it simple stupid (avoid try/catch and d…
cmangeat Sep 19, 2025
4fb8dc6
introduce AnonymousAccessLink in domain and grantViewAuthorityFilter …
cmangeat Sep 19, 2025
fd648df
nominal test
cmangeat Sep 19, 2025
37db114
spring security filter granting view md according to registred hash a…
cmangeat Sep 20, 2025
afd6cd4
need for uuid in GrantedWiewMdSecurity to patch EsFilterBuilder
cmangeat Sep 20, 2025
6d090ba
rename tests
cmangeat Sep 23, 2025
20e26bf
es http proxy take ViewMdGrantedAuthority into account
cmangeat Sep 23, 2025
5e4fff8
create anonymousAccessLink
cmangeat Sep 23, 2025
18117a3
list anonymousAccessLink
cmangeat Sep 23, 2025
af88e90
delete anonymousAccessLink
cmangeat Sep 23, 2025
7116cc1
swagger for api
cmangeat Sep 23, 2025
3fea366
prefer hasAuthority on as hasRole
cmangeat Sep 23, 2025
08d2050
wip override HttpSessionSecurityContextRepository trust resolver so t…
cmangeat Sep 24, 2025
5b72ea7
refactor
cmangeat Sep 24, 2025
229cce9
randomize hash
cmangeat Sep 24, 2025
c536924
rename test method
cmangeat Oct 7, 2025
2f285a3
delete anonymous link using its md uuid
cmangeat Oct 7, 2025
eb66068
add converter / 'encryptor' for hash
cmangeat Oct 7, 2025
fc57473
introduce service and dto
cmangeat Oct 10, 2025
aa29686
add headers
cmangeat Oct 10, 2025
5f7ed7a
service nominal test
cmangeat Oct 10, 2025
401f821
always hide hash but when create
cmangeat Oct 10, 2025
7c21444
by now, api return dto, not entity
cmangeat Oct 10, 2025
9e6a2e9
test for delete with service
cmangeat Oct 10, 2025
9d6f521
does a link exist service
cmangeat Oct 10, 2025
030adb4
refactor test
cmangeat Oct 10, 2025
7ca608e
does a link exist api
cmangeat Oct 10, 2025
f86a5f0
refactor test
cmangeat Oct 10, 2025
7c8f587
do not include null field (hash) when json serialize
cmangeat Oct 10, 2025
e8ed951
delete and create now take a md uuid as path param, no body to provid…
cmangeat Oct 10, 2025
7b3b5cb
refactor test
cmangeat Oct 10, 2025
a231d19
service to return links bound with md info
cmangeat Oct 13, 2025
adc1fce
fix test
cmangeat Oct 13, 2025
9040494
avoid bad url forging when contact logo from existing org
cmangeat Oct 15, 2025
55a7bd3
at this point, two links cannot give access to the same md
cmangeat Oct 15, 2025
2e4079b
trigger link deletion when md published
cmangeat Oct 15, 2025
18c2ab2
make anonymous session not anonymous for httpSessionSecurityContextRe…
cmangeat Oct 15, 2025
1704440
create anonymous access link does not operate for published md
cmangeat Oct 15, 2025
0d26eeb
encrypt hash in db
cmangeat Oct 17, 2025
2f4a5ab
missing headers
cmangeat Oct 17, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions core/src/main/java/org/fao/geonet/kernel/AccessManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import org.fao.geonet.constants.Geonet;
import org.fao.geonet.domain.*;
import org.fao.geonet.kernel.datamanager.IMetadataUtils;
import org.fao.geonet.kernel.security.ViewMdGrantedAuthority;
import org.fao.geonet.kernel.setting.SettingManager;
import org.fao.geonet.kernel.setting.Settings;
import org.fao.geonet.repository.*;
Expand All @@ -40,9 +41,11 @@
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.security.core.context.SecurityContextHolder;

import java.sql.SQLException;
import java.util.*;
import java.util.stream.Stream;

import static org.fao.geonet.kernel.setting.Settings.SYSTEM_INTRANET_IP_SEPARATOR;
import static org.fao.geonet.kernel.setting.Settings.SYSTEM_METADATAPRIVS_PUBLICATIONBYGROUPOWNERONLY;
Expand Down Expand Up @@ -79,6 +82,13 @@ public class AccessManager {
@Autowired
UserRepository userRepository;

public static Stream<AnonymousAccessLink> anonymousAccessLinkStreamFromSecurityContext() {
return SecurityContextHolder.getContext().getAuthentication().getAuthorities().stream()
.filter(ViewMdGrantedAuthority.class::isInstance)
.map(ViewMdGrantedAuthority.class::cast)
.map(ViewMdGrantedAuthority::getAnonymousAccessLink);
}

/**
* Given a user(session) a list of groups and a metadata returns all operations that user can
* perform on that metadata (an set of OPER_XXX as keys). If the user is authenticated the
Expand Down Expand Up @@ -110,6 +120,8 @@ public Set<Operation> getOperations(ServiceContext context, String mdId, String
}
}

processViewMdGrantedAuthorityIfAny(mdId, results);

return results;
}

Expand Down Expand Up @@ -657,4 +669,13 @@ private long getAddress(String ip) {
private boolean isUserAuthenticated(UserSession us) {
return us != null && us.isAuthenticated();
}

private void processViewMdGrantedAuthorityIfAny(String mdId, Set<Operation> results) {
anonymousAccessLinkStreamFromSecurityContext()
.map(AnonymousAccessLink::getMetadataId)
.map(id -> Integer.toString(id))
.filter(mdId::equals)
.forEach(grantedAuthority -> results.add(operationRepository.findReservedOperation(ReservedOperation.view)));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,19 @@
import jeeves.server.context.ServiceContext;
import org.apache.commons.lang.StringUtils;
import org.fao.geonet.NodeInfo;
import org.fao.geonet.domain.AnonymousAccessLink;
import org.fao.geonet.domain.Profile;
import org.fao.geonet.domain.ReservedOperation;
import org.fao.geonet.domain.Source;
import org.fao.geonet.kernel.AccessManager;
import org.fao.geonet.kernel.security.ViewMdGrantedAuthority;
import org.fao.geonet.repository.SourceRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
Expand All @@ -37,23 +42,29 @@ public static String buildPermissionsFilter(ServiceContext context) throws Excep
if (Profile.Administrator.equals(userSession.getProfile())) {
return "*:*";
} else {
List<String> filters = new ArrayList<>();

// op0 (ie. view operation) contains one of the ids of your groups
Set<Integer> groups = accessManager.getUserGroups(userSession, context.getIpAddress(), false);
final String ids = groups.stream()
.map(Object::toString)
.map(e -> e.replace("-", "\\\\-"))
.collect(Collectors.joining(" OR "));
String operationFilter = String.format("op%d:(%s)", ReservedOperation.view.getId(), ids);
filters.add(String.format("op%d:(%s)", ReservedOperation.view.getId(), ids));


String ownerFilter = "";
if (userSession.getUserIdAsInt() > 0) {
// OR you are owner
ownerFilter = String.format("owner:%d", userSession.getUserIdAsInt());
filters.add(String.format("owner:%d", userSession.getUserIdAsInt()));
// OR member of groupOwner
// TODOES
} else {
filters.add(AccessManager.anonymousAccessLinkStreamFromSecurityContext()
.map(AnonymousAccessLink::getMetadataUuid)
.map(uuid -> String.format("_id:%s", uuid))
.collect(Collectors.joining(" ")));
}
return String.format("(%s %s)", operationFilter, ownerFilter).trim();
return String.format("(%s)", String.join(" ", filters).trim());
}
}
/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright (C) 2001-2025 Food and Agriculture Organization of the
* United Nations (FAO-UN), United Nations World Food Programme (WFP)
* and United Nations Environment Programme (UNEP)
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or (at
* your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
*
* Contact: Jeroen Ticheler - FAO - Viale delle Terme di Caracalla 2,
* Rome - Italy. email: [email protected]
*/

package org.fao.geonet.kernel.security;

import org.fao.geonet.domain.AnonymousAccessLink;
import org.springframework.security.core.GrantedAuthority;

public final class ViewMdGrantedAuthority implements GrantedAuthority {
private static final long serialVersionUID = -5004823258126237689L;

private AnonymousAccessLink anonymousAccessLink;

public AnonymousAccessLink getAnonymousAccessLink() {
return anonymousAccessLink;
}

public ViewMdGrantedAuthority setAnonymousAccessLink(AnonymousAccessLink anonymousAccessLink) {
this.anonymousAccessLink = anonymousAccessLink;
return this;
}

@Override
public String getAuthority() {
return anonymousAccessLink.getMetadataUuid();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright (C) 2001-2025 Food and Agriculture Organization of the
* United Nations (FAO-UN), United Nations World Food Programme (WFP)
* and United Nations Environment Programme (UNEP)
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or (at
* your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
*
* Contact: Jeroen Ticheler - FAO - Viale delle Terme di Caracalla 2,
* Rome - Italy. email: [email protected]
*/

package org.fao.geonet.security;

import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.GenericFilterBean;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;

public class AuthenticatedUserFilter extends GenericFilterBean {

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
if (SecurityContextHolder.getContext().getAuthentication() != null) {
filterChain.doFilter(servletRequest, servletResponse);
}
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* Copyright (C) 2001-2025 Food and Agriculture Organization of the
* United Nations (FAO-UN), United Nations World Food Programme (WFP)
* and United Nations Environment Programme (UNEP)
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or (at
* your option) any later version.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
*
* Contact: Jeroen Ticheler - FAO - Viale delle Terme di Caracalla 2,
* Rome - Italy. email: [email protected]
*/

package org.fao.geonet.security;

import org.fao.geonet.domain.AnonymousAccessLink;
import org.fao.geonet.kernel.security.ViewMdGrantedAuthority;
import org.fao.geonet.repository.AnonymousAccessLinkRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.authentication.AuthenticationTrustResolver;
import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.web.filter.GenericFilterBean;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class GrantViewMdAuthorityFilter extends GenericFilterBean {

@Autowired
AnonymousAccessLinkRepository anonymousAccessLinkRepository;

private HttpSessionSecurityContextRepository repo;

public GrantViewMdAuthorityFilter(HttpSessionSecurityContextRepository httpSessionSecurityContextRepository) {
httpSessionSecurityContextRepository.setTrustResolver(new AuthenticationTrustResolver() {
AuthenticationTrustResolver delegate = new AuthenticationTrustResolverImpl();

@Override
public boolean isAnonymous(Authentication authentication) {
if (authentication.getAuthorities().stream().anyMatch(ViewMdGrantedAuthority.class::isInstance)) {
return false;
}
return delegate.isAnonymous(authentication);
}

@Override
public boolean isRememberMe(Authentication authentication) {
return delegate.isRememberMe(authentication);
}
});
}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
boolean isAnonymous = authentication instanceof AnonymousAuthenticationToken;
if (!isAnonymous) {
filterChain.doFilter(servletRequest, servletResponse);
return;
}
String hash = servletRequest.getParameter("hash");
AnonymousAccessLink authority = anonymousAccessLinkRepository.findOneByHash(hash);
if (authority == null) {
filterChain.doFilter(servletRequest, servletResponse);
return;
}
boolean alreadyGranted = authentication.getAuthorities().stream() //
.filter(ViewMdGrantedAuthority.class::isInstance) //
.map(ViewMdGrantedAuthority.class::cast) //
.map(ViewMdGrantedAuthority::getAnonymousAccessLink) //
.anyMatch(authority::equals);
if (alreadyGranted){
filterChain.doFilter(servletRequest, servletResponse);
return;
}
List<GrantedAuthority> authorities = new ArrayList<>(authentication.getAuthorities());
authorities.add(new ViewMdGrantedAuthority().setAnonymousAccessLink(authority));
AnonymousAuthenticationToken token = new AnonymousAuthenticationToken(authentication.getName(), authentication.getPrincipal(), authorities);
SecurityContextHolder.getContext().setAuthentication(token);
filterChain.doFilter(servletRequest, servletResponse);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public abstract class AbstractIntegrationTestWithMockedSingletons extends Abstra
private static final int TEST_OWNER_ID = 42;

@Autowired
private IMetadataManager metadataManager;
protected IMetadataManager metadataManager;

@Autowired
private SchemaManager schemaManager;
Expand Down
Loading
Loading