Estratégias de autorização da API REST

8

Há muitas perguntas aqui que tratam da mecânica de autenticação e autorização de APIs RESTful, mas nenhuma delas parece entrar em detalhes de como implementar serviços seguros no nível do aplicativo.

Por exemplo, digamos que meu aplicativo da web (eu tenho Java em mente, mas isso se aplica a qualquer back-end realmente) tem um sistema de autenticação seguro que permite que os usuários da API efetuem login com um nome de usuário e senha. Quando o usuário faz uma solicitação, a qualquer momento durante o pipeline de processamento de solicitações, posso chamar um getAuthenticatedUser()método que retornará o usuário nulo se o usuário não estiver conectado ou um objeto de domínio do Usuário que represente o usuário conectado.

A API permite que usuários autenticados acessem seus dados, por exemplo, um GET /api/orders/retornará a lista de pedidos desse usuário. Da mesma forma, um GET /api/tasks/{task_id}retornará dados relacionados a essa tarefa específica.

Vamos supor que existem vários objetos de domínio diferentes que podem ser associados à conta de um usuário (pedidos e tarefas são dois exemplos, também podemos ter clientes, faturas etc.). Queremos que apenas usuários autenticados possam acessar dados sobre seus próprios objetos, portanto, quando um usuário faz uma ligação /api/invoices/{invoice_id}, precisamos verificar se o usuário está autorizado a acessar esse recurso antes de servi-lo.

Minha pergunta é: existem padrões ou estratégias para lidar com esse problema de autorização? Uma opção que estou considerando é criar uma interface auxiliar (ou seja SecurityUtils.isUserAuthorized(user, object)), que pode ser chamada durante o processamento de solicitações para garantir que o usuário esteja autorizado a buscar o objeto. Isso não é ideal, pois polui o código do terminal do aplicativo com muitas dessas chamadas, por exemplo

Object someEndpoint(int objectId) {
    if (!SecurityUtils.isUserAuthorized(loggedInUser, objectDAO.get(objectId)) {
        throw new UnauthorizedException();
    }
    ...
}

... e depois há a questão de implementar esse método para cada tipo de domínio que pode ser um pouco trabalhoso. Essa pode ser a única opção, mas eu gostaria de ouvir suas sugestões!

HJCee
fonte
Quando você diz que os usuários "efetuam login", você quer dizer que está mantendo uma sessão?
JimmyJames

Respostas:

8

Por favor, pelo amor de Deus, não crie uma SecurityUtilsclasse!

Sua classe se tornará 10 mil linhas de código espaguete em questão de meses! Você precisaria passar um Actiontipo (criar, ler, atualizar, destruir, listar, etc.) em seu isUserAuthorized()método, que rapidamente se tornaria uma switchdeclaração de mil linhas com uma lógica cada vez mais complexa que seria difícil de testar na unidade. Não faça isso.


Geralmente, o que faço, pelo menos no Ruby on Rails, é que cada objeto de domínio seja responsável por seus próprios privilégios de acesso, tendo uma classe de política para cada modelo . Em seguida, o controlador pergunta à classe de política se o usuário atual da solicitação tem acesso ao recurso ou não. Aqui está um exemplo no Ruby, já que eu nunca implementei algo assim em Java, mas a ideia deve aparecer de maneira limpa:

class OrderPolicy

    class Scope < Struct.new(:user, :scope)

        def resolve

            # A user must be logged in to interact with this resource at all
            raise NotAuthorizedException unless user

            # Admin/moderator can see all orders
            if (user.admin? || user.moderator?)
                scope.all
            else
                # Only allow the user to see their own orders
                scope.where(orderer_id: user.id)
            end
        end
    end

    # Constructor, if you don't know Ruby
    def initialize(user, order)
        raise NotAuthorizedException unless user
        @user = user
        @order= order
    end

    # Whitelist what data can be manipulated by each type of user
    def valid_attributes
        if @user.admin?
            [:probably, :want, :to, :let, :admin, :update, :everything]
        elsif @user.moderator?
            [:fewer, :attributes, :but, :still, :most]
        else
            [:regualar, :user, :attributes]
        end
    end

    # Maybe restrict updatable attributes further
    def valid_update_attributes
    end

    # Who can create new orders
    def create?
        true # anyone, and they would have been authenticated already by #initialize
    end

    # Read operation
    def show?
        @user.admin? || @user.moderator? || owns_order
    end

    # Only superusers can update resources
    def update?
        @user.admin? || @user.moderator?
    end

    # Only admins can delete, because it's extremely destructive or whatever
    def destroy?
        @user.admin?
    end

    private

    # A user 'owns' an order if they were the person who submitted the order
    # E.g. superusers can access the order, but they didn't create it
    def owns_order
        @order.orderer_id == @user.id
    end
end

Mesmo se você tiver recursos aninhados complexos, algum recurso deve 'possuir' os recursos aninhados, para que a lógica de nível superior seja inerentemente reduzida. No entanto, esses recursos aninhados precisam de suas próprias classes de política, caso possam ser atualizados independentemente do recurso 'pai'.

Na minha inscrição, que é para o departamento da minha universidade, tudo gira em torno do Courseobjeto. Não é um Useraplicativo centralizado. No entanto, Users estão matriculados Course, para que eu possa simplesmente garantir que:

@course.users.include? current_user && (whatever_other_logic_I_need)

para qualquer recurso que um particular Userprecise modificar, uma vez que quase todos os recursos estão vinculados a a Course. Isso é feito na classe de política no owns_whatevermétodo

Não fiz muita arquitetura Java, mas parece que você pode criar uma Policyinterface, na qual os diferentes recursos que precisam ser autenticados devem implementar a interface. Então, você tem todos os métodos necessários que podem se tornar tão complexos quanto você precisa que sejam por objeto de domínio . O importante é amarrar a lógica ao modelo em si, mas, ao mesmo tempo, mantê-la em uma classe separada (princípio de responsabilidade única (SRP)).

Suas ações do controlador podem ser algo como:

public List<Order> index(OrderQuery query) {

    authorize(Order.class)
    // you should be auto-rescuing the NotAuthorizedException thrown by
    //the policy class at the controller level (or application level)

    // if the authorization didn't fail/rescue from exception, just render the resource
    List<Order> orders = db.search(query);
    return renderJSON(orders);
}

public Order show(int orderId) {

    authorize(Order.class)
    Order order = db.find(orderId);
    return renderJSON(order);
}
Chris Cirefice
fonte
1

Uma solução mais conveniente é usar anotações para marcar métodos que requerem alguma forma de autorização. Isso se destaca do seu código comercial e pode ser tratado pelo Spring Security ou pelo código AOP personalizado. Se você usar essas anotações nos seus métodos de negócios, e não nos pontos de extremidade, poderá obter uma exceção quando um usuário não autorizado tentar chamá-los, independentemente do ponto de entrada.

Michał Kosmulski
fonte
Não é o que estou perguntando. As anotações do Spring permitem garantir que o usuário tenha um nível de autorização específico (por exemplo, que o usuário é um administrador), mas não acredito que eles ajudem a restringir o acesso a entidades específicas. Digamos que a lógica comercial busque e retorne uma fatura com base em um ID. Faço login e forneço o ID da fatura de outra pessoa ao terminal. Não acredito que as anotações do Spring impeçam o acesso horizontal desse formulário?
HJCee
1
As anotações AH do @HJCee Spring Securities são bastante expressivas . Você pode definir uma @PreAuthorization("hasRole('ADMIN') and #requestingUser.company.uuid == authentication.details.companyUuid")anotação em que o #requestingUsersegmento faça referência a um objeto colado com o fieldName requestingUserque possui um método getCompany()cujo objeto retornado possui outro método getUuid(). O authenticationrefere-se ao Authenticationobjeto armazenado no contexto de segurança.
Roman Vottner
1
@RomanVottner O que acontece se você precisar de uma autorização realmente complexa? por exemplo, apenas moderadores no Stack Exchange com emblemas dourados X na tag Y podem fazer edições para excluir perguntas (ou o que for). Vou passar a anotação de linha única de 300 caracteres.
Chris Cirefice
1
@ChrisCirefice Use @PreAuthorize("hasPermission(#user, 'allowDoSomething')")e implemente seu avaliador de permissão personalizado ou escreva um manipulador de expressão personalizado e raiz . Se você quiser alterar o comportamento de anotações disponíveis hava uma olhada esta discussão
Roman Vottner
0

Use segurança baseada em capacidade.

Uma capacidade é um objeto imperdoável que atua como evidência de que se pode executar uma determinada ação. Nesse caso:

  • Faça com que cada função (conjunto de ações permitidas) seja uma interface.
  • Faça com que as operações que exigem autenticação sejam métodos em suas respectivas interfaces. Isso deve gerar uma exceção se o destinatário não for o usuário atual da solicitação, se possível.

Isso torna impossível tentar fazer algo que o usuário atual não está autorizado a fazer.

Dessa forma, é impossível

Demi
fonte
1
Não deve ser o crítico, mas isso não é um TL; DR da minha resposta? Se sim, seria preferível simplesmente comentar sobre a minha resposta em vez de escrever seu próprio :)
Chris Cirefice
Não é bem assim. A idéia aqui é expressar as diferentes funções que os usuários podem ter no sistema de tipos Java, de modo que você não possa chamar um método em um usuário que exija um privilégio que o usuário não possui.
Demi
Além do comentário de Chris, minha pergunta não é sobre restrição de acesso baseada em função (que é trivial de implementar com qualquer boa estrutura da Web), mas sobre restrições de acesso baseadas em associações entre usuários e dados ('é o objeto X pertencente ao usuário Y' é um exemplo realmente simples dessa associação, mas eles podem ser muito complexos). É sobre esse problema que estou realmente tentando obter conselhos.
HJCee