action_policy-graphql 0.0.1 → 0.1.0.rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 67e8da87c493d0478d778ba4421a404af3fd7b9f84cb95529f9f72a31c7221fb
4
- data.tar.gz: 19f45c885b52f2615d93872d67603e0b9ff07ae5ddb0a61805eeaa6fe4d4e024
3
+ metadata.gz: 3c8a905655156e38c918dc3b222dff1d8642d61416892b3ce89bf033adc08dcf
4
+ data.tar.gz: 4d9994c62eb097299b2e7199dd656cb64d859983b6da0778929ba86bc21f58b8
5
5
  SHA512:
6
- metadata.gz: dc240bd69108871bf60f7ef9b95fbd88565e14a3fdb21542e4330a8a5dbcbaf625ce9fe00cd0fb4caa24dd76e820c27a3975a5eb4862fbb95e130dcd0d1ff873
7
- data.tar.gz: e99ed4c366a6ae8877e0d37eb8bffbec71e6f2d6e0f0acc1bff93250b02b6146f77c89a453e409b2ecd7b2ea37418cf1966793d9f177f250392dacb2740ddd15
6
+ metadata.gz: bc7c087373d6ddf13d6f659a923ee786589d79861fb11f772104e4e5ae3a262d6160a55062340fda17c3729f3ece2461fdd25cc2f55412c2119e73374241cad1
7
+ data.tar.gz: 1ee3ad7f30ad9a37ed728fe8bde93d2405d0e57cc26aae78a204930e22abedd40d3b3381a061da7904eeb21f6af4023423627fa8533e7776d80b646fe5d77af6
data/README.md CHANGED
@@ -6,6 +6,11 @@
6
6
 
7
7
  This gem provides an integration for using [Action Policy](https://github.com/palkan/action_policy) as an authorization framework for GraphQL applications (built with [`graphql` ruby gem](https://graphql-ruby.org)).
8
8
 
9
+ This integration includes the following features:
10
+ - Fields & mutations authorization
11
+ - List and connections scoping
12
+ - **Exposing permissions/authorization rules in the API**.
13
+
9
14
  📑 [Documentation](https://actionpolicy.evilmartians.io/#/graphql)
10
15
 
11
16
  <a href="https://evilmartians.com/?utm_source=action_policy-graphql">
@@ -25,7 +30,106 @@ And then execute:
25
30
 
26
31
  ## Usage
27
32
 
28
- TBD
33
+ **NOTE:** this is a quick overview of the functionality provided by the gem. For more information see the [documentation](https://actionpolicy.evilmartians.io/#/graphql).
34
+
35
+ To start using Action Policy in GraphQL-related code, you need to enhance your base classes with `ActionPolicy::GraphQL::Behaviour`:
36
+
37
+ ```ruby
38
+ # For fields authorization, lists scoping and rules exposing
39
+ class Types::BaseObject < GraphQL::Schema::Object
40
+ include ActionPolicy::GraphQL::Behaviour
41
+ end
42
+
43
+ # For using authorization helpers in mutations
44
+ class Types::BaseMutation < GraphQL::Schema::Mutation
45
+ include ActionPolicy::GraphQL::Behaviour
46
+ end
47
+ ```
48
+
49
+ ### `authorize: *`
50
+
51
+ You can add authorization to the fields by specifying the `authorize: *` option:
52
+
53
+ ```
54
+ field :home, Home, null: false, authorize: true do
55
+ argument :id, ID, required: true
56
+ end
57
+
58
+ # field resolver method
59
+ def home(id:)
60
+ Home.find(id)
61
+ end
62
+ ```
63
+
64
+ The code above is equal to:
65
+
66
+ ```ruby
67
+ field :home, Home, null: false do
68
+ argument :id, ID, required: true
69
+ end
70
+
71
+ def home(id:)
72
+ Home.find(id).tap { |home| authorize! home, to: :show? }
73
+ end
74
+ ```
75
+
76
+ You can customize the authorization options, e.g. `authorize: {to: :preview?, with: CustomPolicy}`.
77
+
78
+ ### `authorized_scope`
79
+
80
+ You can add `authorized_scope: true` option to the field (list or _connection_ field) to
81
+ apply the corresponding policy rules to the data:
82
+
83
+ ```ruby
84
+ class CityType < ::Common::Graphql::Type
85
+ # It would automatically apply the relation scope from the EventPolicy to
86
+ # the relation (city.events)
87
+ field :events, EventType.connection_type, null: false, authorized_scope: true
88
+
89
+ # you can specify the policy explicitly
90
+ field :events, EventType.connection_type, null: false, authorized_scope: {with: CustomEventPolicy}
91
+ end
92
+ ```
93
+
94
+ ### `expose_authorization_rules`
95
+
96
+ You can add permissions/authorization exposing fields to "tell" clients which actions could be performed against the object or not (and why).
97
+
98
+ For example:
99
+
100
+ ```ruby
101
+ class ProfileType < ::Common::Graphql::Type
102
+ # Adds can_edit, can_destroy fields with
103
+ # AuthorizationResult type.
104
+
105
+ # NOTE: prefix "can_" is used by default, no need to specify it explicitly
106
+ expose_authorization_rules :edit?, :destroy?, prefix: "can_"
107
+ end
108
+ ```
109
+
110
+ Then the client could perform the following query:
111
+
112
+ ```gql
113
+ {
114
+ post(id: $id) {
115
+ canEdit {
116
+ # (bool) true|false; not null
117
+ value
118
+ # top-level decline message ("Not authorized" by default); null if value is true
119
+ message
120
+ # detailed information about the decline reasons; null if value is true
121
+ reasons {
122
+ details # JSON-encoded hash of the form { "community/event" => [:privacy_off?] }
123
+ fullMessages # Array of human-readable reasons
124
+ }
125
+ }
126
+
127
+ canDestroy {
128
+ # ...
129
+ }
130
+ }
131
+ }
132
+ ```
29
133
 
30
134
  ## Contributing
31
135
 
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionPolicy
4
+ module GraphQL
5
+ # Add `authorized` option to the field
6
+ #
7
+ # Example:
8
+ #
9
+ # class PostType < ::GraphQL::Schema::Object
10
+ # field :comments, null: false, authorized: true
11
+ #
12
+ # # or with options
13
+ # field :comments, null: false, authorized: { type: :relation, with: MyPostPolicy }
14
+ # end
15
+ module AuthorizedField
16
+ class AuthorizeExtension < ::GraphQL::Schema::FieldExtension
17
+ def initialize(*)
18
+ super
19
+ options[:to] ||= ::ActionPolicy::GraphQL.default_authorize_rule
20
+ options[:raise] = ::ActionPolicy::GraphQL.authorize_raise_exception unless options.key?(:raise)
21
+ end
22
+
23
+ def after_resolve(value:, context:, object:, **_rest)
24
+ return value if value.nil?
25
+
26
+ if options[:raise]
27
+ object.authorize! value, **options
28
+ value
29
+ else
30
+ object.allowed_to?(options[:to], value, options) ? value : nil
31
+ end
32
+ end
33
+ end
34
+
35
+ class ScopeExtension < ::GraphQL::Schema::FieldExtension
36
+ def after_resolve(value:, context:, object:, **_rest)
37
+ return value if value.nil?
38
+
39
+ object.authorized_scope(value, **options)
40
+ end
41
+ end
42
+
43
+ def initialize(*args, authorize: nil, authorized_scope: nil, **kwargs, &block)
44
+ if authorize && authorized_scope
45
+ raise ArgumentError, "Only one of `authorize` and `authorized_scope` " \
46
+ "options could be specified"
47
+ end
48
+
49
+ options = authorize || authorized_scope
50
+
51
+ if options
52
+ options = {} if options == true
53
+
54
+ extension_class = authorized_scope ? ScopeExtension : AuthorizeExtension
55
+
56
+ extension = {extension_class => options}
57
+
58
+ extensions = (kwargs[:extensions] ||= [])
59
+
60
+ if extensions.is_a?(Hash)
61
+ extensions.merge!(extension)
62
+ else
63
+ extensions << extension
64
+ end
65
+ end
66
+
67
+ super(*args, **kwargs, &block)
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_policy/graphql/fields"
4
+ require "action_policy/graphql/authorized_field"
5
+
6
+ module ActionPolicy
7
+ module GraphQL
8
+ module Behaviour
9
+ def self.included(base)
10
+ base.include ActionPolicy::Behaviour
11
+ base.include ActionPolicy::Behaviours::ThreadMemoized
12
+ base.include ActionPolicy::Behaviours::Memoized
13
+ base.include ActionPolicy::Behaviours::Namespaced
14
+
15
+ base.field_class.prepend(ActionPolicy::GraphQL::AuthorizedField)
16
+ base.authorize :user, through: :current_user
17
+
18
+ base.include ActionPolicy::GraphQL::Fields
19
+ end
20
+
21
+ def current_user
22
+ context[:current_user]
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_policy/graphql/types/authorization_result"
4
+
5
+ module ActionPolicy
6
+ module GraphQL
7
+ # Add DSL to add policy rules as fields
8
+ #
9
+ # Example:
10
+ #
11
+ # class PostType < ::GraphQL::Schema::Object
12
+ # # Adds can_edit, can_destroy fields with
13
+ # # AuthorizationResult type.
14
+ #
15
+ # expose_authorization_rules :edit?, :destroy?, prefix: "can_"
16
+ # end
17
+ #
18
+ # Prefix is "can_" by default.
19
+ module Fields
20
+ def self.included(base)
21
+ base.extend ClassMethods
22
+ end
23
+
24
+ def allowance_to(rule, target = object, **options)
25
+ policy_for(record: target, **options).yield_self do |policy|
26
+ policy.apply(authorization_rule_for(policy, rule))
27
+ policy.result
28
+ end
29
+ end
30
+
31
+ module ClassMethods
32
+ def expose_authorization_rules(*rules, prefix: ::ActionPolicy::GraphQL.default_authorization_field_prefix, **options)
33
+ rules.each do |rule|
34
+ field_name = "#{prefix}#{rule.to_s.delete("?")}"
35
+
36
+ field field_name,
37
+ ActionPolicy::GraphQL::Types::AuthorizationResult,
38
+ null: false
39
+
40
+ define_method(field_name) do
41
+ allowance_to(rule, options)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_policy/graphql/types/failure_reasons"
4
+
5
+ module ActionPolicy
6
+ module GraphQL
7
+ module Types
8
+ class AuthorizationResult < ::GraphQL::Schema::Object
9
+ field :value, Boolean, null: false, description: "Result of applying a policy rule"
10
+ field :message, String, null: true, description: "Human-readable error message"
11
+ field :reasons, FailureReasons, null: true, description: "Reasons of check failure"
12
+
13
+ def message
14
+ return if object.value == true
15
+ object.message
16
+ end
17
+
18
+ def reasons
19
+ return if object.value == true
20
+ object.reasons
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionPolicy
4
+ module GraphQL
5
+ module Types
6
+ class FailureReasons < ::GraphQL::Schema::Object
7
+ field :details, String, null: false, description: "JSON-encoded map of reasons"
8
+ field :full_messages, [String], null: false, description: "Human-readable errors"
9
+
10
+ def details
11
+ object.details.to_json
12
+ end
13
+ end
14
+ end
15
+ end
16
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module ActionPolicy
4
4
  module GraphQL
5
- VERSION = "0.0.1"
5
+ VERSION = "0.1.0.rc1"
6
6
  end
7
7
  end
@@ -1,11 +1,29 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "action_policy"
4
3
  require "graphql"
4
+ require "action_policy"
5
5
 
6
- require "action_policy/graphql/version"
6
+ require "action_policy/graphql/behaviour"
7
7
 
8
8
  module ActionPolicy
9
9
  module GraphQL
10
+ class << self
11
+ # Which rule to use when no specified (e.g. `authorize: true`)
12
+ # Defaults to `:show?`
13
+ attr_accessor :default_authorize_rule
14
+
15
+ # Whether to raise an exeption if field is not authorized
16
+ # or return `nil`.
17
+ # Defaults to `true`.
18
+ attr_accessor :authorize_raise_exception
19
+
20
+ # Which prefix to use for authorization fields
21
+ # Defaults to `"can_"`
22
+ attr_accessor :default_authorization_field_prefix
23
+ end
24
+
25
+ self.default_authorize_rule = :show?
26
+ self.authorize_raise_exception = true
27
+ self.default_authorization_field_prefix = "can_"
10
28
  end
11
29
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: action_policy-graphql
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0.rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vladimir Dementyev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-05-20 00:00:00.000000000 Z
11
+ date: 2019-05-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: action_policy
@@ -144,7 +144,6 @@ extensions: []
144
144
  extra_rdoc_files: []
145
145
  files:
146
146
  - ".gitignore"
147
- - ".rspec"
148
147
  - ".rubocop.yml"
149
148
  - ".travis.yml"
150
149
  - CHANGELOG.md
@@ -159,6 +158,11 @@ files:
159
158
  - gemfiles/jruby.gemfile
160
159
  - lib/action_policy-graphql.rb
161
160
  - lib/action_policy/graphql.rb
161
+ - lib/action_policy/graphql/authorized_field.rb
162
+ - lib/action_policy/graphql/behaviour.rb
163
+ - lib/action_policy/graphql/fields.rb
164
+ - lib/action_policy/graphql/types/authorization_result.rb
165
+ - lib/action_policy/graphql/types/failure_reasons.rb
162
166
  - lib/action_policy/graphql/version.rb
163
167
  homepage: https://github.com/palkan/action_policy-graphql
164
168
  licenses:
@@ -180,9 +184,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
180
184
  version: 2.4.0
181
185
  required_rubygems_version: !ruby/object:Gem::Requirement
182
186
  requirements:
183
- - - ">="
187
+ - - ">"
184
188
  - !ruby/object:Gem::Version
185
- version: '0'
189
+ version: 1.3.1
186
190
  requirements: []
187
191
  rubygems_version: 3.0.3
188
192
  signing_key:
data/.rspec DELETED
@@ -1,3 +0,0 @@
1
- --format documentation
2
- --color
3
- --require spec_helper