Статьи

Deadbolt 2 за 10 минут — Scala Edition

Deadbolt 2 — это библиотека авторизации для Play 2, в которой есть API для приложений на базе Java и Scala. Это позволяет применять ограничения к действиям контроллера и настраивать рендеринг шаблонов на основе текущего пользователя.

Вот краткое, краткое руководство по началу работы с версией Scala.

зависимости

Добавьте зависимость в вашу сборку

"be.objectify" %% "deadbolt-scala" % "2.4.2"

Добавьте модуль Deadbolt в свое приложение Play

play {
    modules {
        enabled += be.objectify.deadbolt.scala.DeadboltModule
    }
}

Если вы уже запускаете свое приложение в активаторе, не забудьте перезагрузить проект.

Модель предметной области

Реализуйте Subject, Roleи Permissionчерты.

  • Subject представляет, как правило, пользователя
  • A Role— это одна системная привилегия, например ** admin **, ** user ** и т. Д. Субъект может иметь ноль или более ролей.
  • A Permissionможет использоваться с сопоставлением регулярного выражения, например, субъект с разрешением `printers.admin` может получить доступ к ресурсу, ограниченному для` printers. * `,` * .Admin` и т. Д. У субъекта может быть ноль или более разрешения.

Крючки

Реализуйте be.objectify.deadbolt.scala.DeadboltHandlerчерту. Эта реализация (или реализации — вы можете более одного) используется для

  • получить текущего пользователя — getSubject
  • запустить задачу предварительной авторизации, которая может заблокировать дальнейшее выполнение — beforeAuthCheck
  • обработать ошибку авторизации — onAuthFailure
  • обеспечить связь с динамическими типами ограничений — getDynamicResourceHandler

Вам нужно только реализовать, be.objectify.deadbolt.scala.DynamicResourceHandlerесли вы планируете использовать Dynamicили Pattern.CUSTOMограничения. Динамические ограничения — это тесты, реализованные полностью вашим кодом. Эта черта имеет две функции:

  • isAllowedиспользуется Dynamicограничением
  • checkPermissionиспользуется Patternограничением, когда тип шаблонаCUSTOM

Реализуйте be.objectify.deadbolt.scala.HandlerCacheчерту. Это используется Deadbolt для получения экземпляров DeadboltHandlers и имеет две концепции

  1. Обработчик по умолчанию. Вы всегда можете использовать определенный обработчик в шаблоне или контроллере, но если ничего не указано, будет использован известный экземпляр.
  2. Именованные обработчики.

Ниже приведен пример реализации на основе примера приложения.

@Singleton
class MyHandlerCache extends HandlerCache {
    val defaultHandler: DeadboltHandler = new MyDeadboltHandler

    // HandlerKeys is an user-defined object, containing instances of a case class that extends HandlerKey  
    val handlers: Map[Any, DeadboltHandler] = Map(HandlerKeys.defaultHandler -> defaultHandler,
                                                  HandlerKeys.altHandler -> new MyDeadboltHandler(Some(MyAlternativeDynamicResourceHandler)),
                                                  HandlerKeys.userlessHandler -> new MyUserlessDeadboltHandler)

    // Get the default handler.
    override def apply(): DeadboltHandler = defaultHandler

    // Get a named handler
    override def apply(handlerKey: HandlerKey): DeadboltHandler = handlers(handlerKey)
}

Наконец, выставьте своих обработчиков Deadbolt. Для этого вам нужно создать небольшой модуль, который связывает ваш кеш обработчика по типу…

package com.example.modules

import be.objectify.deadbolt.scala.cache.HandlerCache
import play.api.inject.{Binding, Module}
import play.api.{Configuration, Environment}
import com.example.security.MyHandlerCache

class CustomDeadboltHook extends Module {
    override def bindings(environment: Environment, configuration: Configuration): Seq[Binding[_]] = Seq(
        bind[HandlerCache].to[MyHandlerCache]
    )
}

… и добавьте его в ваше application.conf

play {
    modules {
        enabled += be.objectify.deadbolt.scala.DeadboltModule
        enabled += com.example.modules.CustomDeadboltHook
    }
}

Использование инъекции зависимостей во время компиляции

Если вы предпочитаете связывать все вместе с зависимостями времени компиляции, вам не нужно создавать пользовательский модуль или добавлять DeadboltModuleв него play.modules.

Вместо этого зависимости обрабатываются с использованием пользовательских ApplicationLoader. Чтобы упростить задачу, через эту be.objectify.deadbolt.scala.DeadboltComponentsчерту стали доступны различные компоненты Deadbolt . Вам все еще нужно будет предоставить несколько вещей, например, вашу HandlerCacheреализацию, и тогда вы получите доступ ко всем обычным элементам Deadbolt.

Вот пример ApplicationLoader для DI во время компиляции.

class CompileTimeDiApplicationLoader extends ApplicationLoader  {
  override def load(context: Context): Application 
             = new ApplicationComponents(context).application
}

class ApplicationComponents(context: Context) 
                            extends BuiltInComponentsFromContext(context) 
                            with DeadboltComponents
                            with EhCacheComponents {

  // Define a pattern cache implementation
  // defaultCacheApi is a component from EhCacheComponents
  override lazy val patternCache: PatternCache = new DefaultPatternCache(defaultCacheApi)

  // Declare something required by MyHandlerCache
  lazy val subjectDao: SubjectDao = new TestSubjectDao

  // Specify the DeadboltHandler implementation to use
  override lazy val handlers: HandlerCache = new MyHandlerCache(subjectDao) 

  // everything from here down is application-level
  // configuration, unrelated to Deadbolt, such as controllers, routers, etc
  // ...
}

Компоненты, предоставляемые Deadbolt:

  • scalaAnalyzer — логика ограничения
  • deadboltActions — для составления действий
  • actionBuilders — для строительных мероприятий
  • viewSupport — для шаблонных ограничений
  • patternCache— для кеширования регулярных выражений. Вы должны определить это самостоятельно в загрузчике приложения, но, как в примере выше, легко использовать реализацию по умолчанию
  • handlers— реализация того, HandlerCacheчто вы предоставляете
  • configuration — конфигурация приложения
  • ecContextProvider— контекст выполнения для параллельных операций. По умолчаниюscala.concurrent.ExecutionContext.global
  • templateFailureListenerProvider— для прослушивания ошибок, связанных с Deadbolt, которые возникают при рендеринге шаблонов. По умолчанию для реализации без операций

После того как вы определили свой ApplicationLoader, вы должны добавить его в свой application.conf.

play {
  application {
    loader=com.example.myapp.CompileTimeDiApplicationLoader
  }
}

Какой бы подход вы ни внедрили, теперь вы готовы обеспечить безопасный доступ к функциям контроллера и шаблонам в приложении Play 2.

Ограничения контроллера с помощью Action Builder

Используя ActionsBuildersкласс, вы можете быстро собрать ограничения вокруг ваших функций. Чтобы начать, введите ActionBuildersв свой контроллер.

class ExampleController @Inject() (actionBuilder: ActionBuilders) extends Controller

Теперь у вас есть конструкторы для всех типов ограничений, которые мы кратко рассмотрим через минуту. В следующих примерах я использую обработчик по умолчанию, т. .defaultHandler()Е. Также можно использовать другой обработчик с помощью .key(HandlerKey)или передать непосредственно обработчик .withHandler(DeadboltHandler).

SubjectPresent и SubjectNotPresent

Иногда вам не нужны детальные проверки — вам просто нужно посмотреть, присутствует ли пользователь (или нет).

// DeadboltHandler#getSubject must result in a Some for access to be granted
def someFunctionA = actionBuilder.SubjectPresentAction().defaultHandler() { Ok(accessOk()) }

// DeadboltHandler#getSubject must result in a None for access to be granted
def someFunctionB = actionBuilder.SubjectNotPresentAction().defaultHandler() { Ok(accessOk()) }

ограничивать

Это использует Subjects Roleдля выполнения проверок И / ИЛИ / НЕ. Значения, данные строителю, должны соответствовать Role.nameролям субъекта.

И определяется как Array[String](или, точнее String*, ИЛИ — это List[Array[String]], а НЕ — это имя-ролик с !предшествующим ему).

// subject must have the "foo" role 
def restrictedFunctionA = actionBuilder.RestrictAction("foo").defaultHandler() { Ok(accessOk()) }

// subject must have the "foo" AND "bar" roles 
def restrictedFunctionB = actionBuilder.RestrictAction("foo", "bar").defaultHandler() { Ok(accessOk()) }

// subject must have the "foo" OR "bar" roles 
def restrictedFunctionC = actionBuilder.RestrictAction(List(Array("foo"), Array("bar"))).defaultHandler() { Ok(accessOk()) }

Шаблон

При этом используется Subject«s Permissions для выполнения различных проверок.

// subject must have a permission with the exact value "admin.printer" 
def permittedFunctionA = actionBuilders.PatternAction("admin.printer", PatternType.EQUALITY).defaultHandler() { Ok(accessOk()) }

// subject must have a permission that matches the regular expression (without quotes) "(.)*\.printer" 
def permittedFunctionB = actionBuilders.PatternAction("(.)*\.printer", PatternType.REGEX).defaultHandler() { Ok(accessOk()) }

// the checkPermssion function of the current handler's DynamicResourceHandler will be used.  This is a user-defined test 
def permittedFunctionC = actionBuilders.PatternAction("something arbitrary", PatternType.CUSTOM).defaultHandler() { Ok(accessOk()) }

динамический

Наиболее гибкое ограничение — это полностью определяемое пользователем ограничение, которое используется DynamicResourceHandler#isAllowedдля определения доступа.


    def foo = actionBuilder.DynamicAction(name = "someClassifier").defaultHandler() { Ok(accessOk()) }

Ограничения контроллера с составом действия

Используя DeadboltActionsкласс, вы можете создавать ограниченные функции. Чтобы начать, введите DeadboltActionsв свой контроллер.


    class ExampleController @Inject() (deadbolt: DeadboltActions) extends Controller

Теперь у вас есть функции, эквивалентные функциям сборщиков, упомянутых выше. В следующих примерах я использую обработчик по умолчанию, т.е. обработчик не указан, но также можно использовать другой обработчик handler = <some handler, possibly from the handler cache>.

SubjectPresent и SubjectNotPresent

// DeadboltHandler#getSubject must result in a Some for access to be granted
def someFunctionA = deadbolt.SubjectPresent() {
    Action {
        Ok(accessOk())
    }
}

// DeadboltHandler#getSubject must result in a None for access to be granted
def someFunctionB = deadbolt.SubjectNotPresent() {
    Action {
        Ok(accessOk())
    }
}

ограничивать

// subject must have the "foo" role 
def restrictedFunctionA = deadbolt.Restrict(List(Array("foo")) {
    Action {
        Ok(accessOk())
    }
}

// subject must have the "foo" AND "bar" roles 
def restrictedFunctionB = deadbolt.Restrict(List(Array("foo", "bar")) {
    Action {
        Ok(accessOk())
    }
}

// subject must have the "foo" OR "bar" roles 
def restrictedFunctionC = deadbolt.Restrict(List(Array("foo"), Array("bar"))) {
    Action {
        Ok(accessOk())
    }
}

Шаблон

// subject must have a permission with the exact value "admin.printer" 
def permittedFunctionA = deadbolt.Pattern("admin.printer", PatternType.EQUALITY) {
    Action {
        Ok(accessOk())
    }
}

// subject must have a permission that matches the regular expression (without quotes) "(.)*\.printer" 
def permittedFunctionB = deadbolt.Pattern("(.)*\.printer", PatternType.REGEX) {
    Action {
        Ok(accessOk())
    }
}

// the checkPermssion function of the current handler's DynamicResourceHandler will be used.  This is a user-defined test in DynamicResourceHandler#checkPermission 
def permittedFunctionC = deadbolt.Pattern("something arbitrary", PatternType.CUSTOM) {
    Action {
        Ok(accessOk())
    }
}

динамический

Наиболее гибкое ограничение — это полностью определяемое пользователем ограничение, которое используется DynamicResourceHandler#isAllowedдля определения доступа.

def foo = deadbolt.Dynamic(name = "someClassifier") {
    Action {
        Ok(accessOk())
    }
}

Ограничения шаблона

Используя ограничения шаблонов, вы можете исключить создание частей шаблонов на стороне сервера. Это не манипулирование DOM на стороне клиента! Шаблонные ограничения имеют те же возможности, что и ограничения контроллера.

По умолчанию в шаблонных ограничениях используется стандартный обработчик Deadbolt, но, как и в случае с ограничениями контроллера, вы можете передать определенный обработчик. Самый простой способ сделать это — передать обработчик в шаблон, а затем передать его в ограничения. Еще одним преимуществом этого подхода является то, что вы можете передать упакованную версию обработчика, который будет кэшировать тему; если у вас много ограничений в шаблоне, это может дать значительный выигрыш.

Здесь важно отметить, что шаблоны блокируются, поэтому все используемые фьючерсы должны быть завершены, чтобы их можно было использовать в ограничениях на шаблоны. В результате каждое ограничение может принимать функцию, которая выражает Long, который является миллисекундным значением времени ожидания. По умолчанию это 1000 миллисекунд, но вы можете изменить это глобально, установив deadbolt.scala.view-timeoutзначение в вашем application.conf.

Каждое ограничение имеет вариант, который позволяет вам определять запасной контент. Это приходит в формате `<constraintName> Или`, например

@subjectPresentOr {
    this is protected
} {
    this will be shown if the constraint blocks the other content
}

SubjectPresent

@import be.objectify.deadbolt.scala.views.html.{subjectPresent, subjectPresentOr}

@subjectPresent() {
    This content will be present if handler#getSubject results in a Some 
}

@subjectPresentOr() {
    This content will be present if handler#getSubject results in a Some 
} {
    fallback content
}

SubjectNotPresent

@import be.objectify.deadbolt.scala.views.html.{subjectNotPresent, subjectNotPresentOr}

@subjectNotPresent() {
    This content will be present if handler#getSubject results in a None 
}

@subjectNotPresentOr() {
    This content will be present if handler#getSubject results in a None 
} {
    fallback content
}

ограничивать

@import be.objectify.deadbolt.scala.views.html.{restrict, restrictOr}

@restrict(roles = List(Array("foo", "bar"))) {
    Subject requires the foo role for this to be visible
}

@restrict(List(Array("foo", "bar")) {
     Subject requires the foo AND bar roles for this to be visible
}

@restrict(List(Array("foo"), Array("bar"))) {
     Subject requires the foo OR bar role for this to be visible
}

@restrictOr(List(Array("foo", "bar")) {
     Subject requires the foo AND bar roles for this to be visible
} {
    Subject does not have the necessary roles
}

Шаблон

Тип шаблона по умолчанию PatternType.EQUALITY.

@import be.objectify.deadbolt.scala.views.html.{pattern, patternOr}

@pattern("admin.printer") {
    Subject must have a permission with the exact value "admin.printer" for this to be visible
}

@pattern("(.)*\.printer", PatternType.REGEX) {
    Subject must have a permission that matches the regular expression (without quotes) "(.)*\.printer" for this to be visible
}

@pattern("something arbitrary", PatternType.CUSTOM) {
    DynamicResourceHandler#checkPermission must result in true for this to be visible
}

@patternOr("admin.printer") {
    Subject must have a permission with the exact value "admin.printer" for this to be visible
} {
    Subject did not have necessary permissions
}

динамический

@import be.objectify.deadbolt.scala.views.html.{dynamic, dynamicOr}

@dynamic("someName") {
    DynamicResourceHandler#isAllowed must result in true for this to be visible
}

@dynamicOr("someName") {
    DynamicResourceHandler#isAllowed must result in true for this to be visible
} {
    Custom test failed
}

Контекст выполнения

По умолчанию все фьючерсы выполняются в контексте `scala.concurrent.ExecutionContext.global`. Если вы хотите предоставить отдельный контекст выполнения, вы можете подключить его к Deadbolt, реализовав черту `DeadboltExecutionContextProvider`.

import be.objectify.deadbolt.scala.DeadboltExecutionContextProvider

class CustomDeadboltExecutionContextProvider extends DeadboltExecutionContextProvider {
    override def get(): ExecutionContext = ???
}

NB. Этот провайдер вызывается дважды, один раз DeadboltActionsи один раз ViewSupport.

После того, как вы внедрили провайдера, вам нужно объявить его в своем пользовательском модуле (см. Введение выше).

class CustomDeadboltHook extends Module {
    override def bindings(environment: Environment, configuration: Configuration): Seq[Binding[_]] = Seq(
        bind[HandlerCache].to[MyHandlerCache],
        bind[DeadboltExecutionContextProvider].to[CustomDeadboltExecutionContextProvider]
    )
}

Что дальше?

Если вы хотите изучить Deadbolt дальше, вы можете взглянуть на книгу, которую я сейчас о ней пишу. Вы можете найти его на https://leanpub.com/deadbolt-2 .