Skip to content
Snippets Groups Projects
Verified Commit 49ef6b43 authored by Timm Fitschen's avatar Timm Fitschen
Browse files

WIP acm

parent 5fd18ca8
No related branches found
No related tags found
2 merge requests!58REL: prepare release 0.7.2,!45F grpc f acm
Showing
with 326 additions and 41 deletions
......@@ -25,7 +25,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>org.caosdb</groupId>
<artifactId>caosdb-server</artifactId>
<version>0.5.1-GRPC0.1.0</version>
<version>0.5.1-GRPC0.1.1</version>
<packaging>jar</packaging>
<name>CaosDB Server</name>
<scm>
......@@ -56,11 +56,6 @@
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>io.grpcweb</groupId>
<artifactId>grpcweb-java</artifactId>
<version>0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>de.timmfitschen</groupId>
<artifactId>easy-units</artifactId>
......
......@@ -45,6 +45,7 @@ import org.caosdb.server.database.backend.implementation.MySQL.MySQLInsertSparse
import org.caosdb.server.database.backend.implementation.MySQL.MySQLInsertTransactionHistory;
import org.caosdb.server.database.backend.implementation.MySQL.MySQLIsSubType;
import org.caosdb.server.database.backend.implementation.MySQL.MySQLListRoles;
import org.caosdb.server.database.backend.implementation.MySQL.MySQLListUsers;
import org.caosdb.server.database.backend.implementation.MySQL.MySQLRegisterSubDomain;
import org.caosdb.server.database.backend.implementation.MySQL.MySQLRetrieveAll;
import org.caosdb.server.database.backend.implementation.MySQL.MySQLRetrieveAllUncheckedFiles;
......@@ -101,6 +102,7 @@ import org.caosdb.server.database.backend.interfaces.InsertSparseEntityImpl;
import org.caosdb.server.database.backend.interfaces.InsertTransactionHistoryImpl;
import org.caosdb.server.database.backend.interfaces.IsSubTypeImpl;
import org.caosdb.server.database.backend.interfaces.ListRolesImpl;
import org.caosdb.server.database.backend.interfaces.ListUsersImpl;
import org.caosdb.server.database.backend.interfaces.RegisterSubDomainImpl;
import org.caosdb.server.database.backend.interfaces.RetrieveAllImpl;
import org.caosdb.server.database.backend.interfaces.RetrieveAllUncheckedFilesImpl;
......@@ -207,6 +209,7 @@ public abstract class BackendTransaction implements Undoable {
setImpl(InsertEntityDatatypeImpl.class, MySQLInsertEntityDatatype.class);
setImpl(RetrieveVersionHistoryImpl.class, MySQLRetrieveVersionHistory.class);
setImpl(SetFileChecksumImpl.class, MySQLSetFileChecksum.class);
setImpl(ListUsersImpl.class, MySQLListUsers.class);
}
}
......
package org.caosdb.server.database.backend.implementation.MySQL;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.LinkedList;
import java.util.List;
import org.caosdb.server.accessControl.UserStatus;
import org.caosdb.server.database.access.Access;
import org.caosdb.server.database.backend.interfaces.ListUsersImpl;
import org.caosdb.server.database.exceptions.TransactionException;
import org.caosdb.server.database.proto.ProtoUser;
public class MySQLListUsers extends MySQLTransaction implements ListUsersImpl {
public MySQLListUsers(Access access) {
super(access);
}
public static final String STMT_LIST_USERS =
"SELECT status, realm, name, email, entity FROM user_info";
@Override
public List<ProtoUser> execute() {
List<ProtoUser> users = new LinkedList<>();
try {
final PreparedStatement stmt = prepareStatement(STMT_LIST_USERS);
final ResultSet rs = stmt.executeQuery();
try {
while (rs.next()) {
final ProtoUser user = new ProtoUser();
user.name = rs.getString("name");
user.realm = rs.getString("realm");
user.email = rs.getString("email");
user.entity = rs.getInt("entity");
if (user.entity == 0) {
user.entity = null;
}
user.status = UserStatus.valueOf(rs.getString("status"));
users.add(user);
}
} finally {
rs.close();
}
} catch (final SQLException e) {
throw new TransactionException(e);
} catch (final ConnectionException e) {
throw new TransactionException(e);
}
return users;
}
}
package org.caosdb.server.database.backend.interfaces;
import java.util.List;
import org.caosdb.server.database.proto.ProtoUser;
public interface ListUsersImpl extends BackendTransactionImpl {
List<ProtoUser> execute();
}
package org.caosdb.server.database.backend.transaction;
import java.util.List;
import org.caosdb.server.database.BackendTransaction;
import org.caosdb.server.database.backend.interfaces.ListUsersImpl;
import org.caosdb.server.database.proto.ProtoUser;
public class ListUsers extends BackendTransaction {
private List<ProtoUser> users;
@Override
protected void execute() {
ListUsersImpl t = getImplementation(ListUsersImpl.class);
users = t.execute();
}
public List<ProtoUser> getUsers() {
return users;
}
}
......@@ -5,16 +5,65 @@ import java.util.List;
import org.caosdb.api.acm.v1alpha1.AccessControlManagementServiceGrpc.AccessControlManagementServiceImplBase;
import org.caosdb.api.acm.v1alpha1.CreateSingleRoleRequest;
import org.caosdb.api.acm.v1alpha1.CreateSingleRoleResponse;
import org.caosdb.api.acm.v1alpha1.CreateSingleUserRequest;
import org.caosdb.api.acm.v1alpha1.CreateSingleUserResponse;
import org.caosdb.api.acm.v1alpha1.ListRolesRequest;
import org.caosdb.api.acm.v1alpha1.ListRolesResponse;
import org.caosdb.api.acm.v1alpha1.ListUsersRequest;
import org.caosdb.api.acm.v1alpha1.ListUsersResponse;
import org.caosdb.api.acm.v1alpha1.User;
import org.caosdb.api.acm.v1alpha1.UserStatus;
import org.caosdb.server.accessControl.Role;
import org.caosdb.server.database.proto.ProtoUser;
import org.caosdb.server.transaction.InsertRoleTransaction;
import org.caosdb.server.transaction.InsertUserTransaction;
import org.caosdb.server.transaction.ListRolesTransaction;
import org.caosdb.server.transaction.ListUsersTransaction;
public class AccessControlManagementServiceImpl extends AccessControlManagementServiceImplBase {
/////////////////////////////////// CONVERTERS
private ProtoUser convert(User user) {
ProtoUser result = new ProtoUser();
result.realm = user.getRealm();
result.name = user.getName();
result.email = user.getEmail();
result.status =
(user.getStatus() == UserStatus.USER_STATUS_ACTIVE
? org.caosdb.server.accessControl.UserStatus.ACTIVE
: org.caosdb.server.accessControl.UserStatus.INACTIVE);
return result;
}
private ListUsersResponse convertUsers(List<ProtoUser> users) {
ListUsersResponse.Builder response = ListUsersResponse.newBuilder();
users.forEach(
user -> {
response.addUsers(convert(user));
});
return response.build();
}
private User.Builder convert(ProtoUser user) {
User.Builder result = User.newBuilder();
result.setStatus(convert(user.status));
result.setRealm(user.realm);
result.setName(user.name);
if (user.email != null) {
result.setEmail(user.email);
}
if (user.entity != null) {
result.setEntityId(Integer.toString(user.entity));
}
return result;
}
private UserStatus convert(org.caosdb.server.accessControl.UserStatus status) {
return UserStatus.valueOf(status.toString());
}
private Role convert(org.caosdb.api.acm.v1alpha1.Role role) {
Role result = new Role();
......@@ -62,8 +111,41 @@ public class AccessControlManagementServiceImpl extends AccessControlManagementS
return CreateSingleRoleResponse.newBuilder().build();
}
////////////////// ... for users
private ListUsersResponse listUsersTransaction(ListUsersRequest request) throws Exception {
ListUsersTransaction transaction = new ListUsersTransaction();
transaction.execute();
List<ProtoUser> users = transaction.getUsers();
return convertUsers(users);
}
private CreateSingleUserResponse createSingleUserTransaction(CreateSingleUserRequest request)
throws Exception {
ProtoUser user = convert(request.getUser());
InsertUserTransaction transaction =
new InsertUserTransaction(user, request.getUser().getPassword());
transaction.execute();
return CreateSingleUserResponse.newBuilder().build();
}
///////////////////////////////////// RPC Methods (API)
@Override
public void listUsers(
ListUsersRequest request, StreamObserver<ListUsersResponse> responseObserver) {
try {
final ListUsersResponse response = listUsersTransaction(request);
responseObserver.onNext(response);
responseObserver.onCompleted();
} catch (final Exception e) {
e.printStackTrace();
responseObserver.onError(e);
}
}
@Override
public void listRoles(
ListRolesRequest request, StreamObserver<ListRolesResponse> responseObserver) {
......@@ -91,4 +173,18 @@ public class AccessControlManagementServiceImpl extends AccessControlManagementS
responseObserver.onError(e);
}
}
@Override
public void createSingleUser(
CreateSingleUserRequest request, StreamObserver<CreateSingleUserResponse> responseObserver) {
try {
final CreateSingleUserResponse response = createSingleUserTransaction(request);
responseObserver.onNext(response);
responseObserver.onCompleted();
} catch (final Exception e) {
e.printStackTrace();
responseObserver.onError(e);
}
}
}
package org.caosdb.server.grpc;
import static org.caosdb.server.utils.Utils.URLDecodeWithUTF8;
import io.grpc.Context;
import io.grpc.Contexts;
import io.grpc.ForwardingServerCall;
import io.grpc.Metadata;
import io.grpc.Metadata.Key;
import io.grpc.ServerCall;
......@@ -10,14 +13,21 @@ import io.grpc.ServerCallHandler;
import io.grpc.ServerInterceptor;
import io.grpc.Status;
import java.util.Base64;
import java.util.function.Predicate;
import java.util.regex.Pattern;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.subject.Subject;
import org.caosdb.server.CaosDBServer;
import org.caosdb.server.ServerProperties;
import org.caosdb.server.accessControl.AnonymousAuthenticationToken;
import org.caosdb.server.accessControl.AuthenticationUtils;
import org.caosdb.server.accessControl.RealmUsernamePasswordToken;
import org.caosdb.server.accessControl.SelfValidatingAuthenticationToken;
import org.caosdb.server.accessControl.SessionToken;
import org.caosdb.server.accessControl.UserSources;
import org.caosdb.server.utils.Utils;
import org.restlet.data.CookieSetting;
/**
* ServerInterceptor for Authentication. If the authentication succeeds or if the caller is
......@@ -26,14 +36,20 @@ import org.caosdb.server.accessControl.UserSources;
*
* @author Timm Fitschen <t.fitschen@indiscale.com>
*/
class AuthInterceptor implements ServerInterceptor {
public class AuthInterceptor implements ServerInterceptor {
private static final Key<String> AUTHENTICATION_HEADER =
public static final Key<String> AUTHENTICATION_HEADER =
Key.of("authentication", Metadata.ASCII_STRING_MARSHALLER);
private static final Key<String> AUTHORIZATION_HEADER =
public static final Key<String> AUTHORIZATION_HEADER =
Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER);
private static final Context.Key<Subject> SUBJECT_KEY = Context.key("subject");
private static final String BASIC_SCHEME_PREFIX = "Basic ";
public static final Key<String> COOKIE_HEADER =
Key.of("Cookie", Metadata.ASCII_STRING_MARSHALLER);
public static final Context.Key<Subject> SUBJECT_KEY = Context.key("subject");
public static final String BASIC_SCHEME_PREFIX = "Basic ";
public static final Pattern SESSION_TOKEN_COOKIE_PREFIX_PATTERN =
Pattern.compile("^\\s*" + AuthenticationUtils.SESSION_TOKEN_COOKIE + "\\s*=\\s*");
public static final Predicate<String> SESSION_TOKEN_COOKIE_PREFIX_PREDICATE =
SESSION_TOKEN_COOKIE_PREFIX_PATTERN.asPredicate();
/**
* A no-op listener. This class is used for failed authentications. We couldn't return a null
* instead because the documentation of the {@link ServerInterceptor} explicitely forbids it.
......@@ -73,6 +89,9 @@ class AuthInterceptor implements ServerInterceptor {
if (authentication == null) {
authentication = headers.get(AUTHORIZATION_HEADER);
}
if (authentication == null) {
authentication = headers.get(COOKIE_HEADER);
}
Status status =
Status.UNKNOWN.withDescription(
"An unknown error occured during authentication. Please report a bug.");
......@@ -82,15 +101,47 @@ class AuthInterceptor implements ServerInterceptor {
status = Status.UNAUTHENTICATED.withDescription("Please login.");
} else if (authentication.startsWith(BASIC_SCHEME_PREFIX)) {
return basicAuth(authentication.substring(BASIC_SCHEME_PREFIX.length()), call, headers, next);
} else if (SESSION_TOKEN_COOKIE_PREFIX_PREDICATE.test(authentication)) {
return sessionTokenAuth(
SESSION_TOKEN_COOKIE_PREFIX_PATTERN.split(authentication, 2)[1], call, headers, next);
} else {
status =
Status.UNAUTHENTICATED.withDescription(
"Unsupported authentication scheme: " + authentication.split(" ", 2)[0]);
status = Status.UNAUTHENTICATED.withDescription("Unsupported authentication scheme.");
}
call.close(status, new Metadata());
return new NoOpListener<ReqT>();
}
/**
* Login via AuthenticationToken and add the resulting subject to the call context.
*
* @see #updateContext(Subject, ServerCall, Metadata, ServerCallHandler) for more information.
*/
private <ReqT, RespT> Listener<ReqT> sessionTokenAuth(
String sessionTokenCookie,
ServerCall<ReqT, RespT> call,
Metadata headers,
ServerCallHandler<ReqT, RespT> next) {
try {
final String tokenString = URLDecodeWithUTF8(sessionTokenCookie.split(";")[0]);
final Subject subject = sessionTokenAuth(tokenString);
return updateContext(subject, call, headers, next);
} catch (final AuthenticationException e) {
final Status status =
Status.UNAUTHENTICATED.withDescription(
"Authentication failed. SessionToken was invalid.");
call.close(status, new Metadata());
return new NoOpListener<ReqT>();
}
}
/** Login via AuthenticationToken and return the logged-in subject. */
private Subject sessionTokenAuth(String tokenString) {
Subject subject = SecurityUtils.getSubject();
subject.login(SelfValidatingAuthenticationToken.parse(tokenString));
return subject;
}
/**
* Login via username and password with the basic authentication scheme and add the resulting
* subject to the call context.
......@@ -145,6 +196,54 @@ class AuthInterceptor implements ServerInterceptor {
final ServerCallHandler<ReqT, RespT> next) {
final Context context = Context.current();
context.withValue(SUBJECT_KEY, subject);
return Contexts.interceptCall(Context.current(), call, headers, next);
ServerCall<ReqT, RespT> cookieSetter = new CookieSetter<>(call);
return Contexts.interceptCall(context, cookieSetter, headers, next);
}
}
final class CookieSetter<ReqT, RespT>
extends ForwardingServerCall.SimpleForwardingServerCall<ReqT, RespT> {
private static final Key<String> SET_COOKIE =
Key.of("Set-Cookie", Metadata.ASCII_STRING_MARSHALLER);
protected CookieSetter(ServerCall<ReqT, RespT> delegate) {
super(delegate);
}
String getSessionTimeoutSeconds() {
int ms =
Integer.parseInt(CaosDBServer.getServerProperty(ServerProperties.KEY_SESSION_TIMEOUT_MS));
int seconds = (int) Math.floor(ms / 1000);
return Integer.toString(seconds);
}
@Override
public void sendHeaders(Metadata headers) {
setSessionCookies(headers);
super.sendHeaders(headers);
};
private void setSessionCookies(Metadata headers) {
final Subject subject = SecurityUtils.getSubject();
// if authenticated as a normal user: generate and set session cookie.
if (subject.isAuthenticated()
&& subject.getPrincipal() != AnonymousAuthenticationToken.PRINCIPAL) {
final SessionToken sessionToken = SessionToken.generate(subject);
if (sessionToken != null && sessionToken.isValid()) {
final CookieSetting sessionTokenCookie =
AuthenticationUtils.createSessionTokenCookie(sessionToken);
if (sessionTokenCookie != null) {
// TODO add "Secure;" to cookie setting
headers.put(
SET_COOKIE,
AuthenticationUtils.SESSION_TOKEN_COOKIE
+ "="
+ Utils.URLEncodeWithUTF8(sessionToken.toString())
+ "; Path=/; HttpOnly; SameSite=Strict; Max-Age="
+ getSessionTimeoutSeconds());
}
}
}
}
}
......@@ -24,9 +24,6 @@ import io.grpc.ServerInterceptors;
import io.grpc.ServerServiceDefinition;
import io.grpc.netty.GrpcSslContexts;
import io.grpc.netty.NettyServerBuilder;
import io.grpcweb.GrpcPortNumRelay;
import io.grpcweb.JettyWebserverForGrpcwebTraffic;
import io.grpcweb.ServiceToClassMapping;
import io.netty.handler.ssl.ApplicationProtocolConfig;
import io.netty.handler.ssl.ApplicationProtocolConfig.Protocol;
import io.netty.handler.ssl.ApplicationProtocolConfig.SelectedListenerFailureBehavior;
......@@ -45,10 +42,6 @@ import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.net.ssl.KeyManagerFactory;
import org.caosdb.api.acm.v1alpha1.AccessControlManagementServiceGrpc;
import org.caosdb.api.entity.v1alpha1.EntityTransactionServiceGrpc;
import org.caosdb.api.entity.v1alpha1.FileTransmissionServiceGrpc;
import org.caosdb.api.info.v1alpha1.GeneralInfoServiceGrpc;
import org.caosdb.server.CaosDBServer;
import org.caosdb.server.ServerProperties;
import org.slf4j.Logger;
......@@ -122,29 +115,20 @@ public class GRPCServer {
private List<ServerServiceDefinition> getEnabledServices() {
final List<ServerServiceDefinition> services = new LinkedList<>();
// Add mapping from the service name to the service class (for the grpc-web proxy)
ServiceToClassMapping.put(
AccessControlManagementServiceGrpc.SERVICE_NAME, AccessControlManagementServiceGrpc.class);
final AccessControlManagementServiceImpl accessControlManagementService =
new AccessControlManagementServiceImpl();
services.add(ServerInterceptors.intercept(accessControlManagementService, authInterceptor));
services.add(
ServerInterceptors.intercept(
accessControlManagementService, loggingInterceptor, authInterceptor));
// Add mapping from the service name to the service class (for the grpc-web proxy)
ServiceToClassMapping.put(GeneralInfoServiceGrpc.SERVICE_NAME, GeneralInfoServiceGrpc.class);
final GeneralInfoServiceImpl generalInfoService = new GeneralInfoServiceImpl();
services.add(
ServerInterceptors.intercept(generalInfoService, loggingInterceptor, authInterceptor));
// Add mapping from the service name to the service class (for the grpc-web proxy)
ServiceToClassMapping.put(
FileTransmissionServiceGrpc.SERVICE_NAME, FileTransmissionServiceGrpc.class);
final FileTransmissionServiceImpl fileTransmissionService = new FileTransmissionServiceImpl();
services.add(
ServerInterceptors.intercept(fileTransmissionService, loggingInterceptor, authInterceptor));
// Add mapping from the service name to the service class (for the grpc-web proxy)
ServiceToClassMapping.put(
EntityTransactionServiceGrpc.SERVICE_NAME, EntityTransactionServiceGrpc.class);
final EntityTransactionServiceImpl entityTransactionService =
new EntityTransactionServiceImpl(fileTransmissionService);
services.add(
......@@ -220,12 +204,6 @@ public class GRPCServer {
server.start();
logger.info("Started GRPC (HTTP) on port {}", port_http);
// Start the grpc-web proxy on grpc-web-port.
(new JettyWebserverForGrpcwebTraffic(9443)).start();
// grpc-web proxy needs to know the grpc-port# so it could connect to the grpc service.
GrpcPortNumRelay.setGrpcPortNum(port_http);
} else if (!started) {
logger.warn(
"No GRPC Server has been started. Please configure {} or {} to do so.",
......
......@@ -36,7 +36,7 @@ import org.jdom2.Element;
public class InsertUserTransaction extends AccessControlTransaction {
ProtoUser user = new ProtoUser();
private final ProtoUser user;
private final String password;
public InsertUserTransaction(
......@@ -45,11 +45,16 @@ public class InsertUserTransaction extends AccessControlTransaction {
final String email,
final UserStatus status,
final Integer entity) {
this(new ProtoUser(), password);
this.user.realm = UserSources.getInternalRealm().getName();
this.user.name = username;
this.user.email = email;
this.user.status = status;
this.user.entity = entity;
}
public InsertUserTransaction(ProtoUser user, String password) {
this.user = user;
this.password = password;
}
......
package org.caosdb.server.transaction;
import java.util.List;
import org.caosdb.server.database.backend.transaction.ListUsers;
import org.caosdb.server.database.proto.ProtoUser;
public class ListUsersTransaction extends AccessControlTransaction {
private List<ProtoUser> users = null;
@Override
protected void transaction() throws Exception {
users = execute(new ListUsers(), getAccess()).getUsers();
}
public List<ProtoUser> getUsers() {
return users;
}
}
......@@ -24,6 +24,7 @@ package org.caosdb.server.authentication;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
......@@ -53,6 +54,7 @@ import org.caosdb.server.database.backend.interfaces.RetrievePasswordValidatorIm
import org.caosdb.server.database.backend.interfaces.RetrievePermissionRulesImpl;
import org.caosdb.server.database.backend.interfaces.RetrieveRoleImpl;
import org.caosdb.server.database.backend.interfaces.RetrieveUserImpl;
import org.caosdb.server.grpc.AuthInterceptor;
import org.caosdb.server.resource.TestScriptingResource.RetrievePasswordValidator;
import org.caosdb.server.resource.TestScriptingResource.RetrievePermissionRules;
import org.caosdb.server.resource.TestScriptingResource.RetrieveRole;
......@@ -448,4 +450,10 @@ public class AuthTokenTest {
assertEquals(9223372036854775000L, config.getExpiresAfter());
assertEquals(922337203685477000L, config.getReplayTimeout());
}
@Test
public void testSessionTokenCookiePattern() {
String cookie = "SessionToken=%5B%22S%22%22%5D";
assertTrue(AuthInterceptor.SESSION_TOKEN_COOKIE_PREFIX_PATTERN.matcher(cookie).find());
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment