diff --git a/hugegraph-api/pom.xml b/hugegraph-api/pom.xml index 5457c08092..5c43a2b4f0 100644 --- a/hugegraph-api/pom.xml +++ b/hugegraph-api/pom.xml @@ -153,7 +153,7 @@ - 0.62.0.0 + 0.63.0.0 diff --git a/hugegraph-api/src/main/java/com/baidu/hugegraph/api/auth/LoginAPI.java b/hugegraph-api/src/main/java/com/baidu/hugegraph/api/auth/LoginAPI.java new file mode 100644 index 0000000000..51cd1f3976 --- /dev/null +++ b/hugegraph-api/src/main/java/com/baidu/hugegraph/api/auth/LoginAPI.java @@ -0,0 +1,160 @@ +/* + * Copyright 2017 HugeGraph Authors + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package com.baidu.hugegraph.api.auth; + +import javax.inject.Singleton; +import javax.security.sasl.AuthenticationException; +import javax.ws.rs.BadRequestException; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; +import javax.ws.rs.NotAuthorizedException; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; + +import com.baidu.hugegraph.HugeGraph; +import com.baidu.hugegraph.api.API; +import com.baidu.hugegraph.api.filter.AuthenticationFilter; +import com.baidu.hugegraph.api.filter.StatusFilter; +import com.baidu.hugegraph.api.filter.StatusFilter.Status; +import com.baidu.hugegraph.auth.AuthConstant; +import com.baidu.hugegraph.auth.UserWithRole; +import com.baidu.hugegraph.core.GraphManager; +import com.baidu.hugegraph.define.Checkable; +import com.baidu.hugegraph.server.RestServer; +import com.baidu.hugegraph.util.E; +import com.baidu.hugegraph.util.Log; +import com.codahale.metrics.annotation.Timed; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.collect.ImmutableMap; + +@Path("graphs/{graph}/auth") +@Singleton +public class LoginAPI extends API { + + private static final Logger LOG = Log.logger(RestServer.class); + + @POST + @Timed + @Path("login") + @Status(StatusFilter.Status.OK) + @Consumes(APPLICATION_JSON) + @Produces(APPLICATION_JSON_WITH_CHARSET) + public String login(@Context GraphManager manager, + @PathParam("graph") String graph, + JsonLogin jsonLogin) { + LOG.debug("Graph [{}] user login: {}", graph, jsonLogin); + checkCreatingBody(jsonLogin); + + try { + String token = manager.authManager() + .loginUser(jsonLogin.name, jsonLogin.password); + HugeGraph g = graph(manager, graph); + return manager.serializer(g) + .writeMap(ImmutableMap.of("token", token)); + } catch (AuthenticationException e) { + throw new NotAuthorizedException(e.getMessage(), e); + } + } + + @DELETE + @Timed + @Path("logout") + @Status(StatusFilter.Status.OK) + @Consumes(APPLICATION_JSON) + @Produces(APPLICATION_JSON_WITH_CHARSET) + public void logout(@Context GraphManager manager, + @PathParam("graph") String graph, + @HeaderParam(HttpHeaders.AUTHORIZATION) String auth) { + E.checkArgument(StringUtils.isNotEmpty(auth), + "Request header Authorization must not be null"); + LOG.debug("Graph [{}] user logout: {}", graph, auth); + + if (!auth.startsWith(AuthenticationFilter.BEARER_TOKEN_PREFIX)) { + throw new BadRequestException( + "Only HTTP Bearer authentication is supported"); + } + + String token = auth.substring(AuthenticationFilter.BEARER_TOKEN_PREFIX + .length()); + + manager.authManager().logoutUser(token); + } + + @GET + @Timed + @Path("verify") + @Status(StatusFilter.Status.OK) + @Consumes(APPLICATION_JSON) + @Produces(APPLICATION_JSON_WITH_CHARSET) + public String verifyToken(@Context GraphManager manager, + @PathParam("graph") String graph, + @HeaderParam(HttpHeaders.AUTHORIZATION) + String token) { + E.checkArgument(StringUtils.isNotEmpty(token), + "Request header Authorization must not be null"); + LOG.debug("Graph [{}] get user: {}", graph, token); + + if (!token.startsWith(AuthenticationFilter.BEARER_TOKEN_PREFIX)) { + throw new BadRequestException( + "Only HTTP Bearer authentication is supported"); + } + + token = token.substring(AuthenticationFilter.BEARER_TOKEN_PREFIX + .length()); + UserWithRole userWithRole = manager.authManager().validateUser(token); + + HugeGraph g = graph(manager, graph); + return manager.serializer(g) + .writeMap(ImmutableMap.of(AuthConstant.TOKEN_USER_NAME, + userWithRole.username(), + AuthConstant.TOKEN_USER_ID, + userWithRole.userId())); + } + + private static class JsonLogin implements Checkable { + + @JsonProperty("user_name") + private String name; + @JsonProperty("user_password") + private String password; + + @Override + public void checkCreate(boolean isBatch) { + E.checkArgument(!StringUtils.isEmpty(this.name), + "The name of user can't be null"); + E.checkArgument(!StringUtils.isEmpty(this.password), + "The password of user can't be null"); + } + + @Override + public void checkUpdate() { + // pass + } + } +} diff --git a/hugegraph-api/src/main/java/com/baidu/hugegraph/api/auth/UserAPI.java b/hugegraph-api/src/main/java/com/baidu/hugegraph/api/auth/UserAPI.java index 6b947b6ead..0bb744a976 100644 --- a/hugegraph-api/src/main/java/com/baidu/hugegraph/api/auth/UserAPI.java +++ b/hugegraph-api/src/main/java/com/baidu/hugegraph/api/auth/UserAPI.java @@ -179,6 +179,8 @@ private static class JsonUser implements Checkable { private String email; @JsonProperty("user_avatar") private String avatar; + @JsonProperty("user_description") + private String description; public HugeUser build(HugeUser user) { E.checkArgument(this.name == null || user.name().equals(this.name), @@ -195,6 +197,9 @@ public HugeUser build(HugeUser user) { if (this.avatar != null) { user.avatar(this.avatar); } + if (this.description != null) { + user.description(this.description); + } return user; } @@ -204,6 +209,7 @@ public HugeUser build() { user.phone(this.phone); user.email(this.email); user.avatar(this.avatar); + user.description(this.description); return user; } diff --git a/hugegraph-api/src/main/java/com/baidu/hugegraph/api/filter/AuthenticationFilter.java b/hugegraph-api/src/main/java/com/baidu/hugegraph/api/filter/AuthenticationFilter.java index d63b5ca2b5..bbc6745b85 100644 --- a/hugegraph-api/src/main/java/com/baidu/hugegraph/api/filter/AuthenticationFilter.java +++ b/hugegraph-api/src/main/java/com/baidu/hugegraph/api/filter/AuthenticationFilter.java @@ -21,7 +21,9 @@ import java.io.IOException; import java.security.Principal; +import java.util.HashMap; import java.util.List; +import java.util.Map; import javax.annotation.Priority; import javax.ws.rs.BadRequestException; @@ -51,15 +53,23 @@ import com.baidu.hugegraph.core.GraphManager; import com.baidu.hugegraph.util.E; import com.baidu.hugegraph.util.Log; -import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableList; @Provider @PreMatching @Priority(Priorities.AUTHENTICATION) public class AuthenticationFilter implements ContainerRequestFilter { + public static final String BASIC_AUTH_PREFIX = "Basic "; + public static final String BEARER_TOKEN_PREFIX = "Bearer "; + private static final Logger LOG = Log.logger(AuthenticationFilter.class); + private static final List WHITE_API_LIST = ImmutableList.of( + "auth/login", + "versions" + ); + @Context private javax.inject.Provider managerProvider; @@ -68,6 +78,9 @@ public class AuthenticationFilter implements ContainerRequestFilter { @Override public void filter(ContainerRequestContext context) throws IOException { + if (AuthenticationFilter.isWhiteAPI(context)) { + return; + } User user = this.authenticate(context); Authorizer authorizer = new Authorizer(user, context.getUriInfo()); context.setSecurityContext(authorizer); @@ -91,6 +104,7 @@ protected User authenticate(ContainerRequestContext context) { path = request.getRequestURI(); } + Map credentials = new HashMap<>(); // Extract authentication credentials String auth = context.getHeaderString(HttpHeaders.AUTHORIZATION); if (auth == null) { @@ -98,39 +112,45 @@ protected User authenticate(ContainerRequestContext context) { "Authentication credentials are required", "Missing authentication credentials"); } - if (!auth.startsWith("Basic ")) { - throw new BadRequestException( - "Only HTTP Basic authentication is supported"); - } - auth = auth.substring("Basic ".length()); - auth = new String(DatatypeConverter.parseBase64Binary(auth), - Charsets.ASCII_CHARSET); - String[] values = auth.split(":"); - if (values.length != 2) { - throw new BadRequestException( - "Invalid syntax for username and password"); - } + if (auth.startsWith(BASIC_AUTH_PREFIX)) { + auth = auth.substring(BASIC_AUTH_PREFIX.length()); + auth = new String(DatatypeConverter.parseBase64Binary(auth), + Charsets.ASCII_CHARSET); + String[] values = auth.split(":"); + if (values.length != 2) { + throw new BadRequestException( + "Invalid syntax for username and password"); + } + + final String username = values[0]; + final String password = values[1]; - final String username = values[0]; - final String password = values[1]; + if (StringUtils.isEmpty(username) || + StringUtils.isEmpty(password)) { + throw new BadRequestException( + "Invalid syntax for username and password"); + } - if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) { + credentials.put(HugeAuthenticator.KEY_USERNAME, username); + credentials.put(HugeAuthenticator.KEY_PASSWORD, password); + } else if (auth.startsWith(BEARER_TOKEN_PREFIX)) { + String token = auth.substring(BEARER_TOKEN_PREFIX.length()); + credentials.put(HugeAuthenticator.KEY_TOKEN, token); + } else { throw new BadRequestException( - "Invalid syntax for username and password"); + "Only HTTP Basic or Bearer authentication is supported"); } + credentials.put(HugeAuthenticator.KEY_ADDRESS, peer); + credentials.put(HugeAuthenticator.KEY_PATH, path); + // Validate the extracted credentials try { - return manager.authenticate(ImmutableMap.of( - HugeAuthenticator.KEY_USERNAME, username, - HugeAuthenticator.KEY_PASSWORD, password, - HugeAuthenticator.KEY_ADDRESS, peer, - HugeAuthenticator.KEY_PATH, path)); + return manager.authenticate(credentials); } catch (AuthenticationException e) { - String msg = String.format("Authentication failed for user '%s'", - username); - throw new NotAuthorizedException(msg, e.getMessage()); + throw new NotAuthorizedException("Authentication failed", + e.getMessage()); } } @@ -257,4 +277,18 @@ public boolean equals(Object obj) { } } } + + public static boolean isWhiteAPI(ContainerRequestContext context) { + String path = context.getUriInfo().getPath(); + + E.checkArgument(StringUtils.isNotEmpty(path), + "Invalid request uri '%s'", path); + + for (String whiteApi : WHITE_API_LIST) { + if (path.endsWith(whiteApi)) { + return true; + } + } + return false; + } } diff --git a/hugegraph-api/src/main/java/com/baidu/hugegraph/auth/ConfigAuthenticator.java b/hugegraph-api/src/main/java/com/baidu/hugegraph/auth/ConfigAuthenticator.java index ede416871e..5e230c033d 100644 --- a/hugegraph-api/src/main/java/com/baidu/hugegraph/auth/ConfigAuthenticator.java +++ b/hugegraph-api/src/main/java/com/baidu/hugegraph/auth/ConfigAuthenticator.java @@ -27,6 +27,7 @@ import org.apache.commons.lang.NotImplementedException; import org.apache.tinkerpop.gremlin.groovy.jsr223.dsl.credential.CredentialGraphTokens; +import com.baidu.hugegraph.backend.id.IdGenerator; import com.baidu.hugegraph.config.HugeConfig; import com.baidu.hugegraph.config.ServerOptions; import com.baidu.hugegraph.util.E; @@ -58,12 +59,14 @@ public void setup(HugeConfig config) { * @return String No permission if return ROLE_NONE else return a role */ @Override - public RolePermission authenticate(final String username, - final String password) { + public UserWithRole authenticate(final String username, + final String password, + final String token) { E.checkArgumentNotNull(username, "The username parameter can't be null"); E.checkArgumentNotNull(password, "The password parameter can't be null"); + E.checkArgument(token == null, "The token must be null"); RolePermission role; if (password.equals(this.tokens.get(username))) { @@ -77,7 +80,7 @@ public RolePermission authenticate(final String username, role = ROLE_NONE; } - return role; + return new UserWithRole(IdGenerator.of(username), username, role); } @Override diff --git a/hugegraph-api/src/main/java/com/baidu/hugegraph/auth/HugeAuthenticator.java b/hugegraph-api/src/main/java/com/baidu/hugegraph/auth/HugeAuthenticator.java index f0c07b17d8..31a051820a 100644 --- a/hugegraph-api/src/main/java/com/baidu/hugegraph/auth/HugeAuthenticator.java +++ b/hugegraph-api/src/main/java/com/baidu/hugegraph/auth/HugeAuthenticator.java @@ -45,6 +45,7 @@ public interface HugeAuthenticator extends Authenticator { CredentialGraphTokens.PROPERTY_USERNAME; public static final String KEY_PASSWORD = CredentialGraphTokens.PROPERTY_PASSWORD; + public static final String KEY_TOKEN = "token"; public static final String KEY_ROLE = "role"; public static final String KEY_ADDRESS = "address"; public static final String KEY_PATH = "path"; @@ -63,7 +64,8 @@ public interface HugeAuthenticator extends Authenticator { public void setup(HugeConfig config); - public RolePermission authenticate(String username, String password); + public UserWithRole authenticate(String username, String password, + String token); public AuthManager authManager(); @Override @@ -86,15 +88,16 @@ public default User authenticate(final Map credentials) if (this.requireAuthentication()) { String username = credentials.get(KEY_USERNAME); String password = credentials.get(KEY_PASSWORD); + String token = credentials.get(KEY_TOKEN); // Currently we just use config tokens to authenticate - RolePermission role = this.authenticate(username, password); - if (!verifyRole(role)) { + UserWithRole role = this.authenticate(username, password, token); + if (!verifyRole(role.role())) { // Throw if not certified String message = "Incorrect username or password"; throw new AuthenticationException(message); } - user = new User(username, role); + user = new User(role.username(), role.role()); user.client(credentials.get(KEY_ADDRESS)); } diff --git a/hugegraph-api/src/main/java/com/baidu/hugegraph/auth/HugeGraphAuthProxy.java b/hugegraph-api/src/main/java/com/baidu/hugegraph/auth/HugeGraphAuthProxy.java index 807d3c4f71..ae7e7e36e2 100644 --- a/hugegraph-api/src/main/java/com/baidu/hugegraph/auth/HugeGraphAuthProxy.java +++ b/hugegraph-api/src/main/java/com/baidu/hugegraph/auth/HugeGraphAuthProxy.java @@ -34,7 +34,9 @@ import java.util.concurrent.TimeoutException; import java.util.function.Supplier; +import javax.security.sasl.AuthenticationException; import javax.ws.rs.ForbiddenException; +import javax.ws.rs.NotAuthorizedException; import org.apache.tinkerpop.gremlin.groovy.jsr223.GroovyTranslator; import org.apache.tinkerpop.gremlin.process.computer.GraphComputer; @@ -1365,19 +1367,48 @@ public RolePermission rolePermission(AuthElement element) { } @Override - public RolePermission loginUser(String username, String password) { - // Can't verifyPermission() here, login first with temp permission + public UserWithRole validateUser(String username, String password) { + // Can't verifyPermission() here, validate first with tmp permission Context context = setContext(Context.admin()); try { - return this.authManager.loginUser(username, password); + return this.authManager.validateUser(username, password); + } catch (Exception e) { + LOG.error("Failed to validate user {} with error: ", + username, e); + throw e; + } finally { + setContext(context); + } + } + + @Override + public UserWithRole validateUser(String token) { + // Can't verifyPermission() here, validate first with tmp permission + Context context = setContext(Context.admin()); + try { + return this.authManager.validateUser(token); } catch (Exception e) { - LOG.error("Failed to login user {} with error: ", username, e); + LOG.error("Failed to validate token {} with error: ", token, e); throw e; } finally { setContext(context); } } + @Override + public String loginUser(String username, String password) { + try { + return this.authManager.loginUser(username, password); + } catch (AuthenticationException e) { + throw new NotAuthorizedException(e.getMessage(), e); + } + } + + @Override + public void logoutUser(String token) { + this.authManager.logoutUser(token); + } + private void switchAuthManager(AuthManager authManager) { this.authManager = authManager; HugeGraphAuthProxy.this.hugegraph.switchAuthManager(authManager); diff --git a/hugegraph-api/src/main/java/com/baidu/hugegraph/auth/StandardAuthenticator.java b/hugegraph-api/src/main/java/com/baidu/hugegraph/auth/StandardAuthenticator.java index 708a5094e0..76ce0b4ca8 100644 --- a/hugegraph-api/src/main/java/com/baidu/hugegraph/auth/StandardAuthenticator.java +++ b/hugegraph-api/src/main/java/com/baidu/hugegraph/auth/StandardAuthenticator.java @@ -114,25 +114,36 @@ public void setup(HugeConfig config) { /** * Verify if a user is legal - * @param username the username for authentication - * @param password the password for authentication + * @param username the username for authentication + * @param password the password for authentication + * @param token the token for authentication * @return String No permission if return ROLE_NONE else return a role */ @Override - public RolePermission authenticate(String username, String password) { - E.checkArgumentNotNull(username, - "The username parameter can't be null"); - E.checkArgumentNotNull(password, - "The password parameter can't be null"); - - RolePermission role = this.graph().authManager().loginUser(username, - password); + public UserWithRole authenticate(String username, String password, + String token) { + UserWithRole userWithRole; + if (StringUtils.isNotEmpty(token)) { + userWithRole = this.authManager().validateUser(token); + } else { + E.checkArgumentNotNull(username, + "The username parameter can't be null"); + E.checkArgumentNotNull(password, + "The password parameter can't be null"); + userWithRole = this.authManager().validateUser(username, password); + } + + RolePermission role = userWithRole.role(); if (role == null) { role = ROLE_NONE; - } else if (username.equals(USER_ADMIN)) { + } else if (USER_ADMIN.equals(userWithRole.username())) { role = ROLE_ADMIN; + } else { + return userWithRole; } - return role; + + return new UserWithRole(userWithRole.userId(), + userWithRole.username(), role); } @Override diff --git a/hugegraph-api/src/main/java/com/baidu/hugegraph/version/ApiVersion.java b/hugegraph-api/src/main/java/com/baidu/hugegraph/version/ApiVersion.java index f9f69f325a..15f0a9b604 100644 --- a/hugegraph-api/src/main/java/com/baidu/hugegraph/version/ApiVersion.java +++ b/hugegraph-api/src/main/java/com/baidu/hugegraph/version/ApiVersion.java @@ -112,10 +112,11 @@ public final class ApiVersion { * [0.60] Issue-1392: Support create and resume snapshot * [0.61] Issue-1433: Unify naming of degree for oltp algorithms * [0.62] Issue-1378: Add compact api for rocksdb/cassandra/hbase backend + * [0.63] Issue-1500: Add user-login RESTful API */ // The second parameter of Version.of() is for IDE running without JAR - public static final Version VERSION = Version.of(ApiVersion.class, "0.62"); + public static final Version VERSION = Version.of(ApiVersion.class, "0.63"); public static final void check() { // Check version of hugegraph-core. Firstly do check from version 0.3 diff --git a/hugegraph-core/pom.xml b/hugegraph-core/pom.xml index efd45b978e..fdd2e7652d 100644 --- a/hugegraph-core/pom.xml +++ b/hugegraph-core/pom.xml @@ -174,6 +174,24 @@ fastutil 8.1.0 + + + io.jsonwebtoken + jjwt-api + 0.11.2 + + + io.jsonwebtoken + jjwt-impl + 0.11.2 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.11.2 + runtime + diff --git a/hugegraph-core/src/main/java/com/baidu/hugegraph/auth/AuthConstant.java b/hugegraph-core/src/main/java/com/baidu/hugegraph/auth/AuthConstant.java new file mode 100644 index 0000000000..59379481c9 --- /dev/null +++ b/hugegraph-core/src/main/java/com/baidu/hugegraph/auth/AuthConstant.java @@ -0,0 +1,29 @@ +/* + * Copyright 2017 HugeGraph Authors + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package com.baidu.hugegraph.auth; + +public interface AuthConstant { + + /* + * Fields in token + */ + String TOKEN_USER_NAME = "user_name"; + String TOKEN_USER_ID = "user_id"; +} diff --git a/hugegraph-core/src/main/java/com/baidu/hugegraph/auth/AuthManager.java b/hugegraph-core/src/main/java/com/baidu/hugegraph/auth/AuthManager.java index 57561466ee..2c0a14426a 100644 --- a/hugegraph-core/src/main/java/com/baidu/hugegraph/auth/AuthManager.java +++ b/hugegraph-core/src/main/java/com/baidu/hugegraph/auth/AuthManager.java @@ -21,7 +21,8 @@ import java.util.List; -import com.baidu.hugegraph.auth.RolePermission; +import javax.security.sasl.AuthenticationException; + import com.baidu.hugegraph.auth.SchemaDefine.AuthElement; import com.baidu.hugegraph.backend.id.Id; @@ -72,5 +73,10 @@ public interface AuthManager { public HugeUser matchUser(String name, String password); public RolePermission rolePermission(AuthElement element); - public RolePermission loginUser(String username, String password); + public String loginUser(String username, String password) + throws AuthenticationException; + public void logoutUser(String token); + + public UserWithRole validateUser(String username, String password); + public UserWithRole validateUser(String token); } diff --git a/hugegraph-core/src/main/java/com/baidu/hugegraph/auth/HugeUser.java b/hugegraph-core/src/main/java/com/baidu/hugegraph/auth/HugeUser.java index f327c596ad..a5777bd707 100644 --- a/hugegraph-core/src/main/java/com/baidu/hugegraph/auth/HugeUser.java +++ b/hugegraph-core/src/main/java/com/baidu/hugegraph/auth/HugeUser.java @@ -43,6 +43,7 @@ public class HugeUser extends Entity { private String phone; private String email; private String avatar; + private String description; // This field is just for cache private RolePermission role; @@ -107,6 +108,14 @@ public void avatar(String avatar) { this.avatar = avatar; } + public String description() { + return this.description; + } + + public void description(String description) { + this.description = description; + } + public RolePermission role() { return this.role; } diff --git a/hugegraph-core/src/main/java/com/baidu/hugegraph/auth/StandardAuthManager.java b/hugegraph-core/src/main/java/com/baidu/hugegraph/auth/StandardAuthManager.java index e5f013b9c8..795a3b440a 100644 --- a/hugegraph-core/src/main/java/com/baidu/hugegraph/auth/StandardAuthManager.java +++ b/hugegraph-core/src/main/java/com/baidu/hugegraph/auth/StandardAuthManager.java @@ -22,8 +22,11 @@ import java.time.Duration; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Set; +import javax.security.sasl.AuthenticationException; + import com.baidu.hugegraph.HugeGraphParams; import com.baidu.hugegraph.auth.HugeUser.P; import com.baidu.hugegraph.auth.SchemaDefine.AuthElement; @@ -36,8 +39,11 @@ import com.baidu.hugegraph.util.E; import com.baidu.hugegraph.util.Events; import com.baidu.hugegraph.util.StringEncoding; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import io.jsonwebtoken.Claims; + public class StandardAuthManager implements AuthManager { private static final long CACHE_EXPIRE = Duration.ofDays(1L).toMillis(); @@ -46,6 +52,7 @@ public class StandardAuthManager implements AuthManager { private final EventListener eventListener; private final Cache usersCache; private final Cache pwdCache; + private final Cache tokenCache; private final EntityManager users; private final EntityManager groups; @@ -54,6 +61,8 @@ public class StandardAuthManager implements AuthManager { private final RelationshipManager belong; private final RelationshipManager access; + private final TokenGenerator tokenGenerator; + public StandardAuthManager(HugeGraphParams graph) { E.checkNotNull(graph, "graph"); @@ -61,6 +70,7 @@ public StandardAuthManager(HugeGraphParams graph) { this.eventListener = this.listenChanges(); this.usersCache = this.cache("users"); this.pwdCache = this.cache("users_pwd"); + this.tokenCache = this.cache("token"); this.users = new EntityManager<>(this.graph, HugeUser.P.USER, HugeUser::fromVertex); @@ -73,6 +83,8 @@ public StandardAuthManager(HugeGraphParams graph) { HugeBelong::fromEdge); this.access = new RelationshipManager<>(this.graph, HugeAccess.P.ACCESS, HugeAccess::fromEdge); + + this.tokenGenerator = new TokenGenerator(graph.configuration()); } private Cache cache(String prefix) { @@ -343,6 +355,7 @@ public List listAccessByTarget(Id target, long limit) { public HugeUser matchUser(String name, String password) { E.checkArgumentNotNull(name, "User name can't be null"); E.checkArgumentNotNull(password, "User password can't be null"); + HugeUser user = this.findUser(name); if (user == null) { return null; @@ -368,7 +381,7 @@ public RolePermission rolePermission(AuthElement element) { return this.rolePermission((HugeTarget) element); } - List accesses = new ArrayList<>();; + List accesses = new ArrayList<>(); if (element instanceof HugeBelong) { HugeBelong belong = (HugeBelong) element; accesses.addAll(this.listAccessByGroup(belong.target(), -1)); @@ -425,12 +438,62 @@ private RolePermission rolePermission(HugeTarget target) { } @Override - public RolePermission loginUser(String username, String password) { + public String loginUser(String username, String password) + throws AuthenticationException { HugeUser user = this.matchUser(username, password); if (user == null) { - return null; + String msg = "Incorrect username or password"; + throw new AuthenticationException(msg); } - return this.rolePermission(user); + + Map payload = ImmutableMap.of(AuthConstant.TOKEN_USER_NAME, + username, + AuthConstant.TOKEN_USER_ID, + user.id.asString()); + String token = this.tokenGenerator.create(payload, CACHE_EXPIRE); + + this.tokenCache.update(IdGenerator.of(token), username); + return token; + } + + @Override + public void logoutUser(String token) { + this.tokenCache.invalidate(IdGenerator.of(token)); + } + + @Override + public UserWithRole validateUser(String username, String password) { + HugeUser user = this.matchUser(username, password); + if (user == null) { + return new UserWithRole(username); + } + return new UserWithRole(user.id, username, this.rolePermission(user)); + } + + @Override + public UserWithRole validateUser(String token) { + String username = (String) this.tokenCache.get(IdGenerator.of(token)); + + Claims payload = null; + boolean needBuildCache = false; + if (username == null) { + payload = this.tokenGenerator.verify(token); + username = (String) payload.get(AuthConstant.TOKEN_USER_NAME); + needBuildCache = true; + } + + HugeUser user = this.findUser(username); + if (user == null) { + return new UserWithRole(username); + } else if (needBuildCache) { + long expireAt = payload.getExpiration().getTime(); + long bornTime = CACHE_EXPIRE - + (expireAt - System.currentTimeMillis()); + this.tokenCache.update(IdGenerator.of(token), username, + Math.negateExact(bornTime)); + } + + return new UserWithRole(user.id(), username, this.rolePermission(user)); } /** diff --git a/hugegraph-core/src/main/java/com/baidu/hugegraph/auth/TokenGenerator.java b/hugegraph-core/src/main/java/com/baidu/hugegraph/auth/TokenGenerator.java new file mode 100644 index 0000000000..50937ec923 --- /dev/null +++ b/hugegraph-core/src/main/java/com/baidu/hugegraph/auth/TokenGenerator.java @@ -0,0 +1,70 @@ +/* + * Copyright 2017 HugeGraph Authors + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package com.baidu.hugegraph.auth; + +import java.nio.charset.StandardCharsets; +import java.util.Date; +import java.util.Map; + +import javax.crypto.SecretKey; +import javax.ws.rs.NotAuthorizedException; + +import com.baidu.hugegraph.config.AuthOptions; +import com.baidu.hugegraph.config.HugeConfig; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; + +public class TokenGenerator { + + private final SecretKey key; + + public TokenGenerator(HugeConfig config) { + String secretKey = config.get(AuthOptions.AUTH_TOKEN_SECRET); + this.key = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8)); + } + + public String create(Map payload, long expire) { + return Jwts.builder() + .setClaims(payload) + .setExpiration(new Date(System.currentTimeMillis() + expire)) + .signWith(this.key, SignatureAlgorithm.HS256) + .compact(); + } + + public Claims verify(String token) { + try { + Jws claimsJws = Jwts.parserBuilder() + .setSigningKey(key) + .build() + .parseClaimsJws(token); + return claimsJws.getBody(); + } catch (ExpiredJwtException e) { + throw new NotAuthorizedException("The token is expired", e); + } catch (JwtException e) { + throw new NotAuthorizedException("Invalid token", e); + } + } +} diff --git a/hugegraph-core/src/main/java/com/baidu/hugegraph/auth/UserWithRole.java b/hugegraph-core/src/main/java/com/baidu/hugegraph/auth/UserWithRole.java new file mode 100644 index 0000000000..ee1a84c18c --- /dev/null +++ b/hugegraph-core/src/main/java/com/baidu/hugegraph/auth/UserWithRole.java @@ -0,0 +1,51 @@ +/* + * Copyright 2017 HugeGraph Authors + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package com.baidu.hugegraph.auth; + +import com.baidu.hugegraph.backend.id.Id; + +public class UserWithRole { + + private final Id userId; + private final String username; + private final RolePermission role; + + public UserWithRole(String username) { + this(null, username, null); + } + + public UserWithRole(Id userId, String username, RolePermission role) { + this.userId = userId; + this.username = username; + this.role = role; + } + + public Id userId() { + return this.userId; + } + + public String username() { + return this.username; + } + + public RolePermission role() { + return this.role; + } +} diff --git a/hugegraph-core/src/main/java/com/baidu/hugegraph/config/AuthOptions.java b/hugegraph-core/src/main/java/com/baidu/hugegraph/config/AuthOptions.java new file mode 100644 index 0000000000..874209d855 --- /dev/null +++ b/hugegraph-core/src/main/java/com/baidu/hugegraph/config/AuthOptions.java @@ -0,0 +1,95 @@ +/* + * Copyright 2017 HugeGraph Authors + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package com.baidu.hugegraph.config; + +import static com.baidu.hugegraph.config.OptionChecker.disallowEmpty; + +public class AuthOptions extends OptionHolder { + + private AuthOptions() { + super(); + } + + private static volatile AuthOptions instance; + + public static synchronized AuthOptions instance() { + if (instance == null) { + instance = new AuthOptions(); + instance.registerOptions(); + } + return instance; + } + + public static final ConfigOption AUTHENTICATOR = + new ConfigOption<>( + "auth.authenticator", + "The class path of authenticator implemention. " + + "e.g., com.baidu.hugegraph.auth.StandardAuthenticator, " + + "or com.baidu.hugegraph.auth.ConfigAuthenticator.", + null, + "" + ); + + public static final ConfigOption AUTH_GRAPH_STORE = + new ConfigOption<>( + "auth.graph_store", + "The name of graph used to store authentication information, " + + "like users, only for com.baidu.hugegraph.auth.StandardAuthenticator.", + disallowEmpty(), + "hugegraph" + ); + + public static final ConfigOption AUTH_ADMIN_TOKEN = + new ConfigOption<>( + "auth.admin_token", + "Token for administrator operations, " + + "only for com.baidu.hugegraph.auth.ConfigAuthenticator.", + disallowEmpty(), + "162f7848-0b6d-4faf-b557-3a0797869c55" + ); + + public static final ConfigListOption AUTH_USER_TOKENS = + new ConfigListOption<>( + "auth.user_tokens", + "The map of user tokens with name and password, " + + "only for com.baidu.hugegraph.auth.ConfigAuthenticator.", + disallowEmpty(), + "hugegraph:9fd95c9c-711b-415b-b85f-d4df46ba5c31" + ); + + public static final ConfigOption AUTH_REMOTE_URL = + new ConfigOption<>( + "auth.remote_url", + "If the address is empty, it provide auth service, " + + "otherwise it is auth client and also provide auth service " + + "through rpc forwarding. The remote url can be set to " + + "multiple addresses, which are concat by ','.", + null, + "" + ); + + public static final ConfigOption AUTH_TOKEN_SECRET = + new ConfigOption<>( + "auth.token_secret", + "Secret key of HS256 algorithm.", + disallowEmpty(), + "FXQXbJtbCLxODc6tGci732pkH1cyf8Qg" + ); +} diff --git a/hugegraph-dist/src/main/java/com/baidu/hugegraph/dist/RegisterUtil.java b/hugegraph-dist/src/main/java/com/baidu/hugegraph/dist/RegisterUtil.java index b1e6c8df98..8ce07f7b21 100644 --- a/hugegraph-dist/src/main/java/com/baidu/hugegraph/dist/RegisterUtil.java +++ b/hugegraph-dist/src/main/java/com/baidu/hugegraph/dist/RegisterUtil.java @@ -187,6 +187,8 @@ public static void registerServer() { OptionSpace.register("server", "com.baidu.hugegraph.config.ServerOptions"); // Register RpcOptions (rpc-server) OptionSpace.register("rpc", "com.baidu.hugegraph.config.RpcOptions"); + // Register AuthOptions (auth-server) + OptionSpace.register("auth", "com.baidu.hugegraph.config.AuthOptions"); } /** diff --git a/hugegraph-test/src/main/java/com/baidu/hugegraph/api/ApiTestSuite.java b/hugegraph-test/src/main/java/com/baidu/hugegraph/api/ApiTestSuite.java index 05b69c865b..3200d742d3 100644 --- a/hugegraph-test/src/main/java/com/baidu/hugegraph/api/ApiTestSuite.java +++ b/hugegraph-test/src/main/java/com/baidu/hugegraph/api/ApiTestSuite.java @@ -36,7 +36,8 @@ TaskApiTest.class, GremlinApiTest.class, MetricsApiTest.class, - UserApiTest.class + UserApiTest.class, + LoginApiTest.class }) public class ApiTestSuite { diff --git a/hugegraph-test/src/main/java/com/baidu/hugegraph/api/BaseApiTest.java b/hugegraph-test/src/main/java/com/baidu/hugegraph/api/BaseApiTest.java index 8637fffbfd..9f17e4c201 100644 --- a/hugegraph-test/src/main/java/com/baidu/hugegraph/api/BaseApiTest.java +++ b/hugegraph-test/src/main/java/com/baidu/hugegraph/api/BaseApiTest.java @@ -32,6 +32,7 @@ import javax.ws.rs.client.ClientBuilder; import javax.ws.rs.client.Entity; import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature; @@ -126,6 +127,11 @@ public Response get(String path, String id) { return this.target.path(path).path(id).request().get(); } + public Response get(String path, + MultivaluedMap headers) { + return this.target.path(path).request().headers(headers).get(); + } + public Response get(String path, Map params) { WebTarget target = this.target.path(path); for (Map.Entry i : params.entrySet()) { @@ -162,6 +168,12 @@ public Response delete(String path, Map params) { } return target.request().delete(); } + + public Response delete(String path, + MultivaluedMap headers) { + WebTarget target = this.target.path(path); + return target.request().headers(headers).delete(); + } } /** diff --git a/hugegraph-test/src/main/java/com/baidu/hugegraph/api/LoginApiTest.java b/hugegraph-test/src/main/java/com/baidu/hugegraph/api/LoginApiTest.java new file mode 100644 index 0000000000..dde5eac175 --- /dev/null +++ b/hugegraph-test/src/main/java/com/baidu/hugegraph/api/LoginApiTest.java @@ -0,0 +1,164 @@ +/* + * Copyright 2017 HugeGraph Authors + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with this + * work for additional information regarding copyright ownership. The ASF + * licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package com.baidu.hugegraph.api; + +import java.nio.file.Paths; +import java.util.Map; + +import javax.ws.rs.core.GenericType; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MultivaluedHashMap; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; + +import org.apache.tinkerpop.shaded.jackson.core.type.TypeReference; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import com.baidu.hugegraph.testutil.Assert; +import com.baidu.hugegraph.util.JsonUtil; + +public class LoginApiTest extends BaseApiTest { + + private static final String PATH = "graphs/hugegraph/auth"; + private static final String USER_PATH = "graphs/hugegraph/auth/users"; + private String userId4Test; + + @Before + public void setup() { + Response r = this.createUser("test", "test"); + Map user = r.readEntity( + new GenericType>(){}); + this.userId4Test = (String) user.get("id"); + } + + @After + public void teardown() { + Response r = this.deleteUser(userId4Test); + } + + @Test + public void testLogin() { + Response r; + + r = this.login("test", "test"); + String result = assertResponseStatus(200, r); + assertJsonContains(result, "token"); + + r = this.login("test", "pass"); + assertResponseStatus(401, r); + + r = this.login("pass", "pass"); + assertResponseStatus(401, r); + } + + @Test + public void testLogout() { + Response r; + String result; + + r = this.login("test", "test"); + result = assertResponseStatus(200, r); + assertJsonContains(result, "token"); + + String token = this.tokenFromResponse(result); + + String path = Paths.get(PATH, "logout").toString(); + MultivaluedMap headers = new MultivaluedHashMap<>(); + headers.add(HttpHeaders.AUTHORIZATION, "Bearer " + token); + r = client().delete(path, headers); + assertResponseStatus(204, r); + + String invalidToken = "eyJhbGciOiJIUzI1NiJ9.eyJ1caVyX25hbWUiOiJ0ZXN0IiwidXNlcl9pZCI6Ii02Mzp0ZXN0IiwiZXhwIjoxNjI0MzUzMjUyfQ.kYot-3mSGlfSbEMzxrTs84q8YanhTTxtsKPPG25CNxA"; + headers = new MultivaluedHashMap<>(); + headers.add(HttpHeaders.AUTHORIZATION, "Bearer " + invalidToken); + r = client().delete(path, headers); + assertResponseStatus(401, r); + } + + @Test + public void testVerify() { + Response r; + String result; + + r = this.login("test", "test"); + result = assertResponseStatus(200, r); + assertJsonContains(result, "token"); + + String token = this.tokenFromResponse(result); + + String path = Paths.get(PATH, "verify").toString(); + MultivaluedMap headers = new MultivaluedHashMap<>(); + headers.add(HttpHeaders.AUTHORIZATION, "Bearer " + token); + r = client().get(path, headers); + + result = assertResponseStatus(200, r); + assertJsonContains(result, "user_id"); + assertJsonContains(result, "user_name"); + + Map user = JsonUtil.fromJson( + result, + new TypeReference>(){}); + Assert.assertEquals(this.userId4Test, user.get("user_id")); + Assert.assertEquals("test", user.get("user_name")); + + String invalidToken = "eyJhbGciOiJIUzI1NiJ9.eyJ1caVyX25hbWUiOiJ0ZXN0IiwidXNlcl9pZCI6Ii02Mzp0ZXN0IiwiZXhwIjoxNjI0MzUzMjUyfQ.kYot-3mSGlfSbEMzxrTs84q8YanhTTxtsKPPG25CNxA"; + headers = new MultivaluedHashMap<>(); + headers.add(HttpHeaders.AUTHORIZATION, "Bearer " + invalidToken); + r = client().get(path, headers); + assertResponseStatus(401, r); + + invalidToken = "123.ansfaf"; + headers = new MultivaluedHashMap<>(); + headers.add(HttpHeaders.AUTHORIZATION, "Bearer " + invalidToken); + r = client().get(path, headers); + assertResponseStatus(401, r); + } + + private Response createUser(String name, String password) { + String user = "{\"user_name\":\"%s\",\"user_password\":\"%s" + + "\",\"user_email\":\"user1@baidu.com\"," + + "\"user_phone\":\"123456789\",\"user_avatar\":\"image1" + + ".jpg\"}"; + return this.client().post(USER_PATH, + String.format(user, name, password)); + } + + private Response deleteUser(String id) { + return this.client().delete(USER_PATH, id); + } + + private Response login(String name, String password) { + String login = Paths.get(PATH, "login").toString(); + String loginUser = "{\"user_name\":\"%s\"," + + "\"user_password\":\"%s\"}"; + + return client().post(login, String.format(loginUser, + name, password)); + } + + private String tokenFromResponse(String content) { + Map data = JsonUtil.fromJson( + content, + new TypeReference>(){}); + return (String) data.get("token"); + } +} diff --git a/hugegraph-test/src/main/java/com/baidu/hugegraph/core/AuthTest.java b/hugegraph-test/src/main/java/com/baidu/hugegraph/core/AuthTest.java index 4f65a2d928..e64177a6df 100644 --- a/hugegraph-test/src/main/java/com/baidu/hugegraph/core/AuthTest.java +++ b/hugegraph-test/src/main/java/com/baidu/hugegraph/core/AuthTest.java @@ -24,6 +24,8 @@ import java.util.List; import java.util.Map; +import javax.security.sasl.AuthenticationException; + import org.junit.After; import org.junit.Test; @@ -37,10 +39,13 @@ import com.baidu.hugegraph.auth.HugeTarget; import com.baidu.hugegraph.auth.HugeUser; import com.baidu.hugegraph.auth.RolePermission; +import com.baidu.hugegraph.auth.UserWithRole; +import com.baidu.hugegraph.backend.cache.Cache; import com.baidu.hugegraph.backend.id.Id; import com.baidu.hugegraph.backend.id.IdGenerator; import com.baidu.hugegraph.exception.NotFoundException; import com.baidu.hugegraph.testutil.Assert; +import com.baidu.hugegraph.testutil.Whitebox; import com.baidu.hugegraph.util.JsonUtil; import com.baidu.hugegraph.util.StringEncoding; import com.google.common.collect.ImmutableList; @@ -1270,6 +1275,97 @@ public void testRolePermission() { Assert.assertEquals(expected, role.toJson()); } + @Test + public void testLogin() throws AuthenticationException { + AuthManager authManager = graph().authManager(); + + HugeUser user = makeUser("test", StringEncoding.hashPassword("pass")); + authManager.createUser(user); + + // Login + authManager.loginUser("test", "pass"); + + // Invalid username or password + Assert.assertThrows(AuthenticationException.class, () -> { + authManager.loginUser("huge", "graph"); + }, e -> { + Assert.assertContains("Incorrect username or password", e.getMessage()); + }); + } + + @Test + public void testValidateUserByToken() throws AuthenticationException { + AuthManager authManager = graph().authManager(); + + HugeUser user = makeUser("test", StringEncoding.hashPassword("pass")); + Id userId = authManager.createUser(user); + + String token = authManager.loginUser("test", "pass"); + + UserWithRole userWithRole; + userWithRole = authManager.validateUser(token); + Assert.assertEquals(userId, userWithRole.userId()); + Assert.assertEquals("test", userWithRole.username()); + Assert.assertEquals("{\"roles\":{}}", userWithRole.role().toJson()); + + // Token cache missed + Cache tokenCache = Whitebox.getInternalState(authManager, + "tokenCache"); + tokenCache.invalidate(IdGenerator.of(token)); + Assert.assertFalse(tokenCache.containsKey(IdGenerator.of(token))); + + userWithRole = authManager.validateUser(token); + Assert.assertEquals(userId, userWithRole.userId()); + Assert.assertEquals("test", userWithRole.username()); + Assert.assertEquals("{\"roles\":{}}", userWithRole.role().toJson()); + Assert.assertTrue(tokenCache.containsKey(IdGenerator.of(token))); + + // User deleted after login and token not expire + authManager.deleteUser(userId); + userWithRole = authManager.validateUser(token); + Assert.assertNull(null, userWithRole.userId()); + Assert.assertEquals("test", userWithRole.username()); + Assert.assertNull(userWithRole.role()); + } + + @Test + public void testLogout() throws AuthenticationException { + AuthManager authManager = graph().authManager(); + + HugeUser user = makeUser("test", StringEncoding.hashPassword("pass")); + Id userId = authManager.createUser(user); + + // Login + String token = authManager.loginUser("test", "pass"); + + // Logout + Cache tokenCache = Whitebox.getInternalState(authManager, + "tokenCache"); + Assert.assertTrue(tokenCache.containsKey(IdGenerator.of(token))); + authManager.logoutUser(token); + Assert.assertFalse(tokenCache.containsKey(IdGenerator.of(token))); + } + + @Test + public void testValidateUserByNameAndPassword() { + AuthManager authManager = graph().authManager(); + + HugeUser user = makeUser("test", StringEncoding.hashPassword("pass")); + Id userId = authManager.createUser(user); + + UserWithRole userWithRole; + userWithRole = authManager.validateUser("test", "pass"); + Assert.assertEquals(userId, userWithRole.userId()); + Assert.assertEquals("test", userWithRole.username()); + Assert.assertEquals("{\"roles\":{}}", userWithRole.role().toJson()); + + // Error case + userWithRole = authManager.validateUser("huge", "graph"); + Assert.assertNull(userWithRole.userId()); + Assert.assertEquals("huge", userWithRole.username()); + Assert.assertNull(userWithRole.role()); + } + private static HugeUser makeUser(String name, String password) { HugeUser user = new HugeUser(name); user.password(password);