Apps index access column + summary + admin access checker
Some checks failed
Some checks failed
The Applications index used to render "All users" whenever an app had no allowed_groups; under default-deny that's the opposite of the truth. Replaced with a "No one" badge and, when groups are present, a "N users · M groups" cell so the access reality is visible at a glance. Added a small stats strip above the apps table: applications, users with access, and groups granting access. Backed by preloaded counts in the controller to avoid N+1. Added /admin/access — a small "Access check" tool that takes a user and an application and reports whether the user can reach it, with the granting group(s) when allowed, and the specific reason when not (inactive app/user, no allowed groups, or no shared group). Wired into the admin sidebar. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
77
app/views/admin/access_checks/new.html.erb
Normal file
77
app/views/admin/access_checks/new.html.erb
Normal file
@@ -0,0 +1,77 @@
|
||||
<div class="mb-6">
|
||||
<h1 class="text-2xl font-semibold text-gray-900 dark:text-gray-100">Access check</h1>
|
||||
<p class="mt-2 text-sm text-gray-700 dark:text-gray-300">Pick a user and an application to see whether the user can access it and, if so, which group(s) grant that access.</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<%= form_with url: admin_access_path, method: :post, class: "space-y-4" do |form| %>
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<%= form.label :user_id, "User", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
|
||||
<%= form.select :user_id,
|
||||
@users.map { |u| [u.email_address, u.id] },
|
||||
{ include_blank: "Select a user…", selected: @user&.id },
|
||||
class: "mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" %>
|
||||
</div>
|
||||
<div>
|
||||
<%= form.label :application_id, "Application", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
|
||||
<%= form.select :application_id,
|
||||
@applications.map { |a| [a.name, a.id] },
|
||||
{ include_blank: "Select an application…", selected: @application&.id },
|
||||
class: "mt-1 block w-full rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-gray-100 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm" %>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<%= form.submit "Check access", class: "rounded-md bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if @user && @application %>
|
||||
<div class="mt-6 rounded-md border <%= @allowed ? "border-green-200 dark:border-green-700 bg-green-50 dark:bg-green-900/30" : "border-red-200 dark:border-red-700 bg-red-50 dark:bg-red-900/30" %> p-4">
|
||||
<div class="flex items-start gap-3">
|
||||
<% if @allowed %>
|
||||
<svg class="h-6 w-6 text-green-600 dark:text-green-400 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
<% else %>
|
||||
<svg class="h-6 w-6 text-red-600 dark:text-red-400 shrink-0" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
<% end %>
|
||||
<div class="flex-1">
|
||||
<p class="text-sm font-medium <%= @allowed ? "text-green-800 dark:text-green-200" : "text-red-800 dark:text-red-200" %>">
|
||||
<%= @user.email_address %> <%= @allowed ? "can access" : "cannot access" %> <%= @application.name %>.
|
||||
</p>
|
||||
<% if @allowed %>
|
||||
<p class="mt-1 text-xs text-green-700 dark:text-green-300">
|
||||
Granted via:
|
||||
<% @via.each_with_index do |g, i| %>
|
||||
<%= link_to g.name, admin_group_path(g), class: "underline" %><%= "," unless i == @via.size - 1 %>
|
||||
<% end %>
|
||||
</p>
|
||||
<% else %>
|
||||
<p class="mt-1 text-xs text-red-700 dark:text-red-300">
|
||||
<% reasons = [] %>
|
||||
<% reasons << "the application is inactive" unless @application.active? %>
|
||||
<% reasons << "the user is #{@user.status.humanize.downcase}" unless @user.active? %>
|
||||
<% if @application.active? && @user.active? %>
|
||||
<% if @application.allowed_groups.empty? %>
|
||||
<% reasons << "the application has no allowed groups (default deny)" %>
|
||||
<% else %>
|
||||
<% reasons << "the user shares no group with the application's allowed groups" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
Reason: <%= reasons.join("; ") %>.
|
||||
</p>
|
||||
<% end %>
|
||||
<p class="mt-2 text-xs text-gray-600 dark:text-gray-400">
|
||||
<%= link_to "View user", admin_user_path(@user), class: "underline" %> ·
|
||||
<%= link_to "View application", admin_application_path(@application), class: "underline" %>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user