diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/application/BaseApplication.java b/net-http-server/src/main/java/org/xbib/net/http/server/application/BaseApplication.java index 9f779c0..1561ad3 100644 --- a/net-http-server/src/main/java/org/xbib/net/http/server/application/BaseApplication.java +++ b/net-http-server/src/main/java/org/xbib/net/http/server/application/BaseApplication.java @@ -19,6 +19,7 @@ import org.xbib.net.UserProfile; import org.xbib.net.http.HttpAddress; import org.xbib.net.http.cookie.SameSite; import org.xbib.net.http.server.auth.BaseAttributes; +import org.xbib.net.http.server.auth.PersistUserProfileHandler; import org.xbib.net.http.server.route.BaseHttpRouterContext; import org.xbib.net.http.server.HttpHandler; import org.xbib.net.http.server.HttpRequestBuilder; @@ -178,7 +179,9 @@ public class BaseApplication implements Application { } httpRouterContext.addOpenHandler(newIncomingContextHandler(userProfileCodec, sessionCodec)); httpRouterContext.addCloseHandler(newOutgoingContextHandler()); - httpRouterContext.addCloseHandler(newPersistHandler(userProfileCodec, sessionCodec)); + for (HttpHandler httpHandler : newPersistHandlers(userProfileCodec, sessionCodec)) { + httpRouterContext.addCloseHandler(httpHandler); + } httpRouterContext.addCloseHandler(newOutgoingCookieHandler()); return httpRouterContext; } @@ -243,8 +246,8 @@ public class BaseApplication implements Application { ); } - protected HttpHandler newPersistHandler(Codec userProfileCodec, Codec sessionCodec) { - return new PersistSessionHandler(userProfileCodec, sessionCodec); + protected Collection newPersistHandlers(Codec userProfileCodec, Codec sessionCodec) { + return List.of(new PersistSessionHandler(sessionCodec), new PersistUserProfileHandler(userProfileCodec)); } @Override diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/auth/BaseUserProfile.java b/net-http-server/src/main/java/org/xbib/net/http/server/auth/BaseUserProfile.java index 29eb410..e13260b 100644 --- a/net-http-server/src/main/java/org/xbib/net/http/server/auth/BaseUserProfile.java +++ b/net-http-server/src/main/java/org/xbib/net/http/server/auth/BaseUserProfile.java @@ -129,6 +129,11 @@ public class BaseUserProfile implements UserProfile { this.attributes = attributes; } + @Override + public void updateAttribute(String key, Object value) { + attributes.put(key, value); + } + @Override public Attributes getAttributes() { return attributes; @@ -137,31 +142,23 @@ public class BaseUserProfile implements UserProfile { @Override public Map asMap() { TinyMap.Builder builder = TinyMap.builder(); - builder.put("name", getName()); - String userId = getUserId(); - if (userId == null) { - userId = ""; - } - builder.put("user_id", userId); - String eUserId = getEffectiveUserId(); - if (eUserId == null) { - eUserId = ""; - } - builder.put("e_user_id", eUserId); + builder.putIfNotNull(NAME, getName()); + builder.putIfNotNull(USER_ID, getUserId()); + builder.putIfNotNull(EFFECTIVE_USER_ID, getEffectiveUserId()); if (getRoles() != null && !getRoles().isEmpty()) { - builder.put("roles", getRoles()); + builder.put(ROLES, getRoles()); } if (getEffectiveRoles() != null && !getEffectiveRoles().isEmpty()) { - builder.put("e_roles", getEffectiveRoles()); + builder.put(EFFECTIVE_ROLES, getEffectiveRoles()); } if (getPermissions() != null && !getPermissions().isEmpty()) { - builder.put("perms", getPermissions()); + builder.put(PERMISSIONS, getPermissions()); } if (getEffectivePermissions() != null && !getEffectivePermissions().isEmpty()) { - builder.put("e_perms", getEffectivePermissions()); + builder.put(EFFECTIVE_PERMISSIONS, getEffectivePermissions()); } if (getAttributes() != null && !getAttributes().isEmpty()) { - builder.put("attrs", getAttributes()); + builder.put(ATTRIBUTES, getAttributes()); } return builder.build(); } @@ -169,39 +166,68 @@ public class BaseUserProfile implements UserProfile { @SuppressWarnings("unchecked") public static UserProfile fromMap(Map map) { BaseUserProfile userProfile = new BaseUserProfile(); - if (map.containsKey("name")) { - userProfile.setName((String) map.get("name")); + if (map.containsKey(NAME)) { + userProfile.setName((String) map.get(NAME)); } - if (map.containsKey("user_id")) { - String userId = (String) map.get("user_id"); - // empty user ID for better map transport, change it to null - if (userId != null && userId.isEmpty()) { - userId = null; - } + if (map.containsKey(USER_ID)) { + String userId = (String) map.get(USER_ID); userProfile.setUserId(userId); } - if (map.containsKey("e_user_id")) { - String eUserId = (String) map.get("e_user_id"); - // empty effective user ID for better map transport, change it to null - if (eUserId != null && eUserId.isEmpty()) { - eUserId = null; - } + if (map.containsKey(EFFECTIVE_USER_ID)) { + String eUserId = (String) map.get(EFFECTIVE_USER_ID); userProfile.setEffectiveUserId(eUserId); } - if (map.containsKey("roles")) { - userProfile.setRoles((Collection) map.get("roles")); + if (map.containsKey(ROLES)) { + userProfile.setRoles((Collection) map.get(ROLES)); } - if (map.containsKey("e_roles")) { - userProfile.setEffectiveRoles((Collection) map.get("e_roles")); + if (map.containsKey(EFFECTIVE_ROLES)) { + userProfile.setEffectiveRoles((Collection) map.get(EFFECTIVE_ROLES)); } - if (map.containsKey("perms")) { - userProfile.setPermissions((Collection) map.get("perms")); + if (map.containsKey(PERMISSIONS)) { + userProfile.setPermissions((Collection) map.get(PERMISSIONS)); } - if (map.containsKey("e_perms")) { - userProfile.setEffectivePermissions((Collection) map.get("e_perms")); + if (map.containsKey(EFFECTIVE_PERMISSIONS)) { + userProfile.setEffectivePermissions((Collection) map.get(EFFECTIVE_PERMISSIONS)); } - if (map.containsKey("attrs")) { - userProfile.setAttributes(new BaseAttributes((Map) map.get("attrs"))); + if (map.containsKey(ATTRIBUTES)) { + userProfile.setAttributes(new BaseAttributes((Map) map.get(ATTRIBUTES))); + } + return userProfile; + } + + @SuppressWarnings("unchecked") + public static UserProfile fromMap(UserProfile userProfile, Map map) { + if (userProfile.getName() == null && map.containsKey(NAME)) { + userProfile.setName((String) map.get(NAME)); + } + if (userProfile.getUserId() == null && map.containsKey(USER_ID)) { + String userId = (String) map.get(USER_ID); + userProfile.setUserId(userId); + } + if (map.containsKey(EFFECTIVE_USER_ID)) { + String eUserId = (String) map.get(EFFECTIVE_USER_ID); + userProfile.setEffectiveUserId(eUserId); + } + if ((userProfile.getRoles() == null || userProfile.getRoles().isEmpty()) && map.containsKey(ROLES)) { + userProfile.setRoles((Collection) map.get(ROLES)); + } + if (map.containsKey(EFFECTIVE_ROLES)) { + userProfile.setEffectiveRoles((Collection) map.get(EFFECTIVE_ROLES)); + } + if ((userProfile.getPermissions() == null || userProfile.getPermissions().isEmpty()) && map.containsKey(PERMISSIONS)) { + userProfile.setPermissions((Collection) map.get(PERMISSIONS)); + } + if (map.containsKey(EFFECTIVE_PERMISSIONS)) { + userProfile.setEffectivePermissions((Collection) map.get(EFFECTIVE_PERMISSIONS)); + } + if (map.containsKey(ATTRIBUTES)) { + if (userProfile.getAttributes() == null || userProfile.getAttributes().isEmpty()) { + userProfile.setAttributes(new BaseAttributes((Map) map.get(ATTRIBUTES))); + } else { + for (Map.Entry entry : ((Map) map.get(ATTRIBUTES)).entrySet()) { + userProfile.updateAttribute(entry.getKey(), entry.getValue()); + } + } } return userProfile; } diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/auth/FileJsonUserProfileCodec.java b/net-http-server/src/main/java/org/xbib/net/http/server/auth/FileJsonUserProfileCodec.java index 0f1270b..b17d64e 100644 --- a/net-http-server/src/main/java/org/xbib/net/http/server/auth/FileJsonUserProfileCodec.java +++ b/net-http-server/src/main/java/org/xbib/net/http/server/auth/FileJsonUserProfileCodec.java @@ -33,19 +33,13 @@ public class FileJsonUserProfileCodec implements Codec { public FileJsonUserProfileCodec(Path path) { this.path = path; this.lock = new ReentrantReadWriteLock(); - try { - Files.createDirectories(path); - } catch (IOException e) { - logger.log(Level.SEVERE, e.getMessage(), e); - throw new UncheckedIOException(e); - } } @Override - public UserProfile create(String key) throws IOException { - Objects.requireNonNull(key, "key must not be null"); + public UserProfile create(String userId) throws IOException { + Objects.requireNonNull(userId, "user id must not be null"); BaseUserProfile baseUserProfile = new BaseUserProfile(); - baseUserProfile.setUserId(key); + baseUserProfile.setUserId(userId); return baseUserProfile; } @@ -70,6 +64,7 @@ public class FileJsonUserProfileCodec implements Codec { ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); try { writeLock.lock(); + createPath(); PercentEncoder percentEncoder = PercentEncoders.getUnreservedEncoder(StandardCharsets.UTF_8); try (Writer writer = Files.newBufferedWriter(path.resolve(percentEncoder.encode(key)))) { writer.write(JsonUtil.toString(userProfile.asMap())); @@ -136,4 +131,15 @@ public class FileJsonUserProfileCodec implements Codec { logger.log(Level.WARNING, "i/o error while destroy: " + e.getMessage(), e); } } + + private void createPath() { + try { + if (!Files.exists(path)) { + Files.createDirectories(path); + } + } catch (IOException e) { + logger.log(Level.SEVERE, e.getMessage(), e); + throw new UncheckedIOException(e); + } + } } diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/auth/LoginAuthenticationHandler.java b/net-http-server/src/main/java/org/xbib/net/http/server/auth/LoginAuthenticationHandler.java index 56e5e86..6eac121 100644 --- a/net-http-server/src/main/java/org/xbib/net/http/server/auth/LoginAuthenticationHandler.java +++ b/net-http-server/src/main/java/org/xbib/net/http/server/auth/LoginAuthenticationHandler.java @@ -77,7 +77,10 @@ public class LoginAuthenticationHandler implements HttpHandler { } } - protected boolean authenticate(UserProfile userProfile, String username, String password, Request request) { + protected boolean authenticate(UserProfile userProfile, + String username, + String password, + Request request) { if (username == null) { logger.log(Level.FINE, "no username given for check, doing nothing"); return false; @@ -89,21 +92,36 @@ public class LoginAuthenticationHandler implements HttpHandler { Authenticator auth = securityRealm.getAuthenticator(); Authenticator.Context authContext = new Authenticator.Context(username, password, request); if (auth.authenticate(authContext)) { - logger.log(Level.FINE, "authenticated, augmenting user profile with " + authContext.getUsername()); + logger.log(Level.FINE, "authenticated as " + authContext.getUsername()); userProfile.setUserId(authContext.getUsername()); - UsersProvider.Context userContext = new UsersProvider.Context(username, null); + UsersProvider.Context userContext = new UsersProvider.Context(authContext.getUsername(), null); UserDetails userDetails = securityRealm.getUsersProvider().getUserDetails(userContext); - userProfile.setEffectiveUserId(userDetails.getEffectiveUserId()); userProfile.setName(userDetails.getName()); - GroupsProvider.Context groupContext = new GroupsProvider.Context(username, null); + if (userDetails.getEffectiveUserId() != null) { + userProfile.setEffectiveUserId(userDetails.getEffectiveUserId()); + } else { + userProfile.setEffectiveUserId(authContext.getUsername()); + } + GroupsProvider.Context groupContext = new GroupsProvider.Context(authContext.getUsername(), null); Collection groups = securityRealm.getGroupsProvider().getGroups(groupContext); for (String group : groups) { userProfile.addRole(group); } - logger.log(Level.FINE, "authentication success: userProfile = " + userProfile); + if (!userProfile.getUserId().equals(userProfile.getEffectiveUserId())) { + GroupsProvider.Context effectiveGroupContext = new GroupsProvider.Context(userProfile.getEffectiveUserId(), null); + for (String group : securityRealm.getGroupsProvider().getGroups(effectiveGroupContext)) { + userProfile.addEffectiveRole(group); + } + } else { + for (String group : groups) { + userProfile.addEffectiveRole(group); + } + } + // TODO permissions and effective permissions + logger.log(Level.FINE, "user and role setup completed for " + username); return true; } - logger.log(Level.FINE, "authentication failure"); + logger.log(Level.FINE, "authentication failure for user " + username); return false; } } diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/auth/PersistUserProfileHandler.java b/net-http-server/src/main/java/org/xbib/net/http/server/auth/PersistUserProfileHandler.java new file mode 100644 index 0000000..296e3b3 --- /dev/null +++ b/net-http-server/src/main/java/org/xbib/net/http/server/auth/PersistUserProfileHandler.java @@ -0,0 +1,40 @@ +package org.xbib.net.http.server.auth; + +import org.xbib.net.UserProfile; +import org.xbib.net.http.HttpResponseStatus; +import org.xbib.net.http.server.HttpException; +import org.xbib.net.http.server.HttpHandler; +import org.xbib.net.http.server.persist.Codec; +import org.xbib.net.http.server.route.HttpRouterContext; + +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class PersistUserProfileHandler implements HttpHandler { + + private static final Logger logger = Logger.getLogger(PersistUserProfileHandler.class.getName()); + + private final Codec userProfileCodec; + + public PersistUserProfileHandler(Codec userProfileCodec) { + this.userProfileCodec = userProfileCodec; + } + + @Override + public void handle(HttpRouterContext context) throws IOException { + UserProfile userProfile = context.getAttributes().get(UserProfile.class, "userprofile"); + if (userProfile != null && userProfile.getUserId() != null) { + try { + logger.log(Level.FINEST, "writing user profile id " + userProfile.getUserId()); + userProfileCodec.write(userProfile.getUserId(), userProfile); + } catch (Exception e) { + logger.log(Level.SEVERE, e.getMessage(), e); + throw new HttpException("unable to write user profile data", context, HttpResponseStatus.INTERNAL_SERVER_ERROR); + } + } else { + logger.log(Level.FINEST, "not writing user profile " + userProfile + " user id + " + + (userProfile != null ? userProfile.getUserId() : null)); + } + } +} diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/session/FileJsonSessionCodec.java b/net-http-server/src/main/java/org/xbib/net/http/server/session/FileJsonSessionCodec.java index 3f864b2..baea6a1 100644 --- a/net-http-server/src/main/java/org/xbib/net/http/server/session/FileJsonSessionCodec.java +++ b/net-http-server/src/main/java/org/xbib/net/http/server/session/FileJsonSessionCodec.java @@ -46,12 +46,6 @@ public class FileJsonSessionCodec implements Codec { this.sessionCacheSize = sessionCacheSize; this.sessionDuration = sessionDuration; this.lock = new ReentrantReadWriteLock(); - try { - Files.createDirectories(path); - } catch (IOException e) { - logger.log(Level.SEVERE, e.getMessage(), e); - throw new UncheckedIOException(e); - } } @Override @@ -80,6 +74,7 @@ public class FileJsonSessionCodec implements Codec { ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); try { writeLock.lock(); + createPath(); PercentEncoder percentEncoder = PercentEncoders.getUnreservedEncoder(StandardCharsets.UTF_8); try (Writer writer = Files.newBufferedWriter(path.resolve(percentEncoder.encode(key)))) { writer.write(JsonUtil.toString(session)); @@ -146,4 +141,15 @@ public class FileJsonSessionCodec implements Codec { logger.log(Level.WARNING, "i/o error while destroy: " + e.getMessage(), e); } } + + private void createPath() { + try { + if (!Files.exists(path)) { + Files.createDirectories(path); + } + } catch (IOException e) { + logger.log(Level.SEVERE, e.getMessage(), e); + throw new UncheckedIOException(e); + } + } } diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/session/IncomingContextHandler.java b/net-http-server/src/main/java/org/xbib/net/http/server/session/IncomingContextHandler.java index 7fee936..312513b 100644 --- a/net-http-server/src/main/java/org/xbib/net/http/server/session/IncomingContextHandler.java +++ b/net-http-server/src/main/java/org/xbib/net/http/server/session/IncomingContextHandler.java @@ -77,14 +77,14 @@ public class IncomingContextHandler implements HttpHandler { try { // extract payload from our cookie, must be not null, otherwise security problem with exception payload = toPayload(cookie); - logger.log(Level.FINE, payload != null && !payload.isEmpty() ? "payload found" : "no payload"); + logger.log(Level.FINE, payload != null && !payload.isEmpty() ? "payload found" : "no payload found"); // extract session from payload and recover session from persistence store session = toSession(payload); // do not log explicit content of payload, session, userprofile to not leak sensitive info into log - logger.log(Level.FINE, session != null && !session.isEmpty() ? "session found" : "no session"); + logger.log(Level.FINE, session != null && !session.isEmpty() ? "session found" : "no session found"); // extract userprofile from cookie info, use previous auth handler setup, recover attributes only userProfile = recoverUserProfile(context, session, payload); - logger.log(Level.FINE, userProfile != null ? "user profile found" : "no user profile"); + logger.log(Level.FINE, userProfile != null ? "user profile found" : "no user profile found"); } catch (CookieSignatureException e) { // set exception in context to discard broken cookie later and render exception message context.getAttributes().put("_throwable", e); @@ -105,17 +105,21 @@ public class IncomingContextHandler implements HttpHandler { logger.log(Level.FINE, "new session created, id = " + session.id()); } } + // important context.getAttributes().put("session", session); if (userProfile == null) { try { userProfile = recoverUserProfile(context, session, payload); if (userProfile != null) { - logger.log(Level.FINE, "new user profile recovered"); + logger.log(Level.FINE, "user profile recovered"); + } else { + logger.log(Level.FINE, "no user profile recovered"); } } catch (IOException e) { logger.log(Level.FINE, "unable to recover new user profile: " + e.getMessage(), e); } } + // important context.getAttributes().put("userprofile", userProfile); } @@ -176,39 +180,25 @@ public class IncomingContextHandler implements HttpHandler { Session session, Map payload) throws IOException { - if (payload == null && session != null) { - // session has user profile? this is important for spanning HTTP request/response like in HTTP POST/FORWARD/GET - Map map = (Map) session.get("userprofile"); - if (map != null) { - UserProfile userProfile = BaseUserProfile.fromMap(map); - if (userProfile.getAttributes().isEmpty()) { - // recover complete user profile from codec - userProfileCodec.read(userProfile.getUserId()); - } - return userProfile; - } + // already in context from previous handler? + UserProfile userProfile = context.getAttributes().get(UserProfile.class, "userprofile"); + if (userProfile != null) { + return userProfileCodec.read(userProfile.getUserId()); } - if (payload != null) { - // from cookie - Map m = (Map) payload.get("map"); - if (m != null && m.containsKey("user_id")) { - String key = (String) m.get("user_id"); - logger.log(Level.INFO, "recover: user id = " + key); - // set by previous handler? - UserProfile userProfile = context.getAttributes().get(UserProfile.class, "userprofile"); - if (userProfile == null) { - userProfile = new BaseUserProfile(); - } - userProfile.setUserId(key); - if (m.containsKey("e_user_id")) { - userProfile.setEffectiveUserId((String) m.get("e_user_id")); - } - if (userProfile.getAttributes().isEmpty()) { - // recover complete user profile from codec - userProfileCodec.read(key); - } - return userProfile; - } + // initialize + Map map = null; + // from session? + if (payload == null && session != null) { + // session has stored user profile? this is important for spanning HTTP request/response like in HTTP POST/FORWARD/GET + map = (Map) session.get("userprofile"); + } else if (payload != null) { + // otherwise from cookie? + map = (Map) payload.get("map"); + } + if (map != null && map.containsKey(UserProfile.USER_ID)) { + return userProfileCodec.read(BaseUserProfile.fromMap(map).getUserId()); + } else { + logger.log(Level.FINE, "unable to find info for initialize user profile"); } return null; } diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/session/OutgoingContextHandler.java b/net-http-server/src/main/java/org/xbib/net/http/server/session/OutgoingContextHandler.java index 413690f..90c700b 100644 --- a/net-http-server/src/main/java/org/xbib/net/http/server/session/OutgoingContextHandler.java +++ b/net-http-server/src/main/java/org/xbib/net/http/server/session/OutgoingContextHandler.java @@ -130,8 +130,8 @@ public class OutgoingContextHandler implements HttpHandler { } TinyMap.Builder builder = TinyMap.builder(); if (userProfile != null) { - builder.putIfNotNull("user_id", userProfile.getUserId()); - builder.putIfNotNull("e_user_id", userProfile.getEffectiveUserId()); + builder.putIfNotNull(UserProfile.USER_ID, userProfile.getUserId()); + builder.putIfNotNull(UserProfile.EFFECTIVE_USER_ID, userProfile.getEffectiveUserId()); } logger.log(Level.FINEST, "map for cookie payload = " + builder.build()); String payload = CookieSignatureUtil.toString(builder.build()); diff --git a/net-http-server/src/main/java/org/xbib/net/http/server/session/PersistSessionHandler.java b/net-http-server/src/main/java/org/xbib/net/http/server/session/PersistSessionHandler.java index 28b3a78..2b16a5e 100644 --- a/net-http-server/src/main/java/org/xbib/net/http/server/session/PersistSessionHandler.java +++ b/net-http-server/src/main/java/org/xbib/net/http/server/session/PersistSessionHandler.java @@ -15,33 +15,16 @@ public class PersistSessionHandler implements HttpHandler { private static final Logger logger = Logger.getLogger(PersistSessionHandler.class.getName()); - private final Codec userProfileCodec; - private final Codec sessionCodec; - public PersistSessionHandler(Codec userProfileCodec, - Codec sessionCodec) { - this.userProfileCodec = userProfileCodec; + public PersistSessionHandler(Codec sessionCodec) { this.sessionCodec = sessionCodec; } @Override public void handle(HttpRouterContext context) throws IOException { - UserProfile userProfile = context.getAttributes().get(UserProfile.class, "userprofile"); - if (userProfile != null && userProfile.getUserId() != null) { - try { - logger.log(Level.FINEST, "writing user profile id " + userProfile.getUserId()); - userProfileCodec.write(userProfile.getUserId(), userProfile); - } catch (Exception e) { - logger.log(Level.SEVERE, e.getMessage(), e); - throw new HttpException("unable to write user profile data", context, HttpResponseStatus.INTERNAL_SERVER_ERROR); - } - } else { - logger.log(Level.FINEST, "not writing user profile " + userProfile + " user id + " + - (userProfile != null ? userProfile.getUserId() : null)); - } Session session = context.getAttributes().get(Session.class, "session"); - if (session != null) { + if (session != null && session.hasPayload()) { try { logger.log(Level.FINEST, "writing session id " + session.id() + " keys = " + session.keySet()); sessionCodec.write(session.id(), session); diff --git a/settings.gradle b/settings.gradle index 0c9866d..39da600 100644 --- a/settings.gradle +++ b/settings.gradle @@ -6,7 +6,7 @@ dependencyResolutionManagement { version('netty', '4.1.110.Final') version('netty-tcnative', '2.0.65.Final') version('datastructures', '5.1.0') - version('net', '4.6.0') + version('net', '4.7.0') library('netty-codec-http2', 'io.netty', 'netty-codec-http2').versionRef('netty') library('netty-handler', 'io.netty', 'netty-handler').versionRef('netty') library('netty-handler-proxy', 'io.netty', 'netty-handler-proxy').versionRef('netty')