org.eclipse.hono.service.auth.impl.FileBasedAuthenticationService.java Source code

Java tutorial

Introduction

Here is the source code for org.eclipse.hono.service.auth.impl.FileBasedAuthenticationService.java

Source

/**
 * Copyright (c) 2016, 2017 Bosch Software Innovations GmbH.
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Bosch Software Innovations GmbH - initial creation
 */
package org.eclipse.hono.service.auth.impl;

import static java.nio.charset.StandardCharsets.UTF_8;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import org.eclipse.hono.auth.Activity;
import org.eclipse.hono.auth.Authorities;
import org.eclipse.hono.auth.AuthoritiesImpl;
import org.eclipse.hono.auth.HonoUser;
import org.eclipse.hono.service.auth.AbstractHonoAuthenticationService;
import org.eclipse.hono.service.auth.AuthTokenHelper;
import org.eclipse.hono.util.AuthenticationConstants;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Profile;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service;

import io.vertx.core.AsyncResult;
import io.vertx.core.Future;
import io.vertx.core.Handler;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;

/**
 * An authentication service based on authorities read from a JSON file.
 */
@Service
@Profile("authentication-impl")
public final class FileBasedAuthenticationService
        extends AbstractHonoAuthenticationService<AuthenticationServerConfigProperties> {

    private static final String FIELD_USERS = "users";
    private static final String FIELD_ROLES = "roles";
    private static final String FIELD_OPERATION = "operation";
    private static final String FIELD_RESOURCE = "resource";
    private static final String FIELD_ACTIVITIES = "activities";
    private static final String FIELD_AUTHORITIES = "authorities";
    private static final String FIELD_MECHANISM = "mechanism";

    private static final Map<String, Authorities> roles = new HashMap<>();
    private static final Map<String, JsonObject> users = new HashMap<>();
    private AuthTokenHelper tokenFactory;

    @Autowired
    @Override
    public void setConfig(final AuthenticationServerConfigProperties configuration) {
        setSpecificConfig(configuration);
    }

    /**
     * Sets the factory to use for creating tokens asserting a client's identity and authorities.
     * 
     * @param tokenFactory The factory.
     * @throws NullPointerException if factory is {@code null}.
     */
    @Autowired
    @Qualifier("signing")
    public void setTokenFactory(final AuthTokenHelper tokenFactory) {
        this.tokenFactory = Objects.requireNonNull(tokenFactory);
    }

    @Override
    protected void doStart(final Future<Void> startFuture) {
        if (tokenFactory == null) {
            startFuture.fail("token factory must be set");
        } else {
            try {
                loadPermissions();
                startFuture.complete();
            } catch (final IOException e) {
                log.error("cannot load permissions from resource {}", getConfig().getPermissionsPath(), e);
                startFuture.fail(e);
            }
        }
    }

    /**
     * Loads permissions from <em>permissionsPath</em>.
     * 
     * @throws IOException if the permissions cannot be read.
     * @throws IllegalStateException if no permissions resource path is set.
     */
    void loadPermissions() throws IOException {
        if (getConfig().getPermissionsPath() == null) {
            throw new IllegalStateException("permissions resource is not set");
        }
        if (getConfig().getPermissionsPath().isReadable()) {
            log.info("loading permissions from resource {}", getConfig().getPermissionsPath().getURI().toString());
            final StringBuilder json = new StringBuilder();
            load(getConfig().getPermissionsPath(), json);
            parsePermissions(new JsonObject(json.toString()));
        } else {
            throw new FileNotFoundException("permissions resource does not exist");
        }
    }

    private void load(final Resource source, final StringBuilder target) throws IOException {

        final char[] buffer = new char[4096];
        int bytesRead = 0;
        try (Reader reader = new InputStreamReader(source.getInputStream(), UTF_8)) {
            while ((bytesRead = reader.read(buffer)) > 0) {
                target.append(buffer, 0, bytesRead);
            }
        }
    }

    private void parsePermissions(final JsonObject permissionsObject) {

        Objects.requireNonNull(permissionsObject);
        parseRoles(permissionsObject.getJsonObject(FIELD_ROLES, new JsonObject()));
        parseUsers(permissionsObject.getJsonObject(FIELD_USERS, new JsonObject()));
    }

    private void parseRoles(final JsonObject rolesObject) {
        rolesObject.stream().filter(entry -> entry.getValue() instanceof JsonArray).forEach(entry -> {
            final String roleName = entry.getKey();
            final JsonArray authSpecs = (JsonArray) entry.getValue();
            log.debug("adding role [{}] with {} authorities", roleName, authSpecs.size());
            roles.put(roleName, toAuthorities(authSpecs));
        });
    }

    private void parseUsers(final JsonObject usersObject) {
        usersObject.stream().filter(entry -> entry.getValue() instanceof JsonObject).forEach(entry -> {
            final String authenticationId = entry.getKey();
            final JsonObject userSpec = (JsonObject) entry.getValue();
            log.debug("adding user [{}]", authenticationId);
            users.put(authenticationId, userSpec);
        });
    }

    private JsonObject getUser(final String authenticationId, final String mechanism) {
        final JsonObject result = users.get(authenticationId);
        if (result != null && mechanism.equals(result.getString(FIELD_MECHANISM))) {
            return result;
        } else {
            return null;
        }
    }

    private Authorities getAuthorities(final JsonObject user) {
        final AuthoritiesImpl result = new AuthoritiesImpl();
        user.getJsonArray(FIELD_AUTHORITIES).forEach(obj -> {
            final String authority = (String) obj;
            final Authorities roleAuthorities = roles.get(authority);
            if (roleAuthorities != null) {
                result.addAll(roleAuthorities);
            }
        });
        return result;
    }

    private Authorities toAuthorities(final JsonArray authorities) {

        final AuthoritiesImpl result = new AuthoritiesImpl();
        Objects.requireNonNull(authorities).stream().filter(obj -> obj instanceof JsonObject).forEach(obj -> {
            final JsonObject authSpec = (JsonObject) obj;
            final JsonArray activities = authSpec.getJsonArray(FIELD_ACTIVITIES, new JsonArray());
            final String resource = authSpec.getString(FIELD_RESOURCE);
            final String operation = authSpec.getString(FIELD_OPERATION);
            if (resource != null) {
                final List<Activity> activityList = new ArrayList<>();
                activities.forEach(s -> {
                    final Activity act = Activity.valueOf((String) s);
                    if (act != null) {
                        activityList.add(act);
                    }
                });
                result.addResource(resource, activityList.toArray(new Activity[activityList.size()]));
            } else if (operation != null) {
                final String[] parts = operation.split(":", 2);
                if (parts.length == 2) {
                    result.addOperation(parts[0], parts[1]);
                } else {
                    log.debug("ignoring malformed operation spec [{}], operation name missing", operation);
                }
            } else {
                throw new IllegalArgumentException("malformed authorities");
            }
        });
        return result;
    }

    private boolean hasAuthority(final JsonObject user, final String role) {
        return user.getJsonArray(FIELD_AUTHORITIES, new JsonArray()).contains(role);
    }

    private boolean isAuthorizedToImpersonate(final JsonObject user) {
        return hasAuthority(user, "hono-component");
    }

    @Override
    public void verifyPlain(final String authzid, final String username, final String password,
            final Handler<AsyncResult<HonoUser>> authenticationResultHandler) {

        if (username == null || username.isEmpty()) {
            authenticationResultHandler.handle(Future.failedFuture("missing username"));
        } else if (password == null || password.isEmpty()) {
            authenticationResultHandler.handle(Future.failedFuture("missing password"));
        } else {
            final JsonObject user = getUser(username, AuthenticationConstants.MECHANISM_PLAIN);
            if (user == null) {
                log.debug("no such user [{}]", username);
                authenticationResultHandler.handle(Future.failedFuture("unauthorized"));
            } else if (password.equals(user.getString("password"))) {
                verify(username, user, authzid, authenticationResultHandler);
            } else {
                log.debug("password mismatch");
                authenticationResultHandler.handle(Future.failedFuture("unauthorized"));
            }
        }
    }

    @Override
    public void verifyExternal(final String authzid, final String subjectDn,
            final Handler<AsyncResult<HonoUser>> authenticationResultHandler) {

        if (subjectDn == null || subjectDn.isEmpty()) {
            authenticationResultHandler.handle(Future.failedFuture("missing subject DN"));
        } else {
            final String commonName = AuthenticationConstants.getCommonName(subjectDn);
            if (commonName == null) {
                authenticationResultHandler
                        .handle(Future.failedFuture("could not determine authorization ID for subject DN"));
            } else {
                final JsonObject user = getUser(commonName, AuthenticationConstants.MECHANISM_EXTERNAL);
                if (user == null) {
                    authenticationResultHandler.handle(Future.failedFuture("unauthorized"));
                } else {
                    verify(commonName, user, authzid, authenticationResultHandler);
                }
            }
        }
    }

    private void verify(final String authenticationId, final JsonObject user, final String authorizationId,
            final Handler<AsyncResult<HonoUser>> authenticationResultHandler) {

        JsonObject effectiveUser = user;
        String effectiveAuthorizationId = authenticationId;
        if (authorizationId != null && !authorizationId.isEmpty() && isAuthorizedToImpersonate(user)) {
            final JsonObject impersonatedUser = users.get(authorizationId);
            if (impersonatedUser != null) {
                effectiveUser = impersonatedUser;
                effectiveAuthorizationId = authorizationId;
                log.debug("granting authorization id specified by client");
            } else {
                log.debug(
                        "no user found for authorization id provided by client, granting authentication id instead");
            }
        }
        final Authorities grantedAuthorities = getAuthorities(effectiveUser);
        final String grantedAuthorizationId = effectiveAuthorizationId;
        final Instant tokenExpirationTime = Instant.now().plus(tokenFactory.getTokenLifetime());
        final String token = tokenFactory.createToken(grantedAuthorizationId, grantedAuthorities);
        final HonoUser honoUser = new HonoUser() {

            @Override
            public String getName() {
                return grantedAuthorizationId;
            }

            @Override
            public String getToken() {
                return token;
            }

            @Override
            public Authorities getAuthorities() {
                return grantedAuthorities;
            }

            @Override
            public boolean isExpired() {
                return !Instant.now().isBefore(tokenExpirationTime);
            }

            @Override
            public Instant getExpirationTime() {
                return tokenExpirationTime;
            }
        };
        authenticationResultHandler.handle(Future.succeededFuture(honoUser));
    }
}