Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ACLs - Making use of resolved GeorchestraUser's roles along with the resolved authorities #89

Closed
wants to merge 7 commits into from
23 changes: 18 additions & 5 deletions docs/authzn.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,15 @@ Both standard and non-standard claims can be used to set the `GeorchestraUser`'s
|
|JSONPath expression to extract the user role names from the OIDC claims map

|`georchestra.gateway.security.oidc.claims.roles.json.split`
| false
|Whether to split parsed strings into multiple strings using `${georchestra.gateway.security.oidc.claims.roles.json.split-on}` as separator.
For example, if the expression evaluates to `ROLE_1, ROLE_2`, split it into two roles `[ROLE_1, ROLE_2]`.

|`georchestra.gateway.security.oidc.claims.roles.json.split-on`
| `,` (comma)
| String literal to use as separator for splitting string values evaluated by the JSONPath expression.

|`georchestra.gateway.security.oidc.claims.roles.uppercase`
| true
|Whether to return mapped role names as upper-case.
Expand Down Expand Up @@ -209,6 +218,7 @@ Take as example the following claims provided by an OIDC ID Token:
"GDI Planer",
"GDI Editor (exten)"
],
"permission": "GDI Planner, GDI Admin"
"PartyOrganisationID": "6007280321"
}
----
Expand Down Expand Up @@ -253,10 +263,13 @@ georchestra:
# }
#
# The resulting list of roles will be ["ORG_6007280321", "GDI_PLANER_EXTERN", "GDI_EDITOR_EXTERN"]
# and the request header will be `sec-roles: ROLE_ORG_6007280321;ROLE_GDI_PLANER;ROLE_GDI_EDITOR_EXTERN;ROLE_USER`
json.path:
- "$.concat(\"ORG_\", $.PartyOrganisationID)"
- "$.groups_json..['name']"
# and the request header will be `sec-roles: ROLE_ORG_6007280321;ROLE_GDI_PLANER;ROLE_GDI_EDITOR_EXTERN;ROLE_ADMIN;ROLE_USER`
json:
split: true
path:
- "$.concat(\"ORG_\", $.PartyOrganisationID)"
- "$.groups_json..['name']"
- "$.permission"
uppercase: true
normalize: true
append: true
Expand All @@ -265,7 +278,7 @@ georchestra:
Resulting in the following property values for the `GeorchestraUser` instance associated to the request:

```
roles = ["ROLE_GDI_PLANER", "ROLE_GDI_EDITOR_EXTERN"]
roles = ["ROLE_GDI_PLANER", "ROLE_GDI_EDITOR_EXTERN", "ROLE_GDI_ADMIN"]
organization = "6007280321"
```

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ protected Optional<GeorchestraUser> find(GeorchestraUser mappedUser) {
}

protected Optional<GeorchestraUser> findInternal(GeorchestraUser mappedUser) {
if (null != mappedUser.getOAuth2ProviderId()) {
return findByOAuth2ProviderId(mappedUser.getOAuth2ProviderId());
if ((null != mappedUser.getOAuth2Provider()) && (null != mappedUser.getOAuth2Uid())) {
return findByOAuth2Uid(mappedUser.getOAuth2Provider(), mappedUser.getOAuth2Uid());
}
return findByUsername(mappedUser.getUsername());
}
Expand All @@ -73,7 +73,7 @@ GeorchestraUser createIfMissing(GeorchestraUser mapped) {
}
}

protected abstract Optional<GeorchestraUser> findByOAuth2ProviderId(String oauth2ProviderId);
protected abstract Optional<GeorchestraUser> findByOAuth2Uid(String oauth2Provider, String oauth2Uid);

protected abstract Optional<GeorchestraUser> findByUsername(String username);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public class CreateAccountUserCustomizer implements GeorchestraUserCustomizerExt
final boolean isOauth2 = auth instanceof OAuth2AuthenticationToken;
final boolean isPreAuth = auth instanceof PreAuthenticatedAuthenticationToken;
if (isOauth2) {
Objects.requireNonNull(mappedUser.getOAuth2ProviderId(), "GeorchestraUser.oAuth2ProviderId is null");
Objects.requireNonNull(mappedUser.getOAuth2Uid(), "GeorchestraUser.oAuth2ProviderId is null");
}
if (isPreAuth) {
Objects.requireNonNull(mappedUser.getUsername(), "GeorchestraUser.username is null");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,8 @@ public LdapAccountsManager(Consumer<AccountCreated> eventPublisher, AccountDao a
}

@Override
protected Optional<GeorchestraUser> findByOAuth2ProviderId(@NonNull String oauth2ProviderId) {
return usersApi.findByOAuth2ProviderId(oauth2ProviderId).map(this::ensureRolesPrefixed);
protected Optional<GeorchestraUser> findByOAuth2Uid(@NonNull String oAuth2Provider, @NonNull String oAuth2Uid) {
return usersApi.findByOAuth2Uid(oAuth2Provider, oAuth2Uid).map(this::ensureRolesPrefixed);
}

@Override
Expand Down Expand Up @@ -145,10 +145,11 @@ private Account mapToAccountBrief(@NonNull GeorchestraUser preAuth) {
String phone = "";
String title = "";
String description = "";
final @javax.annotation.Nullable String oAuth2ProviderId = preAuth.getOAuth2ProviderId();
final @javax.annotation.Nullable String oAuth2Provider = preAuth.getOAuth2Provider();
final @javax.annotation.Nullable String oAuth2Uid = preAuth.getOAuth2Uid();

Account newAccount = AccountFactory.createBrief(username, password, firstName, lastName, email, phone, title,
description, oAuth2ProviderId);
description, oAuth2Provider, oAuth2Uid);
newAccount.setPending(false);
if (StringUtils.isEmpty(org) && !StringUtils.isBlank(defaultOrganization)) {
newAccount.setOrg(defaultOrganization);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,23 +44,28 @@ public RabbitmqAccountCreatedEventSender(AmqpTemplate eventTemplate) {
@EventListener(AccountCreated.class)
public void on(AccountCreated event) {
GeorchestraUser user = event.getUser();
final String oAuth2ProviderId = user.getOAuth2ProviderId();
if (null != oAuth2ProviderId) {
final String oAuth2Provider = user.getOAuth2Provider();
if (null != oAuth2Provider) {
String fullName = user.getFirstName() + " " + user.getLastName();
String localUid = user.getUsername();
String email = user.getEmail();
String provider = oAuth2ProviderId;
sendNewOAuthAccountMessage(fullName, email, provider);
String organization = user.getOrganization();
String oAuth2Uid = user.getOAuth2Uid();
sendNewOAuthAccountMessage(fullName, localUid, email, organization, oAuth2Provider, oAuth2Uid);
}
}

public void sendNewOAuthAccountMessage(String fullName, String email, String provider) {
// beans getting a reference to the sender
public void sendNewOAuthAccountMessage(String fullName, String localUid, String email, String organization,
String providerName, String providerUid) {
JSONObject jsonObj = new JSONObject();
jsonObj.put("uid", UUID.randomUUID());
jsonObj.put("subject", OAUTH2_ACCOUNT_CREATION);
jsonObj.put("username", fullName); // bean
jsonObj.put("email", email); // bean
jsonObj.put("provider", provider); // bean
jsonObj.put("fullName", fullName);
jsonObj.put("localUid", localUid);
jsonObj.put("email", email);
jsonObj.put("organization", organization);
jsonObj.put("providerName", providerName);
jsonObj.put("providerUid", providerUid);
eventTemplate.convertAndSend("routing-gateway", jsonObj.toString());// send
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,7 @@
*/
package org.georchestra.gateway.app;

import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;

import javax.annotation.PostConstruct;

import lombok.extern.slf4j.Slf4j;
import org.georchestra.gateway.security.GeorchestraUserMapper;
import org.georchestra.gateway.security.ldap.LdapConfigProperties;
import org.georchestra.security.model.GeorchestraUser;
Expand All @@ -36,54 +28,56 @@
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.oauth2.client.OAuth2ClientProperties;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.event.EventListener;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.core.env.Environment;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.registration.InMemoryClientRegistrationRepository;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.unit.DataSize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.reactive.result.view.Rendering;
import org.springframework.web.server.ServerWebExchange;

import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Mono;

import javax.annotation.PostConstruct;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.util.*;

@Controller
@Slf4j
@SpringBootApplication
@EnableConfigurationProperties(LdapConfigProperties.class)
public class GeorchestraGatewayApplication {

private @Autowired RouteLocator routeLocator;
private @Autowired GeorchestraUserMapper userMapper;

private @Autowired(required = false) LdapConfigProperties ldapConfigProperties;

private boolean ldapEnabled = false;

private @Autowired(required = false) OAuth2ClientProperties oauth2ClientConfig;
private @Value("${georchestra.gateway.headerEnabled:true}") boolean headerEnabled;
private boolean ldapEnabled = false;
private @Value("${georchestra.gateway.headerUrl:/header/}") String georchestraHeaderUrl;
private @Value("${georchestra.gateway.headerHeight:90}") String georchestraHeaderHeight;
private @Value("${georchestra.gateway.footerUrl:#{null}}") String georchestraFooterUrl;
private @Value("${spring.messages.basename:}") String messagesBasename;

public static void main(String[] args) {
SpringApplication.run(GeorchestraGatewayApplication.class, args);
}

@PostConstruct
void initialize() {
if (ldapConfigProperties != null) {
ldapEnabled = ldapConfigProperties.getLdap().values().stream().anyMatch((server -> server.isEnabled()));
}
}

public static void main(String[] args) {
SpringApplication.run(GeorchestraGatewayApplication.class, args);
}

@GetMapping(path = "/whoami", produces = "application/json")
@ResponseBody
public Mono<Map<String, Object>> whoami(Authentication principal, ServerWebExchange exchange) {
Expand All @@ -100,32 +94,28 @@ public Mono<Map<String, Object>> whoami(Authentication principal, ServerWebExcha
// Mono.just(Map.of(principal.getClass().getCanonicalName(), principal));
}

@GetMapping(path = "/logout")
public String logout(Model mdl) {
mdl.addAttribute("header_enabled", headerEnabled);
return "logout";
}

@GetMapping(path = "/login")
public String loginPage(@RequestParam Map<String, String> allRequestParams, Model mdl) {
public String loginPage(Model mdl) {
Map<String, String> oauth2LoginLinks = new HashMap<String, String>();
if (oauth2ClientConfig != null) {
oauth2ClientConfig.getRegistration().forEach((k, v) -> {
String clientName = Optional.ofNullable(v.getClientName()).orElse(k);
oauth2LoginLinks.put("/oauth2/authorization/" + k, clientName);
oauth2LoginLinks.put("/oauth2/authorization/" + k, v.getClientName());
});
}
mdl.addAttribute("header_enabled", headerEnabled);
mdl.addAttribute("header_url", georchestraHeaderUrl);
mdl.addAttribute("header_height", georchestraHeaderHeight);
mdl.addAttribute("footer_url", georchestraFooterUrl);
mdl.addAttribute("ldapEnabled", ldapEnabled);
mdl.addAttribute("oauth2LoginLinks", oauth2LoginLinks);
boolean expired = "expired_password".equals(allRequestParams.get("error"));
mdl.addAttribute("passwordExpired", expired);
boolean invalidCredentials = "invalid_credentials".equals(allRequestParams.get("error"));
mdl.addAttribute("invalidCredentials", invalidCredentials);

return "login";
}

@GetMapping(path = "/logout", produces = "text/html")
public Mono<Rendering.Builder<?>> logout(Authentication principal, ServerWebExchange exchange) {
return Mono.just(Rendering.view("logout"));
}

@EventListener(ApplicationReadyEvent.class)
public void onApplicationReady(ApplicationReadyEvent e) {
Environment env = e.getApplicationContext().getEnvironment();
Expand Down Expand Up @@ -161,4 +151,5 @@ public MessageSource messageSource() {
messageSource.setDefaultEncoding(StandardCharsets.UTF_8.name());
return messageSource;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import java.util.List;

import org.georchestra.security.model.GeorchestraUser;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;

Expand Down Expand Up @@ -64,7 +65,13 @@ public class RoleBasedAccessRule {
/**
* Role names that the authenticated user must be part of to be granted access
* to the intercepted URIs. The ROLE_ prefix is optional. For example, the role
* set [ROLE_USER, ROLE_AUDITOR] is equivalent to [USER, AUDITOR]
* set [ROLE_USER, ROLE_AUDITOR] is equivalent to [USER, AUDITOR]. The check
* will be performed against the {@link GeorchestraUser#getRoles()} list, as the
* effectively resolved role names, which will always be prefixed with
* {@literal ROLE_}, as opposed to
* {@link org.springframework.security.core.Authentication#getAuthorities()},
* where {@link GrantedAuthority} names are preserved as provided by the
* authentication manager that produced them.
*/
private List<String> allowedRoles = List.of();
}
Loading