package org.yamcs.security; import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; import java.util.Base64; import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.FutureTask; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.yamcs.ConfigurationException; import org.yamcs.YConfiguration; import org.yamcs.api.MediaType; import org.yamcs.protobuf.Web.RestExceptionMessage; import org.yamcs.security.Privilege.Type; import org.yamcs.web.BadRequestException; import org.yamcs.web.rest.RestRequest; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.channel.ChannelHandlerContext; import io.netty.handler.codec.http.DefaultFullHttpResponse; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http.HttpRequest; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseStatus; import io.netty.util.CharsetUtil; public class BasicAuthModule implements AuthModule { private static final Logger log = LoggerFactory.getLogger(BasicAuthModule.class); private final Realm realm; private String realmName; // time to cache a user entry static final int PRIV_CACHE_TIME = 30*1000; // time to cache a certificate to username mapping private final ConcurrentHashMap<AuthenticationToken, Future<User>> cache = new ConcurrentHashMap<>(); public BasicAuthModule(Map<String, Object> config) { String realmClass = YConfiguration.getString(config, "realm"); realm = loadRealm(realmClass); } private Realm loadRealm(String realmClass) throws ConfigurationException { // load the specified class; Realm r; try { r = (Realm) Realm.class.getClassLoader() .loadClass(realmClass).newInstance(); realmName = r.getClass().getSimpleName(); } catch (Exception e) { throw new ConfigurationException("Unable to load the realm class: " + realmClass, e); } return r; } /** * * @return the roles of the calling user */ public String[] getRoles(final AuthenticationToken authenticationToken) { // Load user and read roles from result User user = getUser(authenticationToken); if(user == null) { return null; } return user.getRoles(); } @Override public CompletableFuture<AuthenticationToken> authenticateHttp(ChannelHandlerContext ctx, HttpRequest req) { if (!req.headers().contains(HttpHeaderNames.AUTHORIZATION)) { sendUnauthorized(ctx, req, "No "+HttpHeaderNames.AUTHORIZATION+" header present"); return completedExceptionally(new AuthorizationPendingException()); } String authorizationHeader = req.headers().get(HttpHeaderNames.AUTHORIZATION); if (!authorizationHeader.startsWith("Basic ")) { // Exact case only return completedExceptionally(new BadRequestException("Unsupported Authorization header '" + authorizationHeader + "'")); } // Get encoded user and password, comes after "Basic " String userpassEncoded = authorizationHeader.substring(6); String userpassDecoded; try { userpassDecoded = new String(Base64.getDecoder().decode(userpassEncoded)); } catch (IllegalArgumentException e) { return completedExceptionally( new BadRequestException("Could not decode Base64-encoded credentials")); } // Username is not allowed to contain ':', but passwords are String[] parts = userpassDecoded.split(":", 2); if (parts.length < 2) { return completedExceptionally( new BadRequestException("Malformed username/password (Not separated by colon?)")); } AuthenticationToken token = new UsernamePasswordToken(parts[0], parts[1]); if (!realm.authenticates(token)) { sendUnauthorized(ctx, req, "the realm could not authenticate the provided token"); return completedExceptionally( new AuthorizationPendingException()); } return CompletableFuture.completedFuture(token); } static private CompletableFuture<AuthenticationToken> completedExceptionally(Exception e) { CompletableFuture<AuthenticationToken> cf = new CompletableFuture<AuthenticationToken>(); cf.completeExceptionally(e); return cf; } public User getUser(final AuthenticationToken authenticationToken) { while (true) { if(authenticationToken == null) return null; Future<User> f = cache.get(authenticationToken); if (f == null) { Callable<User> eval = new Callable<User>() { @Override public User call() { try { // check the realm support the type of provided token if (!realm.supports(authenticationToken)) { log.error("Realm {} does not support authentication token of type {}" ,realmName, authenticationToken.getClass()); return null; } return realm.loadUser(authenticationToken); } catch (Exception e) { log.error("Unable to load user from realm {}", realmName, e); return new User(authenticationToken); } } }; FutureTask<User> ft = new FutureTask<User>(eval); f = cache.putIfAbsent(authenticationToken, ft); if (f == null) { f = ft; ft.run(); } } try { User u = f.get(); if ((System.currentTimeMillis() - u.lastUpdated) < PRIV_CACHE_TIME) return u; cache.remove(authenticationToken, f); // too old } catch (CancellationException e) { cache.remove(authenticationToken, f); } catch (ExecutionException e) { cache.remove(authenticationToken,f); //we don't cache exceptions if (e.getCause() instanceof RuntimeException) throw (RuntimeException) e.getCause(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return null; } catch (Exception e) { log.error("Unable to load user", e); return null; } } } /** * * @param type * @param privilege * a opsname of tc, tm parameter or tm packet * @return true if the privilege is known and the current user has it. */ public boolean hasPrivilege(final AuthenticationToken authenticationToken, Type type, String privilege) { User user = getUser(authenticationToken); if(user == null) { return false; } return user.hasPrivilege(type, privilege); } public boolean hasRole(final AuthenticationToken authenticationToken, String role ) { // Load user and read role from result User user = getUser(authenticationToken); if(user == null) { return false; } return user.hasRole(role); } private ChannelFuture sendUnauthorized(ChannelHandlerContext ctx, HttpRequest request, String reason) { ByteBuf buf; MediaType mt = RestRequest.deriveTargetContentType(request); if(mt==MediaType.PROTOBUF) { RestExceptionMessage rem = RestExceptionMessage.newBuilder().setMsg(HttpResponseStatus.UNAUTHORIZED.toString()).build(); buf = Unpooled.copiedBuffer(rem.toByteArray()); } else { buf = Unpooled.copiedBuffer(HttpResponseStatus.UNAUTHORIZED.toString() + "\r\n", CharsetUtil.UTF_8); } HttpResponse res = new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.UNAUTHORIZED, buf); res.headers().set(HttpHeaderNames.WWW_AUTHENTICATE, "Basic realm=\"" + Privilege.getAuthModuleName() + "\""); log.warn("{} {} {} [realm=\"{}\"]: {}", request.method(), request.uri(), res.status().code(), Privilege.getAuthModuleName(), reason); return ctx.writeAndFlush(res).addListener(ChannelFutureListener.CLOSE); } }