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