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

Optional Feature: require admin for prod (un)lock. #2492

Merged
merged 3 commits into from
Jan 31, 2018

Conversation

jgoerz
Copy link
Contributor

@jgoerz jgoerz commented Jan 24, 2018

  • When set, will require admin role for (un)locking stages with production attribute set.
  • All other locking is unchanged.

/cc @zendesk/samson

Tasks

  • 👍 from team

References

  • Jira link:

Risks

  • Level: Low/Med/High

@jgoerz jgoerz force-pushed the only-admin-locks-unlocks-production branch 2 times, most recently from b7eb192 to c487df7 Compare January 24, 2018 21:43
@jgoerz
Copy link
Contributor Author

jgoerz commented Jan 24, 2018

Build failure is unrelated to the changes for this pull request. Fails on slow test (didn't see this when I ran it locally)

Error:
JobExecution::builds_in_environment#test_0001_makes builds available via env:
Maxitest::Timeout::TestCaseTimeout: Test took too long to finish, aborting. To use a debugger, set Maxitest.timeout = false at the top of the test file.

And coverage:

app/models/event_streamer.rb has less uncovered lines (2 current vs 3 configured), decrement configured uncovered?

@grosser
Copy link
Contributor

grosser commented Jan 25, 2018

hmmm I kinda like the idea of only letting admins unlock ... do you think the same restriction is necessary for locking ? ... I'd rather have anyone able to lock when something goes wrong ...

.env.example Outdated
@@ -173,3 +173,6 @@ PERIODICAL=stop_expired_deploys:60,remove_expired_locks:60,report_system_stats:6
# GCLOUD_OPTIONS=--verbose # optional, options
# GCLOUD_PROJECT=foo-123
# GCLOUD_ACCOUNT=my-account

## Feature: Only admins can (un)lock stages which affect production.
# FEATURE_ONLY_ADMINS_CAN_LOCK_UNLOCK_PRODUCTION_STAGE=1
Copy link
Contributor

@grosser grosser Jan 25, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ONLY_ADMINS_CAN_UNLOCK_PRODUCTION_STAGE should be long enough :D

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... maybe PRODUCTION_STAGE_LOCK_REQUIRES_ADMIN

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, naming things is always hard ;-) I like PRODUCTION_STAGE_LOCK_REQUIRES_ADMIN.

@@ -60,11 +60,25 @@ def lock
def require_project
case action_name
when 'create' then
@project = Stage.find(params[:lock][:resource_id]).project
stage = Stage.find(params[:lock][:resource_id])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should only be handles by access_control.rb and not copied here ...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with this, but it seems that only the project object is passed into access control to make the decision for the locks. I struggled with my debugger configuration, so maybe I overlooked something... but this is the reason for the hacking singleton method below. I was basically trying to attach the stage attribute information to the project. I'll do some more investigating...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I have a question about this:

# app/controllers/locks_controller.rb line 6
 before_action :require_project, if: :for_stage_lock?

I was going to originally change this to

 before_action :require_stage, if: :for_stage_lock?

Is there some reason that could not be done? Are there other use cases that use a Project to determine lockability? It seems the locks apply to 2 things, global, and Stages. Why is it that we pass a Project in for the access check?

when 'destroy' then
@project = lock.resource.project
if ENV['FEATURE_ONLY_ADMINS_CAN_LOCK_UNLOCK_PRODUCTION_STAGE'] && lock.resource.production
@project.define_singleton_method(:production) { true }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks very wrong :D

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, see my other comment above.

@grosser
Copy link
Contributor

grosser commented Jan 25, 2018 via email

@jgoerz
Copy link
Contributor Author

jgoerz commented Jan 25, 2018

The below is going to fail on the "locks" current_user test (line 245: "renders when authorized via the project"). I'm a little confused by this test. It seems to be testing if a project can be viewed by checking project level authorization... but a lock is only accessed after viewing the project/stage so I don't believe this is a valid test for the locks controller. Let me know what you think.

@jgoerz
Copy link
Contributor Author

jgoerz commented Jan 26, 2018

grosser commented:
hmmm I kinda like the idea of only letting admins unlock ... do you think the same restriction is necessary for locking ? ... I'd rather have anyone able to lock when something goes wrong ...

For our use case, we just want to prevent accidental production deploys. We plan on locking production stages with non-expiring locks. So letting anyone lock wouldn't be a problem for us. I could see how someone would want to strictly control both (un)lock. The only thing I could see a problem with, is that after someone locked it, because of the role/perms, the Unlock button would be grayed out or not displayed at all... which might be confusing if someone was just pushing buttons (exploring/learning).

If it is implemented... maybe split the lock actions into

  1. :read
  2. :create_or_update (or maybe just :lock)
  3. :destroy or :unlock

If I had a choice, I'd choose to leave it as is (less work for me :-P ). But I'm flexible.

@jgoerz jgoerz force-pushed the only-admin-locks-unlocks-production branch from 649f8e3 to 22aeacf Compare January 26, 2018 19:43
protected

def require_stage
@stage = (Stage.find_by_param!(params[:stage_id]) if params[:stage_id])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this won't work since stage permalinks are not globally unique, need to find base on project

@@ -66,7 +66,12 @@ def resource_action
end

def can?(action, resource_namespace, scope = nil)
scope ||= current_project if respond_to?(:current_project)
case resource_namespace
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should be scope ||= to avoid duplication and to avoid the respond_to? calls

user.admin? # global locks
elsif ENV['PRODUCTION_STAGE_LOCK_REQUIRES_ADMIN'] && scope.production
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might be easier to read as:

else # stage locks
  if PRODUCTION_STAGE_LOCK_REQUIRES_ADMIN
  else
  end
end

perform_get(project_id: projects(:test).id)
# This still authorizes based on Project, but for the lock controller, it needs a Stage object
# passed to access control since locks are per Stage. See app/models/access_control.rb lines
# 22 - 28.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lines change ... prefer names :)

@jgoerz jgoerz force-pushed the only-admin-locks-unlocks-production branch from 22aeacf to 31bc90b Compare January 29, 2018 22:56
@jgoerz
Copy link
Contributor Author

jgoerz commented Jan 30, 2018

Ready for review/feedback.

- When set, will require admin role for (un)locking stages with production attribute set.
- All other locking is unchanged.
@jgoerz jgoerz force-pushed the only-admin-locks-unlocks-production branch from c9c8982 to 183e702 Compare January 30, 2018 15:36
@@ -66,7 +66,11 @@ def resource_action
end

def can?(action, resource_namespace, scope = nil)
scope ||= current_project if respond_to?(:current_project)
scope ||= if resource_namespace == :locks
try(:current_stage)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

try does not make sense here, try is for nil prevention like nil.try(:foo?)


if scope.nil? # global locks
user.admin?
elsif ENV['PRODUCTION_STAGE_LOCK_REQUIRES_ADMIN'] && scope.production
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prefer to guard this in a elsif scope.is_a?(Stage)so future additions don't require a refactor

@@ -118,6 +119,33 @@ def create_lock(resource = nil, options = {})
end
end

describe '#create with PRODUCTION_STAGE_LOCK_REQUIRES_ADMIN' do
with_env 'PRODUCTION_STAGE_LOCK_REQUIRES_ADMIN' => 'true'
before { travel_to Time.now }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove the before and after ... they are no necessary ?

@@ -118,6 +119,33 @@ def create_lock(resource = nil, options = {})
end
end

describe '#create with PRODUCTION_STAGE_LOCK_REQUIRES_ADMIN' do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if it's not too complicated I'd prefer this under #create as describe "with PRODUCTION_STAGE_LOCK_REQUIRES_ADMIN"

@@ -159,6 +212,48 @@ def create_lock(resource = nil, options = {})
end
end

describe '#create with PRODUCTION_STAGE_LOCK_REQUIRES_ADMIN' do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

too much copy-paste for my taste ... the tests above should be sufficient ...

@@ -170,6 +265,19 @@ def create_lock(resource = nil, options = {})
end
end

describe '#destroy with PRODUCTION_STAGE_LOCK_REQUIRES_ADMIN' do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

end

it 'cannot destroy a stage production lock' do
stage.production = true
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

stage.update_column(:production, true)

stage.save
delete :destroy, params: {id: lock.id}

assert_response :unauthorized
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this might be enough and drop the later assertions ?

it 'cannot create a stage lock for a production stage' do
create_lock prod_stage, delete_in: 3600

assert_response :unauthorized
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can drop the later assertions ... same in the test below

@jgoerz
Copy link
Contributor Author

jgoerz commented Jan 31, 2018

Ready for review/feedback.

Copy link
Contributor

@grosser grosser left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

awesome 🎉

@grosser grosser merged commit 0212441 into zendesk:master Jan 31, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants