blob: 123c8e9ffd59198686b841fd25c972e594d03a9b [file] [log] [blame]
/*
* 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 org.apache.hugegraph.auth;
import java.time.Duration;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import javax.security.sasl.AuthenticationException;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.hugegraph.HugeException;
import org.apache.hugegraph.HugeGraphParams;
import org.apache.hugegraph.auth.HugeUser.P;
import org.apache.hugegraph.auth.SchemaDefine.AuthElement;
import org.apache.hugegraph.backend.cache.Cache;
import org.apache.hugegraph.backend.cache.CacheManager;
import org.apache.hugegraph.backend.id.Id;
import org.apache.hugegraph.backend.id.IdGenerator;
import org.apache.hugegraph.config.AuthOptions;
import org.apache.hugegraph.config.HugeConfig;
import org.apache.hugegraph.type.define.Directions;
import org.apache.hugegraph.util.E;
import org.apache.hugegraph.util.LockUtil;
import org.apache.hugegraph.util.Log;
import org.apache.hugegraph.util.StringEncoding;
import org.slf4j.Logger;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import io.jsonwebtoken.Claims;
import jakarta.ws.rs.ForbiddenException;
public class StandardAuthManager implements AuthManager {
protected static final Logger LOG = Log.logger(StandardAuthManager.class);
private final HugeGraphParams graph;
// Cache <username, HugeUser>
private final Cache<Id, HugeUser> usersCache;
// Cache <userId, passwd>
private final Cache<Id, String> pwdCache;
// Cache <token, username>
private final Cache<Id, String> tokenCache;
private final EntityManager<HugeUser> users;
private final EntityManager<HugeGroup> groups;
private final EntityManager<HugeTarget> targets;
private final EntityManager<HugeProject> project;
private final RelationshipManager<HugeBelong> belong;
private final RelationshipManager<HugeAccess> access;
private final TokenGenerator tokenGenerator;
private final long tokenExpire;
private Set<String> ipWhiteList;
private Boolean ipWhiteListEnabled;
public StandardAuthManager(HugeGraphParams graph) {
E.checkNotNull(graph, "graph");
HugeConfig config = graph.configuration();
long expired = config.get(AuthOptions.AUTH_CACHE_EXPIRE);
long capacity = config.get(AuthOptions.AUTH_CACHE_CAPACITY);
this.tokenExpire = config.get(AuthOptions.AUTH_TOKEN_EXPIRE) * 1000;
this.graph = graph;
this.usersCache = this.cache("users", capacity, expired);
this.pwdCache = this.cache("users_pwd", capacity, expired);
this.tokenCache = this.cache("token", capacity, expired);
this.users = new EntityManager<>(this.graph, HugeUser.P.USER,
HugeUser::fromVertex);
this.groups = new EntityManager<>(this.graph, HugeGroup.P.GROUP,
HugeGroup::fromVertex);
this.targets = new EntityManager<>(this.graph, HugeTarget.P.TARGET,
HugeTarget::fromVertex);
this.project = new EntityManager<>(this.graph, HugeProject.P.PROJECT,
HugeProject::fromVertex);
this.belong = new RelationshipManager<>(this.graph, HugeBelong.P.BELONG,
HugeBelong::fromEdge);
this.access = new RelationshipManager<>(this.graph, HugeAccess.P.ACCESS,
HugeAccess::fromEdge);
this.tokenGenerator = new TokenGenerator(config);
this.ipWhiteList = new HashSet<>();
this.ipWhiteListEnabled = false;
}
private <V> Cache<Id, V> cache(String prefix, long capacity,
long expiredTime) {
String name = prefix + "-" + this.graph.name();
Cache<Id, V> cache = CacheManager.instance().cache(name, capacity);
if (expiredTime > 0L) {
cache.expire(Duration.ofSeconds(expiredTime).toMillis());
} else {
cache.expire(expiredTime);
}
return cache;
}
@Override
public void init() {
this.invalidateUserCache();
HugeUser.schema(this.graph).initSchemaIfNeeded();
HugeGroup.schema(this.graph).initSchemaIfNeeded();
HugeTarget.schema(this.graph).initSchemaIfNeeded();
HugeBelong.schema(this.graph).initSchemaIfNeeded();
HugeAccess.schema(this.graph).initSchemaIfNeeded();
HugeProject.schema(this.graph).initSchemaIfNeeded();
}
@Override
public boolean close() {
return true;
}
private void invalidateUserCache() {
this.usersCache.clear();
}
private void invalidatePasswordCache(Id id) {
this.pwdCache.invalidate(id);
// Clear all tokenCache because can't get userId in it
this.tokenCache.clear();
}
@Override
public Id createUser(HugeUser user) {
this.invalidateUserCache();
return this.users.add(user);
}
@Override
public Id updateUser(HugeUser user) {
this.invalidateUserCache();
this.invalidatePasswordCache(user.id());
return this.users.update(user);
}
@Override
public HugeUser deleteUser(Id id) {
this.invalidateUserCache();
this.invalidatePasswordCache(id);
return this.users.delete(id);
}
@Override
public HugeUser findUser(String name) {
Id username = IdGenerator.of(name);
HugeUser user = this.usersCache.get(username);
if (user != null) {
return user;
}
List<HugeUser> users = this.users.query(P.NAME, name, 2L);
if (users.size() > 0) {
assert users.size() == 1;
user = users.get(0);
this.usersCache.update(username, user);
}
return user;
}
@Override
public HugeUser getUser(Id id) {
return this.users.get(id);
}
@Override
public List<HugeUser> listUsers(List<Id> ids) {
return this.users.list(ids);
}
@Override
public List<HugeUser> listAllUsers(long limit) {
return this.users.list(limit);
}
@Override
public Id createGroup(HugeGroup group) {
this.invalidateUserCache();
return this.groups.add(group);
}
@Override
public Id updateGroup(HugeGroup group) {
this.invalidateUserCache();
return this.groups.update(group);
}
@Override
public HugeGroup deleteGroup(Id id) {
this.invalidateUserCache();
return this.groups.delete(id);
}
@Override
public HugeGroup getGroup(Id id) {
return this.groups.get(id);
}
@Override
public List<HugeGroup> listGroups(List<Id> ids) {
return this.groups.list(ids);
}
@Override
public List<HugeGroup> listAllGroups(long limit) {
return this.groups.list(limit);
}
@Override
public Id createTarget(HugeTarget target) {
this.invalidateUserCache();
return this.targets.add(target);
}
@Override
public Id updateTarget(HugeTarget target) {
this.invalidateUserCache();
return this.targets.update(target);
}
@Override
public HugeTarget deleteTarget(Id id) {
this.invalidateUserCache();
return this.targets.delete(id);
}
@Override
public HugeTarget getTarget(Id id) {
return this.targets.get(id);
}
@Override
public List<HugeTarget> listTargets(List<Id> ids) {
return this.targets.list(ids);
}
@Override
public List<HugeTarget> listAllTargets(long limit) {
return this.targets.list(limit);
}
@Override
public Id createBelong(HugeBelong belong) {
this.invalidateUserCache();
E.checkArgument(this.users.exists(belong.source()),
"Not exists user '%s'", belong.source());
E.checkArgument(this.groups.exists(belong.target()),
"Not exists group '%s'", belong.target());
return this.belong.add(belong);
}
@Override
public Id updateBelong(HugeBelong belong) {
this.invalidateUserCache();
return this.belong.update(belong);
}
@Override
public HugeBelong deleteBelong(Id id) {
this.invalidateUserCache();
return this.belong.delete(id);
}
@Override
public HugeBelong getBelong(Id id) {
return this.belong.get(id);
}
@Override
public List<HugeBelong> listBelong(List<Id> ids) {
return this.belong.list(ids);
}
@Override
public List<HugeBelong> listAllBelong(long limit) {
return this.belong.list(limit);
}
@Override
public List<HugeBelong> listBelongByUser(Id user, long limit) {
return this.belong.list(user, Directions.OUT,
HugeBelong.P.BELONG, limit);
}
@Override
public List<HugeBelong> listBelongByGroup(Id group, long limit) {
return this.belong.list(group, Directions.IN,
HugeBelong.P.BELONG, limit);
}
@Override
public Id createAccess(HugeAccess access) {
this.invalidateUserCache();
E.checkArgument(this.groups.exists(access.source()),
"Not exists group '%s'", access.source());
E.checkArgument(this.targets.exists(access.target()),
"Not exists target '%s'", access.target());
return this.access.add(access);
}
@Override
public Id updateAccess(HugeAccess access) {
this.invalidateUserCache();
return this.access.update(access);
}
@Override
public HugeAccess deleteAccess(Id id) {
this.invalidateUserCache();
return this.access.delete(id);
}
@Override
public HugeAccess getAccess(Id id) {
return this.access.get(id);
}
@Override
public List<HugeAccess> listAccess(List<Id> ids) {
return this.access.list(ids);
}
@Override
public List<HugeAccess> listAllAccess(long limit) {
return this.access.list(limit);
}
@Override
public List<HugeAccess> listAccessByGroup(Id group, long limit) {
return this.access.list(group, Directions.OUT,
HugeAccess.P.ACCESS, limit);
}
@Override
public List<HugeAccess> listAccessByTarget(Id target, long limit) {
return this.access.list(target, Directions.IN,
HugeAccess.P.ACCESS, limit);
}
@Override
public Id createProject(HugeProject project) {
E.checkArgument(!StringUtils.isEmpty(project.name()),
"The name of project can't be null or empty");
return commit(() -> {
// Create project admin group
if (project.adminGroupId() == null) {
HugeGroup adminGroup = new HugeGroup("admin_" + project.name());
/*
* "creator" is a necessary parameter, other places are passed
* in "AuthManagerProxy", but here is the underlying module, so
* pass it directly here
*/
adminGroup.creator(project.creator());
Id adminGroupId = this.createGroup(adminGroup);
project.adminGroupId(adminGroupId);
}
// Create project op group
if (project.opGroupId() == null) {
HugeGroup opGroup = new HugeGroup("op_" + project.name());
// Ditto
opGroup.creator(project.creator());
Id opGroupId = this.createGroup(opGroup);
project.opGroupId(opGroupId);
}
// Create project target to verify permission
final String targetName = "project_res_" + project.name();
HugeResource resource = new HugeResource(ResourceType.PROJECT,
project.name(),
null);
HugeTarget target = new HugeTarget(targetName,
this.graph.name(),
"localhost:8080",
ImmutableList.of(resource));
// Ditto
target.creator(project.creator());
Id targetId = this.targets.add(target);
project.targetId(targetId);
Id adminGroupId = project.adminGroupId();
Id opGroupId = project.opGroupId();
HugeAccess adminGroupWriteAccess = new HugeAccess(
adminGroupId, targetId,
HugePermission.WRITE);
// Ditto
adminGroupWriteAccess.creator(project.creator());
HugeAccess adminGroupReadAccess = new HugeAccess(
adminGroupId, targetId,
HugePermission.READ);
// Ditto
adminGroupReadAccess.creator(project.creator());
HugeAccess opGroupReadAccess = new HugeAccess(opGroupId, targetId,
HugePermission.READ);
// Ditto
opGroupReadAccess.creator(project.creator());
this.access.add(adminGroupWriteAccess);
this.access.add(adminGroupReadAccess);
this.access.add(opGroupReadAccess);
return this.project.add(project);
});
}
@Override
public HugeProject deleteProject(Id id) {
return this.commit(() -> {
LockUtil.Locks locks = new LockUtil.Locks(this.graph.name());
try {
locks.lockWrites(LockUtil.PROJECT_UPDATE, id);
HugeProject oldProject = this.project.get(id);
/*
* Check whether there are any graph binding this project,
* throw ForbiddenException, if it is
*/
if (!CollectionUtils.isEmpty(oldProject.graphs())) {
String errInfo = String.format("Can't delete project '%s' " +
"that contains any graph, " +
"there are graphs bound " +
"to it", id);
throw new ForbiddenException(errInfo);
}
HugeProject project = this.project.delete(id);
E.checkArgumentNotNull(project,
"Failed to delete the project '%s'",
id);
E.checkArgumentNotNull(project.adminGroupId(),
"Failed to delete the project '%s'," +
"the admin group of project can't " +
"be null", id);
E.checkArgumentNotNull(project.opGroupId(),
"Failed to delete the project '%s'," +
"the op group of project can't be null",
id);
E.checkArgumentNotNull(project.targetId(),
"Failed to delete the project '%s', " +
"the target resource of project " +
"can't be null", id);
// Delete admin group
this.groups.delete(project.adminGroupId());
// Delete op group
this.groups.delete(project.opGroupId());
// Delete project_target
this.targets.delete(project.targetId());
return project;
} finally {
locks.unlock();
}
});
}
@Override
public Id updateProject(HugeProject project) {
return this.project.update(project);
}
@Override
public Id projectAddGraphs(Id id, Set<String> graphs) {
E.checkArgument(!CollectionUtils.isEmpty(graphs),
"Failed to add graphs to project '%s', the graphs " +
"parameter can't be empty", id);
LockUtil.Locks locks = new LockUtil.Locks(this.graph.name());
try {
locks.lockWrites(LockUtil.PROJECT_UPDATE, id);
HugeProject project = this.project.get(id);
Set<String> sourceGraphs = new HashSet<>(project.graphs());
int oldSize = sourceGraphs.size();
sourceGraphs.addAll(graphs);
// Return if there is none graph been added
if (sourceGraphs.size() == oldSize) {
return id;
}
project.graphs(sourceGraphs);
return this.project.update(project);
} finally {
locks.unlock();
}
}
@Override
public Id projectRemoveGraphs(Id id, Set<String> graphs) {
E.checkArgumentNotNull(id,
"Failed to remove graphs, the project id " +
"parameter can't be null");
E.checkArgument(!CollectionUtils.isEmpty(graphs),
"Failed to delete graphs from the project '%s', " +
"the graphs parameter can't be null or empty", id);
LockUtil.Locks locks = new LockUtil.Locks(this.graph.name());
try {
locks.lockWrites(LockUtil.PROJECT_UPDATE, id);
HugeProject project = this.project.get(id);
Set<String> sourceGraphs = new HashSet<>(project.graphs());
int oldSize = sourceGraphs.size();
sourceGraphs.removeAll(graphs);
// Return if there is none graph been removed
if (sourceGraphs.size() == oldSize) {
return id;
}
project.graphs(sourceGraphs);
return this.project.update(project);
} finally {
locks.unlock();
}
}
@Override
public HugeProject getProject(Id id) {
return this.project.get(id);
}
@Override
public List<HugeProject> listAllProject(long limit) {
return this.project.list(limit);
}
@Override
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;
}
if (password.equals(this.pwdCache.get(user.id()))) {
return user;
}
if (StringEncoding.checkPassword(password, user.password())) {
this.pwdCache.update(user.id(), password);
return user;
}
return null;
}
@Override
public RolePermission rolePermission(AuthElement element) {
if (element instanceof HugeUser) {
return this.rolePermission((HugeUser) element);
} else if (element instanceof HugeTarget) {
return this.rolePermission((HugeTarget) element);
}
List<HugeAccess> accesses = new ArrayList<>();
if (element instanceof HugeBelong) {
HugeBelong belong = (HugeBelong) element;
accesses.addAll(this.listAccessByGroup(belong.target(), -1));
} else if (element instanceof HugeGroup) {
HugeGroup group = (HugeGroup) element;
accesses.addAll(this.listAccessByGroup(group.id(), -1));
} else if (element instanceof HugeAccess) {
HugeAccess access = (HugeAccess) element;
accesses.add(access);
} else {
E.checkArgument(false, "Invalid type for role permission: %s",
element);
}
return this.rolePermission(accesses);
}
private RolePermission rolePermission(HugeUser user) {
if (user.role() != null) {
// Return cached role (40ms => 10ms)
return user.role();
}
// Collect accesses by user
List<HugeAccess> accesses = new ArrayList<>();
List<HugeBelong> belongs = this.listBelongByUser(user.id(), -1);
for (HugeBelong belong : belongs) {
accesses.addAll(this.listAccessByGroup(belong.target(), -1));
}
// Collect permissions by accesses
RolePermission role = this.rolePermission(accesses);
user.role(role);
return role;
}
private RolePermission rolePermission(List<HugeAccess> accesses) {
// Mapping of: graph -> action -> resource
RolePermission role = new RolePermission();
for (HugeAccess access : accesses) {
HugePermission accessPerm = access.permission();
HugeTarget target = this.getTarget(access.target());
role.add(target.graph(), accessPerm, target.resources());
}
return role;
}
private RolePermission rolePermission(HugeTarget target) {
RolePermission role = new RolePermission();
// TODO: improve for the actual meaning
role.add(target.graph(), HugePermission.READ, target.resources());
return role;
}
@Override
public String loginUser(String username, String password)
throws AuthenticationException {
HugeUser user = this.matchUser(username, password);
if (user == null) {
String msg = "Incorrect username or password";
throw new AuthenticationException(msg);
}
Map<String, ?> payload = ImmutableMap.of(AuthConstant.TOKEN_USER_NAME,
username,
AuthConstant.TOKEN_USER_ID,
user.id.asString());
String token = this.tokenGenerator.create(payload, this.tokenExpire);
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 = this.tokenCache.get(IdGenerator.of(token));
Claims payload = null;
boolean needBuildCache = false;
if (username == null) {
try{
payload = this.tokenGenerator.verify(token);
}catch (Throwable t){
LOG.error(String.format("Failed to verify token:[ %s ], cause:",token),t);
return new UserWithRole("");
}
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 = this.tokenCache.expire() -
(expireAt - System.currentTimeMillis());
this.tokenCache.update(IdGenerator.of(token), username,
Math.negateExact(bornTime));
}
return new UserWithRole(user.id(), username, this.rolePermission(user));
}
@Override
public Set<String> listWhiteIPs() {
return ipWhiteList;
}
@Override
public void setWhiteIPs(Set<String> ipWhiteList) {
this.ipWhiteList = ipWhiteList;
}
@Override
public boolean getWhiteIpStatus() {
return this.ipWhiteListEnabled;
}
@Override
public void enabledWhiteIpList(boolean status) {
this.ipWhiteListEnabled = status;
}
/**
* Maybe can define an proxy class to choose forward or call local
*/
public static boolean isLocal(AuthManager authManager) {
return authManager instanceof StandardAuthManager;
}
public <R> R commit(Callable<R> callable) {
this.groups.autoCommit(false);
this.access.autoCommit(false);
this.targets.autoCommit(false);
this.project.autoCommit(false);
this.belong.autoCommit(false);
this.users.autoCommit(false);
try {
R result = callable.call();
this.graph.systemTransaction().commit();
return result;
} catch (Throwable e) {
this.groups.autoCommit(true);
this.access.autoCommit(true);
this.targets.autoCommit(true);
this.project.autoCommit(true);
this.belong.autoCommit(true);
this.users.autoCommit(true);
try {
this.graph.systemTransaction().rollback();
} catch (Throwable rollbackException) {
LOG.error("Failed to rollback transaction: {}",
rollbackException.getMessage(), rollbackException);
}
if (e instanceof HugeException) {
throw (HugeException) e;
} else {
throw new HugeException("Failed to commit transaction: %s",
e.getMessage(), e);
}
}
}
}