All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.krotscheck.kangaroo.authz.admin.v1.resource.OAuthTokenService Maven / Gradle / Ivy

There is a newer version: 1.4.0
Show newest version
/*
 * Copyright (c) 2017 Michael Krotscheck
 *
 * Licensed 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 net.krotscheck.kangaroo.authz.admin.v1.resource;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.Authorization;
import io.swagger.annotations.AuthorizationScope;
import net.krotscheck.kangaroo.authz.admin.Scope;
import net.krotscheck.kangaroo.authz.admin.v1.auth.ScopesAllowed;
import net.krotscheck.kangaroo.authz.common.database.entity.Application;
import net.krotscheck.kangaroo.authz.common.database.entity.Client;
import net.krotscheck.kangaroo.authz.common.database.entity.ClientType;
import net.krotscheck.kangaroo.authz.common.database.entity.OAuthToken;
import net.krotscheck.kangaroo.authz.common.database.entity.OAuthTokenType;
import net.krotscheck.kangaroo.authz.common.database.entity.User;
import net.krotscheck.kangaroo.authz.common.database.entity.UserIdentity;
import net.krotscheck.kangaroo.authz.common.database.util.SortUtil;
import net.krotscheck.kangaroo.authz.common.util.ValidationUtil;
import net.krotscheck.kangaroo.common.hibernate.id.IdUtil;
import net.krotscheck.kangaroo.common.hibernate.transaction.Transactional;
import net.krotscheck.kangaroo.common.response.ApiParam;
import net.krotscheck.kangaroo.common.response.ListResponseBuilder;
import net.krotscheck.kangaroo.common.response.SortOrder;
import org.apache.commons.lang.ObjectUtils;
import org.apache.lucene.search.Query;
import org.hibernate.Criteria;
import org.hibernate.Session;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Restrictions;
import org.hibernate.search.FullTextQuery;
import org.hibernate.search.query.dsl.BooleanJunction;
import org.hibernate.search.query.dsl.QueryBuilder;
import org.jvnet.hk2.annotations.Optional;

import javax.ws.rs.BadRequestException;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.math.BigInteger;
import java.net.URI;

/**
 * A RESTful api that permits management of OAuth Tokens that were issued by
 * the ancillary servlets.
 *
 * @author Michael Krotscheck
 */
@Path("/token")
@ScopesAllowed({Scope.TOKEN_ADMIN, Scope.TOKEN})
@Transactional
@Api(tags = "Token",
        authorizations = {
                @Authorization(value = "Kangaroo", scopes = {
                        @AuthorizationScope(
                                scope = Scope.TOKEN,
                                description = "Modify tokens in one"
                                        + " application."),
                        @AuthorizationScope(
                                scope = Scope.TOKEN_ADMIN,
                                description = "Modify tokens in all"
                                        + " applications.")
                })
        })
public final class OAuthTokenService extends AbstractService {

    /**
     * Search the tokens in the system.
     *
     * @param offset         The offset of the first entity to fetch.
     * @param limit          The number of entities to fetch.
     * @param queryString    The search term for the query.
     * @param ownerId        An optional owner ID to filter by.
     * @param userId         An optional user ID to filter by.
     * @param userIdentityId An optional identity ID to filter by.
     * @param clientId       An optional client ID to filter by.
     * @param type           An optional OAuth Token Type to filter by.
     * @return A list of search results.
     */
    @GET
    @Path("/search")
    @Produces(MediaType.APPLICATION_JSON)
    @ApiOperation(value = "Search tokens")
    @SuppressWarnings("CPD-START")
    public Response search(
            @DefaultValue("0") @QueryParam("offset") final Integer offset,
            @DefaultValue("10") @QueryParam("limit") final Integer limit,
            @DefaultValue("") @QueryParam("q") final String queryString,
            @io.swagger.annotations.ApiParam(type = "string")
            @Optional @QueryParam("owner") final BigInteger ownerId,
            @io.swagger.annotations.ApiParam(type = "string")
            @Optional @QueryParam("user") final BigInteger userId,
            @io.swagger.annotations.ApiParam(type = "string")
            @Optional @QueryParam("identity") final BigInteger userIdentityId,
            @io.swagger.annotations.ApiParam(type = "string")
            @Optional @QueryParam("client") final BigInteger clientId,
            @Optional @QueryParam("type") final OAuthTokenType type) {

        // Start a query builder...
        QueryBuilder builder = getSearchFactory()
                .buildQueryBuilder()
                .forEntity(OAuthToken.class)
                .get();
        BooleanJunction junction = builder.bool();

        Query fuzzy = builder.keyword()
                .fuzzy()
                .onFields(new String[]{
                        "identity.remoteId",
                        "identity.claims"
                })
                .matching(queryString)
                .createQuery();
        junction = junction.must(fuzzy);

        // Attach an ownership filter.
        User owner = resolveOwnershipFilter(ownerId);
        if (owner != null) {
            Query ownerQuery = builder
                    .keyword()
                    .onField("client.application.owner.id")
                    .matching(owner.getId())
                    .createQuery();
            junction.must(ownerQuery);
        }

        // Attach a user filter.
        User filterByUser = resolveFilterEntity(User.class, userId);
        if (filterByUser != null) {
            Query userQuery = builder
                    .keyword()
                    .onField("identity.user.id")
                    .matching(filterByUser.getId())
                    .createQuery();
            junction.must(userQuery);
        }

        // Attach an identity filter.
        UserIdentity filterByIdentity =
                resolveFilterEntity(UserIdentity.class, userIdentityId);
        if (filterByIdentity != null) {
            Query identityQuery = builder
                    .keyword()
                    .onField("identity.id")
                    .matching(filterByIdentity.getId())
                    .createQuery();
            junction.must(identityQuery);
        }

        // Attach a client filter.
        Client filterByClient =
                resolveFilterEntity(Client.class, clientId);
        if (filterByClient != null) {
            Query clientQuery = builder
                    .keyword()
                    .onField("client.id")
                    .matching(filterByClient.getId())
                    .createQuery();
            junction.must(clientQuery);
        }

        // Attach a type filter.
        if (type != null) {
            Query typeQuery = builder.keyword()
                    .onField("tokenType")
                    .matching(type)
                    .createQuery();
            junction.must(typeQuery);
        }

        FullTextQuery query = getFullTextSession()
                .createFullTextQuery(junction.createQuery(),
                        OAuthToken.class);

        return executeQuery(OAuthToken.class, query, offset, limit);
    }

    /**
     * Browse the identities in the system.
     *
     * @param offset         The offset of the first entity to fetch.
     * @param limit          The number of entities to fetch.
     * @param sort           The field on which the entities should be sorted.
     * @param order          The sort order, ASC or DESC.
     * @param ownerId        An optional owner ID to filter by.
     * @param userIdentityId An optional identity ID to filter by.
     * @param clientId       An optional client ID to filter by.
     * @return A list of search results.
     */
    @GET
    @Produces(MediaType.APPLICATION_JSON)
    @ApiOperation(value = "Browse tokens")
    public Response browse(
            @QueryParam(ApiParam.OFFSET_QUERY)
            @DefaultValue(ApiParam.OFFSET_DEFAULT) final int offset,
            @QueryParam(ApiParam.LIMIT_QUERY)
            @DefaultValue(ApiParam.LIMIT_DEFAULT) final int limit,
            @QueryParam(ApiParam.SORT_QUERY)
            @DefaultValue(ApiParam.SORT_DEFAULT) final String sort,
            @QueryParam(ApiParam.ORDER_QUERY)
            @DefaultValue(ApiParam.ORDER_DEFAULT) final SortOrder order,
            @io.swagger.annotations.ApiParam(type = "string")
            @Optional @QueryParam("owner") final BigInteger ownerId,
            @io.swagger.annotations.ApiParam(type = "string")
            @Optional @QueryParam("identity") final BigInteger userIdentityId,
            @io.swagger.annotations.ApiParam(type = "string")
            @Optional @QueryParam("client") final BigInteger clientId) {

        // Validate the incoming filters.
        User filterByOwner =
                resolveOwnershipFilter(ownerId);
        UserIdentity filterByIdentity =
                resolveFilterEntity(UserIdentity.class, userIdentityId);
        Client filterByClient =
                resolveFilterEntity(Client.class, clientId);

        // Assert that the sort is on a valid column
        Criteria countCriteria = getSession()
                .createCriteria(OAuthToken.class)
                .createAlias("client", "c")
                .setProjection(Projections.rowCount());

        Criteria browseCriteria = getSession()
                .createCriteria(OAuthToken.class)
                .setFirstResult(offset)
                .setMaxResults(limit)
                .createAlias("client", "c")
                .addOrder(SortUtil.order(order, sort));

        if (filterByClient != null) {
            browseCriteria
                    .add(Restrictions.eq("c.id", filterByClient.getId()));
            countCriteria
                    .add(Restrictions.eq("c.id", filterByClient.getId()));
        }

        if (filterByIdentity != null) {
            browseCriteria
                    .createAlias("identity", "i")
                    .add(Restrictions.eq("i.id", filterByIdentity.getId()));
            countCriteria
                    .createAlias("identity", "i")
                    .add(Restrictions.eq("i.id", filterByIdentity.getId()));
        }

        if (filterByOwner != null) {
            browseCriteria
                    .createAlias("c.application", "a")
                    .createAlias("a.owner", "o")
                    .add(Restrictions.eq("o.id", filterByOwner.getId()));
            countCriteria
                    .createAlias("c.application", "a")
                    .createAlias("a.owner", "o")
                    .add(Restrictions.eq("o.id", filterByOwner.getId()));
        }

        return ListResponseBuilder.builder()
                .offset(offset)
                .limit(limit)
                .order(order)
                .sort(sort)
                .total(countCriteria.uniqueResult())
                .addResult(browseCriteria.list())
                .build();
    }

    /**
     * Returns a specific entity.
     *
     * @param id The Unique Identifier for the entity.
     * @return A response with the entity that was requested.
     */
    @SuppressWarnings("CPD-END")
    @GET
    @Path("/{id: [a-f0-9]{32}}")
    @Produces(MediaType.APPLICATION_JSON)
    @ApiOperation(value = "Read token")
    public Response getResource(
            @io.swagger.annotations.ApiParam(type = "string")
            @PathParam("id") final BigInteger id) {
        OAuthToken token = getSession().get(OAuthToken.class, id);
        assertCanAccess(token, getAdminScope());
        return Response.ok(token).build();
    }

    /**
     * Create a new token.
     *
     * @param token The oauth token to create.
     * @return A redirect to the location where the identity was created.
     */
    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @ApiOperation(value = "Create token")
    public Response createResource(final OAuthToken token) {

        OAuthToken validToken = validateInputData(token);

        // Validate that we have no ID.
        if (validToken.getId() != null) {
            throw new BadRequestException();
        }

        // Assert that we can create a token in this application.
        Client parent = validToken.getClient();
        if (!getSecurityContext().isUserInRole(getAdminScope())) {
            Application scopeApp = parent.getApplication();
            if (getCurrentUser() == null
                    || !getCurrentUser().equals(scopeApp.getOwner())) {
                throw new BadRequestException();
            }
        }

        // Yay, we're valid! Save it.
        Session s = getSession();
        s.save(validToken);
        s.getTransaction().commit();

        // Build the URI of the new resources.
        URI resourceLocation = getUriInfo().getAbsolutePathBuilder()
                .path(OAuthTokenService.class, "getResource")
                .build(IdUtil.toString(validToken.getId()));

        return Response.created(resourceLocation).build();
    }


    /**
     * Update a token.
     *
     * @param id    The Unique Identifier for the token.
     * @param token The token to update.
     * @return A response with the token that was updated.
     */
    @PUT
    @Path("/{id: [a-f0-9]{32}}")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    @ApiOperation(value = "Update token")
    public Response updateResource(
            @io.swagger.annotations.ApiParam(type = "string")
            @PathParam("id") final BigInteger id,
            final OAuthToken token) {
        Session s = getSession();

        // Load the old instance.
        OAuthToken current = s.get(OAuthToken.class, id);

        // Make sure we can access this token.
        assertCanAccess(current, getAdminScope());

        // Make sure the body ID's match
        if (!current.equals(token)) {
            throw new BadRequestException();
        }

        // Make sure that we're not trying to change something we're not
        // permitted to change.
        if (!ObjectUtils.equals(current.getIdentity(), token.getIdentity())) {
            throw new BadRequestException();
        }
        if (!ObjectUtils.equals(current.getClient(), token.getClient())) {
            throw new BadRequestException();
        }
        if (!current.getTokenType().equals(token.getTokenType())) {
            throw new BadRequestException();
        }
        if (!ObjectUtils.equals(current.getAuthToken(), token.getAuthToken())) {
            throw new BadRequestException();
        }

        // Run all our other validations...
        OAuthToken validToken = validateInputData(token);

        // Copy over all the things we're allowed to edit.
        current.setExpiresIn(validToken.getExpiresIn());
        current.setRedirect(validToken.getRedirect());

        s.update(current);

        return Response.ok(current).build();
    }

    /**
     * Delete an scope.
     *
     * @param id The Unique Identifier for the scope.
     * @return A response that indicates the success of this operation.
     */
    @DELETE
    @Path("/{id: [a-f0-9]{32}}")
    @ApiOperation(value = "Delete token")
    public Response deleteResource(
            @io.swagger.annotations.ApiParam(type = "string")
            @PathParam("id") final BigInteger id) {
        Session s = getSession();
        OAuthToken token = s.get(OAuthToken.class, id);

        assertCanAccess(token, getAdminScope());

        // Let's hope they know what they're doing.
        s.delete(token);

        return Response.noContent().build();
    }

    /**
     * This method validates that an OAuthToken's referencing data is
     * entirely accurate, that all appropriate references are honored, and
     * that we're not trying to create a token with, say, an authenticator
     * from one application and a client from another.
     *
     * @param input The token to validate.
     * @return The same token, hydrated with validated data.
     */
    private OAuthToken validateInputData(final OAuthToken input) {
        // Validate that we have a body.
        if (input == null) {
            throw new BadRequestException();
        }

        // Validate that the token type is set.
        if (input.getTokenType() == null) {
            throw new BadRequestException();
        }

        // Validate that expiresIn is set and larger than zero.
        if (input.getExpiresIn() == null || input.getExpiresIn() < 1) {
            throw new BadRequestException();
        }

        // Validate that we have a valid client.
        Client client = requireEntityInput(Client.class, input.getClient());
        ClientType clientType = client.getType();
        input.setClient(client);

        // Validate that this particular client permits the creation of this
        // tokenType.
        if (clientType.equals(ClientType.OwnerCredentials)) {
            if (input.getTokenType().equals(OAuthTokenType.Authorization)) {
                throw new BadRequestException();
            }
        } else if (clientType.in(ClientType.ClientCredentials,
                ClientType.Implicit)
                && !input.getTokenType().equals(OAuthTokenType.Bearer)) {
            throw new BadRequestException();
        }

        // Assert that we have a valid identity.
        if (clientType.equals(ClientType.ClientCredentials)) {
            if (input.getIdentity() != null) {
                throw new BadRequestException();
            }
        } else {
            UserIdentity identity = requireEntityInput(UserIdentity.class,
                    input.getIdentity());
            if (!identity.getUser().getApplication()
                    .equals(client.getApplication())) {
                throw new BadRequestException();
            }
            input.setIdentity(identity);
        }

        // Validate that if we're creating a refresh token, it must be linked
        // to an auth token. Otherwise it must not be.
        if (!input.getTokenType().equals(OAuthTokenType.Refresh)) {
            if (input.getAuthToken() != null) {
                throw new BadRequestException();
            }
        } else {
            // Make sure we have a valid auth token.
            OAuthToken authToken = requireEntityInput(OAuthToken.class,
                    input.getAuthToken());

            // Make sure it's the correct type.
            if (!authToken.getTokenType().equals(OAuthTokenType.Bearer)) {
                throw new BadRequestException();
            }

            // Make sure it's for the correct user.
            if (!authToken.getIdentity().equals(input.getIdentity())) {
                throw new BadRequestException();
            }

            // It's valid, put it back.
            input.setAuthToken(authToken);
        }

        // Only authorization tokens use redirects.
        if (!input.getTokenType().equals(OAuthTokenType.Authorization)) {
            if (input.getRedirect() != null) {
                throw new BadRequestException();
            }
        } else {
            URI redirect = ValidationUtil.validateRedirect(input.getRedirect(),
                    client.getRedirects());
            if (redirect == null) {
                throw new BadRequestException();
            }
        }

        return input;
    }

    /**
     * Return the scope required to access ALL resources on this services.
     *
     * @return A string naming the scope.
     */
    @Override
    protected String getAdminScope() {
        return Scope.TOKEN_ADMIN;
    }

    /**
     * Return the scope required to access resources on this service.
     *
     * @return A string naming the scope.
     */
    @Override
    protected String getAccessScope() {
        return Scope.TOKEN;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy