Draft: [skip ci] Add endpoint and page data for GitLab Duo for Slack

What does this MR do and why?

An Owner needs one place to see and change where GitLab Duo runs for their Slack workspace, and everyone else needs to know where it runs and whom to ask. This MR adds the backend for that place: an endpoint that turns GitLab Duo on or off, and the state the integration page will show to each person who looks at it. The state tells whether GitLab Duo is off, on here, on in another group, managed by an administrator, or unclear because several Slack workspaces are connected in the projects of a group.

Nothing renders it yet, so nothing changes for anyone: everything is behind the default-off slack_duo_group flag. It builds on the services in !261845 (closed).

What's in it

  • PUT /groups/:group/-/settings/slack_duo: turns GitLab Duo on or off for a workspace and answers with the new page state. A missing enabled parameter answers 400, so a malformed request never turns GitLab Duo off. Plain Rails JSON, like the other integration settings pages.
  • StatusPresenter: for one integration page and one viewer, works out the state (not_connected, multiple_workspaces, off, on_here, elsewhere, instance_managed), what the viewer may do, and the requirements GitLab Duo needs with how to fix each. The backend decides, the frontend only renders.
  • Page data slack_duo in integration_form_data, unused until the UI MR.
  • A private group is never named to someone who cannot see it. The presenter returns the Duo group only if the viewer can read it.
  • A group with several workspaces and no install of its own gets multiple_workspaces and is asked to install the app on the group, instead of the presenter guessing one.

Where this fits

This is one of a stack of merge requests. Each one targets the branch of the one before it, so review and merge them in this order. Audit events and the documentation follow as separate merge requests.

  1. !261830 (closed): Table and model
  2. !261845 (closed): Services to turn GitLab Duo on or off
  3. !262156 (closed): Endpoint and page data (this MR)
  4. !262157 (closed): Mention path
  5. !262158 (closed): GitLab Duo section on the integration page
  6. !262159 (closed): Claim on the first group install
  7. !262160 (closed): Admin area picker (self-managed)
  8. !262161 (closed): Slack Marketplace install page
  9. !262165 (closed): Audit events
  10. !262171 (closed): Documentation

Plan and decisions: #633566. Rollout of the feature flag: #633582. Design discussion: gitlab-org#23704.

Context for review agents
  • Goal: every GitLab Duo session started from a Slack workspace runs and bills in one top-level group chosen by an Owner (the administrator on self-managed for an instance-wide install), instead of each user's default Duo namespace. Plan: #633566.
  • Everything is behind the default-off slack_duo_group feature flag (type wip, becomes beta before rollout, rolled out per top-level group). The Slack flow has its own flag slack_duo_api_flow; DuoGroup.available? requires both for the same group.
  • Deliberate decisions, do not flag them unless you find a real defect:
    • One Duo group per workspace (team_id unique), one group can run several workspaces. No organization_id column: namespace_id is the single sharding key. The unique index on team_id holds per cell only, tracked in #633611.
    • A group install claims the workspace, a project install does not. Copies of group or instance installs (inherit_from_id) never let a group claim a workspace. A workspace installed for the whole instance (self-managed only) is the administrator's to assign.
    • An unclaimed workspace has no fallback to the user's default namespace once the flag is on: the user gets a reply that says who can set it up.
    • A Duo group always runs the Slack API flow (slack_assistant/v1), the Developer flow is out. Members of the Duo group only, also on self-managed. Guests pass the membership check and are blocked later, documented as not supported for now.
    • Audit events for enable and disable come in a separate small MR before the flag leaves wip.
  • Test world for manual validation: #633566 (comment 4012898135).
  • Please look hard at: StatusPresenter#state, and visible_duo_group (no name leak). The Gitlab/Authz/PermissionCheck rubocop disables use the same admin_group check as installing the app on a group (SlackInstallation::GroupService), so this MR needs an authorization review.
  • The endpoint is gated like the page that will render the section: slack_duo_group for the root group plus slack_duo_agent_available?.

References

Screenshots or screen recordings

None: this MR has no UI. The Duo section on the integration page is the next-but-one MR, and that is where you can click through the whole flow.

How to set up and validate locally

  1. Seed the test world (users, groups and fake Slack installs in every state, no Slack needed): #633566 (comment 4012898135). Run it from your gitlab directory:

    bin/rails runner -e development seed_duo_slack.rb

    It also enables the slack_duo_group, slack_duo_agent and slack_duo_api_flow feature flags. Run it again at any time to reset all GitLab Duo state.

  2. Drive the real endpoint as different people. Save this as drive_endpoint.rb and run it with bin/rails runner -e development drive_endpoint.rb:

    require 'warden'
    include Warden::Test::Helpers
    Warden.test_mode!
    ActionController::Base.allow_forgery_protection = false
    
    team = 'T0WWA'
    session = ActionDispatch::Integration::Session.new(Rails.application)
    session.host! Gitlab.config.gitlab.host
    put = lambda do |username, path, params|
      login_as(User.find_by_username(username))
      session.put "/groups/#{path}/-/settings/slack_duo", params: params
      body = (JSON.parse(session.response.body) rescue {})
      "HTTP #{session.response.status} #{body.slice('state', 'reason').inspect}"
    end
    duo_row = -> { Integrations::SlackWorkspace::DuoGroup.for_team(team)&.namespace&.full_path.inspect }
    
    Integrations::SlackWorkspace::DuoGroup.where(team_id: team).destroy_all
    puts "Maintainer turns on:        #{put.call('sd-maint', 'sd-a', { team_id: team, enabled: true })}"
    puts "Owner turns on:             #{put.call('sd-owner', 'sd-a', { team_id: team, enabled: true })}   row=#{duo_row.call}"
    puts "Owner turns on again:       #{put.call('sd-owner', 'sd-a', { team_id: team, enabled: true })}"
    puts "Owner without enabled:      #{put.call('sd-owner', 'sd-a', { team_id: team })}   row=#{duo_row.call}"
    puts "Other group's Owner:        #{put.call('sd-owner2', 'sd-other', { team_id: team, enabled: true })}"
    puts "Owner turns off:            #{put.call('sd-owner', 'sd-a', { team_id: team, enabled: false })}   row=#{duo_row.call}"
    Feature.disable(:slack_duo_group)
    puts "Flag off:                   #{put.call('sd-owner', 'sd-a', { team_id: team, enabled: true })}"
    Feature.enable(:slack_duo_group)
  3. Expected output:

    Maintainer turns on:        HTTP 404 {}
    Owner turns on:             HTTP 200 {"state"=>"on_here"}   row="sd-a"
    Owner turns on again:       HTTP 200 {"state"=>"on_here"}
    Owner without enabled:      HTTP 400 {"reason"=>"missing_enabled"}   row="sd-a"
    Other group's Owner:        HTTP 422 {"reason"=>"claimed_by_another_group"}
    Owner turns off:            HTTP 200 {"state"=>"off"}   row=nil
    Flag off:                   HTTP 404 {}
    Step What it shows
    Maintainer Cannot change it, the endpoint answers 404 like other group admin pages
    Owner without enabled A malformed request leaves GitLab Duo as it is
    Other group's Owner The reason is returned as JSON for the page to show
    Flag off The endpoint is gone

MR acceptance checklist

Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Edited by Thomas Schmidt

Merge request reports

Loading
Loading