Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deny actor gate #618

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 29 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@

Flipper gives you control over who has access to features in your app.

* Enable or disable features for everyone, specific actors, groups of actors, a percentage of actors, or a percentage of time.
* Configure your feature flags from the console or a web UI.
* Regardless of what data store you are using, Flipper can performantly store your feature flags.
* Use [Flipper Cloud](#flipper-cloud) to cascade features from multiple environments, share settings with your team, control permissions, keep an audit history, and rollback.
- Enable or disable features for everyone, specific actors, groups of actors, a percentage of actors, or a percentage of time.
- Configure your feature flags from the console or a web UI.
- Regardless of what data store you are using, Flipper can performantly store your feature flags.
- Use [Flipper Cloud](#flipper-cloud) to cascade features from multiple environments, share settings with your team, control permissions, keep an audit history, and rollback.

Control your software — don't let it control you.

Expand Down Expand Up @@ -66,19 +66,29 @@ Flipper.enable_group :search, :admin
Flipper.enable_percentage_of_actors :search, 2
```

You also have the ability to explicitly deny a user for a feature. This takes precedence over enabling them.

```ruby
# Deny access to a feature for a specific actor
Flipper.deny_actor :search, current_user

# Reinstate access to that feature
Flipper.reinstate_actor :search, current_user
```

Read more about [getting started with Flipper](https://flippercloud.io/docs) and [enabling features](https://flippercloud.io/docs/features).

## Flipper Cloud

Like Flipper and want more? Check out [Flipper Cloud](https://www.flippercloud.io), which comes with:

* **everything in one place** — no need to bounce around from different application UIs or IRB consoles.
* **permissions** — grant access to everyone in your organization or lockdown each project to particular people.
* **multiple environments** — production, staging, enterprise, by continent, whatever you need.
* **personal environments** — no more rake scripts or manual enable/disable to get your laptop to look like production. Every developer gets a personal environment that inherits from production that they can override as they please ([read more](https://www.johnnunemaker.com/flipper-cloud-environments/)).
* **no maintenance** — we'll keep the lights on for you. We also have handy webhooks for keeping your app in sync with Cloud, so **our availability won't affect yours**. All your feature flag reads are local to your app.
* **audit history** — every feature change and who made it.
* **rollbacks** — enable or disable a feature accidentally? No problem. You can roll back to any point in the audit history with a single click.
- **everything in one place** — no need to bounce around from different application UIs or IRB consoles.
- **permissions** — grant access to everyone in your organization or lockdown each project to particular people.
- **multiple environments** — production, staging, enterprise, by continent, whatever you need.
- **personal environments** — no more rake scripts or manual enable/disable to get your laptop to look like production. Every developer gets a personal environment that inherits from production that they can override as they please ([read more](https://www.johnnunemaker.com/flipper-cloud-environments/)).
- **no maintenance** — we'll keep the lights on for you. We also have handy webhooks for keeping your app in sync with Cloud, so **our availability won't affect yours**. All your feature flag reads are local to your app.
- **audit history** — every feature change and who made it.
- **rollbacks** — enable or disable a feature accidentally? No problem. You can roll back to any point in the audit history with a single click.

[![Flipper Cloud Screenshot](docs/images/flipper_cloud.png)](https://www.flippercloud.io)

Expand All @@ -101,11 +111,11 @@ Cloud is super simple to integrate with Rails ([demo app](https://github.com/few

## Brought To You By

| pic | @mention | area |
|---|---|---|
| ![@jnunemaker](https://avatars3.githubusercontent.com/u/235?s=64) | [@jnunemaker](https://github.com/jnunemaker) | most things |
| ![@bkeepers](https://avatars3.githubusercontent.com/u/173?s=64) | [@bkeepers](https://github.com/bkeepers) | most things |
| ![@dpep](https://avatars3.githubusercontent.com/u/918804?s=64) | [@dpep](https://github.com/dpep) | tbd |
| ![@alexwheeler](https://avatars3.githubusercontent.com/u/3260042?s=64) | [@alexwheeler](https://github.com/alexwheeler) | api |
| ![@thetimbanks](https://avatars1.githubusercontent.com/u/471801?s=64) | [@thetimbanks](https://github.com/thetimbanks) | ui |
| ![@lazebny](https://avatars1.githubusercontent.com/u/6276766?s=64) | [@lazebny](https://github.com/lazebny) | docker |
| pic | @mention | area |
| ---------------------------------------------------------------------- | ---------------------------------------------- | ----------- |
| ![@jnunemaker](https://avatars3.githubusercontent.com/u/235?s=64) | [@jnunemaker](https://github.com/jnunemaker) | most things |
| ![@bkeepers](https://avatars3.githubusercontent.com/u/173?s=64) | [@bkeepers](https://github.com/bkeepers) | most things |
| ![@dpep](https://avatars3.githubusercontent.com/u/918804?s=64) | [@dpep](https://github.com/dpep) | tbd |
| ![@alexwheeler](https://avatars3.githubusercontent.com/u/3260042?s=64) | [@alexwheeler](https://github.com/alexwheeler) | api |
| ![@thetimbanks](https://avatars1.githubusercontent.com/u/471801?s=64) | [@thetimbanks](https://github.com/thetimbanks) | ui |
| ![@lazebny](https://avatars1.githubusercontent.com/u/6276766?s=64) | [@lazebny](https://github.com/lazebny) | docker |
1 change: 1 addition & 0 deletions lib/flipper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ def groups_registry=(registry)
require 'flipper/registry'
require 'flipper/type'
require 'flipper/types/actor'
require 'flipper/types/denied_actor'
require 'flipper/types/boolean'
require 'flipper/types/group'
require 'flipper/types/percentage'
Expand Down
1 change: 1 addition & 0 deletions lib/flipper/adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ def default_config
{
boolean: nil,
groups: Set.new,
denied_actors: Set.new,
actors: Set.new,
percentage_of_actors: nil,
percentage_of_time: nil,
Expand Down
3 changes: 2 additions & 1 deletion lib/flipper/adapters/rollout.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,9 @@ def get(feature)
boolean: boolean,
groups: groups,
actors: actors,
denied_actors: Set.new, # Rollout doesn't support this
percentage_of_actors: percentage_of_actors,
percentage_of_time: nil,
percentage_of_time: nil, # Rollout doesn't support this
}
end

Expand Down
1 change: 1 addition & 0 deletions lib/flipper/api/middleware.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ def initialize(app, options = {})
@action_collection.add Api::V1::Actions::PercentageOfTimeGate
@action_collection.add Api::V1::Actions::PercentageOfActorsGate
@action_collection.add Api::V1::Actions::ActorsGate
@action_collection.add Api::V1::Actions::DeniedActorsGate
@action_collection.add Api::V1::Actions::GroupsGate
@action_collection.add Api::V1::Actions::BooleanGate
@action_collection.add Api::V1::Actions::ClearFeature
Expand Down
44 changes: 44 additions & 0 deletions lib/flipper/api/v1/actions/denied_actors_gate.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
require 'flipper/api/action'
require 'flipper/api/v1/decorators/feature'

module Flipper
module Api
module V1
module Actions
class DeniedActorsGate < Api::Action
include FeatureNameFromRoute

route %r{\A/features/(?<feature_name>.*)/denied_actors/?\Z}

def post
ensure_valid_params
feature = flipper[feature_name]
actor = Actor.new(flipper_id)
feature.deny_actor(actor)
decorated_feature = Decorators::Feature.new(feature)
json_response(decorated_feature.as_json, 200)
end

def delete
ensure_valid_params
feature = flipper[feature_name]
actor = Actor.new(flipper_id)
feature.reinstate_actor(actor)
decorated_feature = Decorators::Feature.new(feature)
json_response(decorated_feature.as_json, 200)
end

private

def ensure_valid_params
json_error_response(:flipper_id_invalid) if flipper_id.nil?
end

def flipper_id
@flipper_id ||= params['flipper_id']
end
end
end
end
end
end
32 changes: 32 additions & 0 deletions lib/flipper/dsl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,28 @@ def disable_percentage_of_actors(name)
feature(name).disable_percentage_of_actors
end

# Public: Deny a feature for an actor.
#
# name - The String or Symbol name of the feature.
# actor - a Flipper::Types::DeniedActor instance or an object that responds
# to flipper_id.
#
# Returns result of Feature#deny_actor.
def deny_actor(name, actor)
feature(name).deny_actor(actor)
end

# Public: Reinstate a feature for an actor.
#
# name - The String or Symbol name of the feature.
# actor - a Flipper::Types::DeniedActor instance or an object that responds
# to flipper_id.
#
# Returns result of Feature#reinstate_actor.
def reinstate_actor(name, actor)
feature(name).reinstate_actor(actor)
end

# Public: Add a feature.
#
# name - The String or Symbol name of the feature.
Expand Down Expand Up @@ -245,6 +267,16 @@ def actor(thing)
Types::Actor.new(thing)
end

# Public: Wraps an object as a flipper denied actor.
#
# thing - The object that you would like to wrap.
#
# Returns an instance of Flipper::Types::DeniedActor.
# Raises ArgumentError if thing does not respond to `flipper_id`.
def denied_actor(thing)
Types::DeniedActor.new(thing)
end

# Public: Shortcut for getting a percentage of time instance.
#
# number - The percentage of time that should be enabled.
Expand Down
98 changes: 95 additions & 3 deletions lib/flipper/feature.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,40 @@ def disable(thing = false)
end
end

# Public: Deny this feature for something.
# Works like an enable under the hood.
#
# Returns the result of Adapter#enable.
def deny(thing)
instrument(:deny) do |payload|
adapter.add self

gate = deniable_gate_for(thing)
wrapped_thing = gate.wrap(thing)
payload[:gate_name] = gate.name
payload[:thing] = wrapped_thing

adapter.enable self, gate, wrapped_thing
end
end

# Public: Reinstate this disabled feature for something.
# Works like a disable under the hood.
#
# Returns the result of Adapter#disable.
def reinstate(thing)
instrument(:reinstate) do |payload|
adapter.add self

gate = deniable_gate_for(thing)
wrapped_thing = gate.wrap(thing)
payload[:gate_name] = gate.name
payload[:thing] = wrapped_thing

adapter.disable self, gate, wrapped_thing
end
end

# Public: Adds this feature.
#
# Returns the result of Adapter#add.
Expand Down Expand Up @@ -110,7 +144,13 @@ def enabled?(thing = nil)
thing: thing
)

if open_gate = gates.detect { |gate| gate.open?(context) }
# We first check if we have any deniable gate activated here,
# because they have preference over the other gates
if closed_deny_gate = deniable_gates.detect { |gate| !gate.open?(context) }
return false
end

if open_gate = forward_gates.detect { |gate| gate.open?(context) }
payload[:gate_name] = open_gate.name
true
else
Expand Down Expand Up @@ -199,6 +239,26 @@ def disable_percentage_of_actors
disable Types::PercentageOfActors.new(0)
end

# Public: Denies a feature for an actor.
#
# actor - a Flipper::Types::Actor instance or an object that responds
# to flipper_id.
#
# Returns result of deny.
def deny_actor(actor)
deny Types::DeniedActor.wrap(actor)
end

# Public: Reinstates a denied feature for an actor.
#
# actor - a Flipper::Types::Actor instance or an object that responds
# to flipper_id.
#
# Returns result of reinstate.
def reinstate_actor(actor)
reinstate Types::DeniedActor.wrap(actor)
end

# Public: Returns state for feature (:on, :off, or :conditional).
def state
values = gate_values
Expand Down Expand Up @@ -264,6 +324,13 @@ def actors_value
gate_values.actors
end

# Public: Get the adapter value for the denied actors gate.
#
# Returns Set of String flipper_id's.
def denied_actors_value
gate_values.denied_actors
end

# Public: Get the adapter value for the boolean gate.
#
# Returns true or false.
Expand Down Expand Up @@ -342,27 +409,52 @@ def gates
@gates ||= [
Gates::Boolean.new,
Gates::Actor.new,
Gates::DeniedActor.new,
Gates::PercentageOfActors.new,
Gates::PercentageOfTime.new,
Gates::Group.new,
]
end

# Public: Get all the non deniable gates used to determine enabled/disabled for the feature.
#
# Returns an array of gates
def forward_gates
@forward_gates ||= gates.reject(&:deniable?)
end

# Public: Get all the deniable gates used to determine enabled/disabled for the feature.
#
# Returns an array of gates
def deniable_gates
@deniable_gates ||= gates.select(&:deniable?)
end

# Public: Find a gate by name.
#
# Returns a Flipper::Gate if found, nil if not.
def gate(name)
gates.detect { |gate| gate.name == name.to_sym }
end

# Public: Find the gate that protects a thing.
# Public: Find the forward gate that protects a thing.
#
# thing - The object for which you would like to find a gate
#
# Returns a Flipper::Gate.
# Raises Flipper::GateNotFound if no gate found for thing
def gate_for(thing)
gates.detect { |gate| gate.protects?(thing) } || raise(GateNotFound, thing)
forward_gates.detect { |gate| gate.protects?(thing) } || raise(GateNotFound, thing)
end

# Public: Find the deniable gate that protects a thing.
#
# thing - The object for which you would like to find a gate
#
# Returns a Flipper::Gate.
# Raises Flipper::GateNotFound if no gate found for thing
def deniable_gate_for(thing)
deniable_gates.detect { |gate| gate.protects?(thing) } || raise(GateNotFound, thing)
end

private
Expand Down
5 changes: 5 additions & 0 deletions lib/flipper/feature_check_context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ def actors_value
values.actors
end

# Public: Convenience method for denied actors value value like Feature has.
def denied_actors_value
values.denied_actors
end

# Public: Convenience method for boolean value value like Feature has.
def boolean_value
values.boolean
Expand Down
8 changes: 8 additions & 0 deletions lib/flipper/gate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ def protects?(_thing)
false
end

# Internal: Check if a gate is responsible for denying access for a thing. Implemented in subclass.
#
# Returns true if gate denies, false if not.
def deniable?
false
end

# Internal: Allows gate to wrap thing using one of the supported flipper
# types so adapters always get something that responds to value.
def wrap(thing)
Expand All @@ -55,6 +62,7 @@ def inspect
end

require 'flipper/gates/actor'
require 'flipper/gates/denied_actor'
require 'flipper/gates/boolean'
require 'flipper/gates/group'
require 'flipper/gates/percentage_of_actors'
Expand Down
Loading