mirror of
https://github.com/System-End/identity-vault.git
synced 2026-04-19 19:45:08 +00:00
176 lines
5.3 KiB
Ruby
176 lines
5.3 KiB
Ruby
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)
|
||
.to_a
|
||
|
||
# Preload backend_user through the polymorphic owner to avoid N+1 in IdentityMention
|
||
identities = @activities.filter_map { |a| a.owner if a.owner_type == "Identity" }
|
||
ActiveRecord::Associations::Preloader.new(records: identities, associations: :backend_user).call if identities.any?
|
||
|
||
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_byline?
|
||
permitted << :byline
|
||
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
|