Skip to content

Commit

Permalink
Merge pull request #103 from folded-ear/profile-from-graphql
Browse files Browse the repository at this point in the history
Profile from graphql
  • Loading branch information
barneyb authored Sep 22, 2024
2 parents 66ec810 + 934ad04 commit 135ac5e
Show file tree
Hide file tree
Showing 11 changed files with 107 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -104,13 +104,13 @@ public GraphQLKickstartContext build(HttpServletRequest request, HttpServletResp
Map<Object, Object> map = new HashMap<>();
map.put(HttpServletRequest.class, request);
map.put(HttpServletResponse.class, response);
// Building the context happens on the main HTTP thread, so
// Spring Security's holder will always be available. Query
// execution may become asynchronous, rendering Spring's context
// unavailable. So grab the Principal now - though not the User
// object - so it's available to resolvers, without creating any
// mandates about transaction/session demarcation.
map.put(UserPrincipal.class, principalAccess.getUserPrincipal());
// Spring Security's holder will always be available at this
// point in execution. Get the Principal - if it exists - now,
// so it's available to resolvers. They can decide if its
// required or not, as well as inflate to a full User (so they
// deal with session and transaction demarcation).
principalAccess.findUserPrincipal()
.ifPresent(p -> map.put(UserPrincipal.class, p));
return new DefaultGraphQLContext(dataLoadRegistry, map);
}

Expand Down
15 changes: 9 additions & 6 deletions src/main/java/com/brennaswitzer/cookbook/graphql/Query.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
import com.brennaswitzer.cookbook.domain.AccessControlled;
import com.brennaswitzer.cookbook.domain.AccessLevel;
import com.brennaswitzer.cookbook.domain.User;
import com.brennaswitzer.cookbook.graphql.support.PrincipalUtil;
import com.brennaswitzer.cookbook.repositories.BaseEntityRepository;
import com.brennaswitzer.cookbook.util.UserPrincipalAccess;
import com.brennaswitzer.cookbook.repositories.UserRepository;
import graphql.kickstart.tools.GraphQLQueryResolver;
import graphql.schema.DataFetchingEnvironment;
import org.hibernate.Hibernate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
Expand All @@ -21,7 +23,7 @@ public class Query implements GraphQLQueryResolver {
private List<BaseEntityRepository<?>> repositories;

@Autowired
private UserPrincipalAccess userPrincipalAccess;
private UserRepository userRepo;

@Autowired
public FavoriteQuery favorite;
Expand All @@ -38,21 +40,22 @@ public class Query implements GraphQLQueryResolver {
@Autowired
public PlannerQuery planner;

Object getNode(Long id) {
Object getNode(Long id,
DataFetchingEnvironment env) {
return repositories.stream()
.map(r -> r.findById(id))
.filter(Optional::isPresent)
.map(Optional::get)
.map(Hibernate::unproxy)
.filter(it -> !(it instanceof AccessControlled ac) ||
ac.isPermitted(userPrincipalAccess.getUser(),
ac.isPermitted(getCurrentUser(env),
AccessLevel.VIEW))
.findFirst()
.orElse(null);
}

User getCurrentUser() {
return userPrincipalAccess.getUser();
User getCurrentUser(DataFetchingEnvironment env) {
return userRepo.getById(PrincipalUtil.from(env).getId());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
import com.brennaswitzer.cookbook.domain.Recipe;
import com.brennaswitzer.cookbook.graphql.loaders.IsFavorite;
import com.brennaswitzer.cookbook.graphql.loaders.IsFavoriteBatchLoader;
import com.brennaswitzer.cookbook.graphql.support.PrincipalUtil;
import com.brennaswitzer.cookbook.mapper.LabelMapper;
import com.brennaswitzer.cookbook.security.UserPrincipal;
import com.brennaswitzer.cookbook.services.favorites.FetchFavorites;
import graphql.kickstart.tools.GraphQLResolver;
import graphql.schema.DataFetchingEnvironment;
Expand Down Expand Up @@ -56,12 +56,10 @@ public List<String> labels(Recipe recipe) {

public CompletableFuture<Boolean> favorite(Recipe recipe,
DataFetchingEnvironment env) {
// Spring Security's principal is copied to the GraphQL context when it
// is safe. It's not safe to interrogate Spring here, though it does
// work in some situations.
UserPrincipal up = env.getGraphQlContext().get(UserPrincipal.class);
return env.<IsFavorite, Boolean>getDataLoader(IsFavoriteBatchLoader.class.getName())
.load(new IsFavorite(up.getId(), FavoriteType.RECIPE, recipe.getId()));
.load(new IsFavorite(PrincipalUtil.from(env).getId(),
FavoriteType.RECIPE,
recipe.getId()));
}

public Photo photo(Recipe recipe) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.brennaswitzer.cookbook.graphql.resolvers;

import com.brennaswitzer.cookbook.domain.User;
import com.brennaswitzer.cookbook.graphql.support.PrincipalUtil;
import com.brennaswitzer.cookbook.security.UserPrincipal;
import graphql.kickstart.tools.GraphQLResolver;
import graphql.schema.DataFetchingEnvironment;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.Objects;

@SuppressWarnings("unused") // component-scanned for graphql-java
@Component
public class UserResolver implements GraphQLResolver<User> {

public List<String> roles(User user,
DataFetchingEnvironment env) {
UserPrincipal principal = PrincipalUtil.from(env);
// if not the current user, create a new instance
if (!Objects.equals(principal.getId(), user.getId())) {
principal = UserPrincipal.create(user);
}
return principal.getAuthorities()
.stream()
.map(GrantedAuthority::getAuthority)
.filter(it -> it.startsWith("ROLE_"))
.map(it -> it.substring(5))
.toList();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.brennaswitzer.cookbook.graphql.support;

import com.brennaswitzer.cookbook.security.UserPrincipal;
import com.brennaswitzer.cookbook.util.NoUserPrincipalException;
import graphql.schema.DataFetchingEnvironment;

public class PrincipalUtil {

public static UserPrincipal from(DataFetchingEnvironment env) {
return env.getGraphQlContext()
.<UserPrincipal>getOrEmpty(UserPrincipal.class)
.orElseThrow(NoUserPrincipalException::new);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

import org.springframework.security.core.annotation.AuthenticationPrincipal;

import java.lang.annotation.*;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@AuthenticationPrincipal
public @interface CurrentUser {

}
@AuthenticationPrincipal(errorOnInvalidType = true)
public @interface CurrentUser {}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.brennaswitzer.cookbook.security;

import com.brennaswitzer.cookbook.domain.User;
import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
Expand All @@ -26,7 +27,9 @@ private static boolean isDeveloper(User user) {
}
// end "framework"

@Getter
private final Long id;
@Getter
private final String email;
private final Collection<? extends GrantedAuthority> authorities;
private Map<String, Object> attributes;
Expand Down Expand Up @@ -56,14 +59,6 @@ public static UserPrincipal create(User user, Map<String, Object> attributes) {
return userPrincipal;
}

public Long getId() {
return id;
}

public String getEmail() {
return email;
}

@Override
public String getUsername() {
return email;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

public class NoUserPrincipalException extends AuthenticationException {

public NoUserPrincipalException() {
this("No user principal found. Are you logged in?");
}

public NoUserPrincipalException(String msg) {
super(msg);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,20 @@
import com.brennaswitzer.cookbook.domain.User;
import com.brennaswitzer.cookbook.security.UserPrincipal;

import java.util.Optional;

public interface UserPrincipalAccess {

default Long getId() {
return getUserPrincipal().getId();
}

UserPrincipal getUserPrincipal();
Optional<UserPrincipal> findUserPrincipal();

default UserPrincipal getUserPrincipal() {
return findUserPrincipal()
.orElseThrow(NoUserPrincipalException::new);
}

default User getUser() {
throw new UnsupportedOperationException();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
import com.brennaswitzer.cookbook.domain.User;
import com.brennaswitzer.cookbook.repositories.UserRepository;
import com.brennaswitzer.cookbook.security.UserPrincipal;
import lombok.val;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;

import java.util.Optional;

@SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection")
@Component
public class UserPrincipalAccessImpl implements UserPrincipalAccess {
Expand All @@ -16,14 +18,16 @@ public class UserPrincipalAccessImpl implements UserPrincipalAccess {
private UserRepository userRepo;

@Override
public UserPrincipal getUserPrincipal() {
val auth = SecurityContextHolder
.getContext()
.getAuthentication();
if (!auth.isAuthenticated() || !auth.getAuthorities().contains(UserPrincipal.ROLE_USER)) {
throw new NoUserPrincipalException("No user principal found. Are you logged in?");
}
return (UserPrincipal) auth.getPrincipal();
public Optional<UserPrincipal> findUserPrincipal() {
return Optional.ofNullable(
SecurityContextHolder
.getContext()
.getAuthentication())
.filter(Authentication::isAuthenticated)
.filter(auth -> auth.getAuthorities()
.contains(UserPrincipal.ROLE_USER))
.map(Authentication::getPrincipal)
.map(UserPrincipal.class::cast);
}

@Override
Expand Down
7 changes: 4 additions & 3 deletions src/main/resources/graphqls/profile.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,11 @@ extend type Query {
getCurrentUser: User
}

type User {
type User implements Node {
id: ID!
name: String
email: String
email: String!
imageUrl: String
provider: String
provider: String!
roles: [String!]!
}

0 comments on commit 135ac5e

Please sign in to comment.