class DeveloperAppsController < ApplicationController include IdentityAuthorizable before_action :set_app, only: [ :show, :edit, :update, :destroy, :rotate_credentials, :revoke_all_authorizations, :activity_log ] def index authorize Program @apps = policy_scope(Program).includes(:owner_identity).order(created_at: :desc) if admin? @apps = @apps.where( "oauth_applications.name ILIKE :q OR oauth_applications.uid = :uid", q: "%#{params[:search]}%", uid: params[:search] ) end @apps = @apps.page(params[:page]).per(25) @pending_invitations = current_identity.pending_collaboration_invitations end def show authorize @app @identities_count = @app.identities.distinct.count if policy(@app).manage_collaborators? @collaborators = @app.program_collaborators.accepted.includes(:identity) @pending_invitations_for_app = @app.program_collaborators.pending end end def activity_log authorize @app @activities = PublicActivity::Activity .where(trackable: @app) .includes(:owner) .order(created_at: :desc) .limit(50) render layout: "htmx" end def new @app = Program.new(trust_level: :community_untrusted) authorize @app end def create @app = Program.new(app_params_for_identity) authorize @app unless policy(@app).update_trust_level? @app.trust_level = :community_untrusted end @app.owner_identity = current_identity # Server-side scope enforcement: only allow scopes within this user's tier enforce_allowed_scopes!(@app, existing_scopes: []) if @app.save redirect_to developer_app_path(@app), notice: t(".success") else render :new, status: :unprocessable_entity end end def edit authorize @app end def update authorize @app snapshot = @app.audit_snapshot existing_scopes = @app.scopes_array.dup @app.assign_attributes(app_params_for_identity) # Server-side scope enforcement: preserve locked scopes, reject unauthorized additions enforce_allowed_scopes!(@app, existing_scopes: existing_scopes) if @app.save changes = @app.audit_diff(snapshot) if changes.any? @app.create_activity :change, owner: current_identity, parameters: { changes: changes } end redirect_to developer_app_path(@app), notice: t(".success") else render :edit, status: :unprocessable_entity end end def destroy authorize @app app_name = @app.name @app.create_activity :destroy, owner: current_identity, parameters: { name: app_name } @app.destroy redirect_to developer_apps_path, notice: t(".success"), status: :see_other end def rotate_credentials authorize @app @app.rotate_credentials! @app.create_activity :rotate_credentials, owner: current_identity redirect_to developer_app_path(@app), notice: t(".success") end def revoke_all_authorizations authorize @app count = @app.access_tokens.update_all(revoked_at: Time.current) PaperTrail.request.whodunnit = current_identity.id.to_s @app.paper_trail_event = "revoke_all_authorizations" @app.paper_trail.save_with_version @app.create_activity :revoke_all_authorizations, owner: current_identity, parameters: { count: count } redirect_to developer_app_path(@app), notice: t(".success", count: count) end private def set_app @app = Program.find(params[:id]) rescue ActiveRecord::RecordNotFound flash[:error] = t("developer_apps.set_app.not_found") redirect_to developer_apps_path end # Server-side enforcement: a user can only add/remove scopes within their # allowed tier. Scopes outside that tier that already exist on the app # ("locked scopes") are always preserved — a community user editing an # hq_official app cannot strip `basic_info`, and nobody can inject # `set_slack_id` via a forged form. # # Formula: final = (submitted ∩ allowed) ∪ (existing ∩ ¬allowed) def enforce_allowed_scopes!(app, existing_scopes:) allowed = policy(app).allowed_scopes submitted = app.scopes_array user_controlled = submitted & allowed # only keep what they're allowed to touch locked = existing_scopes - allowed # preserve what they can't touch app.scopes_array = (user_controlled + locked).uniq end def app_params_for_identity permitted = [ :name, :redirect_uri, scopes_array: [] ] if policy(@app || Program.new).update_trust_level? permitted << :trust_level end if policy(@app || Program.new).update_onboarding_scenario? permitted << :onboarding_scenario end if policy(@app || Program.new).update_active? permitted << :active end params.require(:program).permit(permitted) end def admin? backend_user = current_identity.backend_user backend_user&.program_manager? || backend_user&.super_admin? end helper_method :admin? end