Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions app/controllers/admin/rdap_privilege_grants_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
module Admin
class RdapPrivilegeGrantsController < BaseController
load_and_authorize_resource

def index
@q = RdapPrivilegeGrant.ransack(params[:q])
@rdap_privilege_grants = @q.result.page(params[:page]).order(created_at: :desc)
@count = @q.result.count
@rdap_privilege_grants = @rdap_privilege_grants.per(params[:results_per_page]) if paginate?
end

def new
@rdap_privilege_grant = RdapPrivilegeGrant.new
end

def show; end

def edit; end

def create
@rdap_privilege_grant = RdapPrivilegeGrant.new(rdap_privilege_grant_params)

if @rdap_privilege_grant.save
flash[:notice] = I18n.t('record_created')
redirect_to [:admin, @rdap_privilege_grant]
else
flash.now[:alert] = I18n.t('failed_to_create_record')
render 'new'
end
end

def update
if @rdap_privilege_grant.update(rdap_privilege_grant_params)
flash[:notice] = I18n.t('record_updated')
redirect_to [:admin, @rdap_privilege_grant]
else
flash.now[:alert] = I18n.t('failed_to_update_record')
render 'edit'
end
end

# Suspend and revoke are distinct member actions that change only `status`,
# never a generic edit of unrelated fields (RPD §9 lines 461; AC4/AC5).
def suspend
@rdap_privilege_grant.update!(status: 'suspended')
flash[:notice] = I18n.t('admin.rdap_privilege_grants.grant_suspended')
redirect_to [:admin, @rdap_privilege_grant]
end

def revoke
@rdap_privilege_grant.update!(status: 'revoked')
flash[:notice] = I18n.t('admin.rdap_privilege_grants.grant_revoked')
redirect_to [:admin, @rdap_privilege_grant]
end

private

def rdap_privilege_grant_params
params.require(:rdap_privilege_grant).permit(:eeid_subject,
:full_name,
:legal_basis_ref,
:personal_id_code,
:organization,
:category,
:valid_from,
:valid_until,
:notes)
end
end
end
1 change: 1 addition & 0 deletions app/models/ability.rb
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ def admin # Admin/admin_user dynamic role
can :manage, BankTransaction
can :manage, Invoice
can :manage, WhiteIp
can :manage, RdapPrivilegeGrant
can :manage, Account
can :manage, AccountActivity
can :manage, Dispute
Expand Down
19 changes: 19 additions & 0 deletions app/models/rdap_privilege_grant.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
class RdapPrivilegeGrant < ApplicationRecord
include Versions

CATEGORIES = %w[police cert ria eis_internal].freeze
STATUSES = %w[active revoked suspended].freeze

Expand All @@ -8,6 +10,8 @@ class RdapPrivilegeGrant < ApplicationRecord
validates :category, presence: true, inclusion: { in: CATEGORIES }
validates :status, presence: true, inclusion: { in: STATUSES }
validates :valid_from, presence: true
validates :full_name, presence: true
validates :legal_basis_ref, presence: true
validate :valid_until_after_valid_from

# The single authoritative "active" rule (mirrors the RDAP contract §4):
Expand All @@ -25,6 +29,21 @@ def grant_id
uuid.presence || id.to_s
end

# Expiry is DERIVED from valid_until at read time; it is never a stored status.
# STATUSES stays %w[active revoked suspended] (see AC15) so the frozen RDAP
# contract is untouched.
def expired?
valid_until.present? && valid_until <= Time.zone.now
end

# The status to present in the admin UI: an active grant whose valid_until has
# passed reads as "expired" without any stored status change.
def display_status
return 'expired' if status == 'active' && expired?

status
end

private

def assign_uuid
Expand Down
5 changes: 5 additions & 0 deletions app/models/version/rdap_privilege_grant_version.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class Version::RdapPrivilegeGrantVersion < PaperTrail::Version
include VersionSession
self.table_name = :log_rdap_privilege_grants
self.sequence_name = :log_rdap_privilege_grants_id_seq
end
2 changes: 2 additions & 0 deletions app/views/admin/base/_menu.haml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
%li.dropdown-header= t('.users')
%li= link_to t('.api_users'), admin_api_users_path
%li= link_to t('.admin_users'), admin_admin_users_path
- if can? :read, RdapPrivilegeGrant
%li= link_to t('.rdap_privilege_grants'), admin_rdap_privilege_grants_path
%li.divider
%li.dropdown-header= t(:billing)
- if can? :view, Billing::Price
Expand Down
57 changes: 57 additions & 0 deletions app/views/admin/rdap_privilege_grants/_form.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
= form_for([:admin, @rdap_privilege_grant], html: { class: 'form-horizontal', autocomplete: 'off' }) do |f|
= render 'shared/full_errors', object: @rdap_privilege_grant

.row
.col-md-8
.form-group
.col-md-4.control-label
= f.label :full_name, nil, class: 'required'
.col-md-8
= f.text_field :full_name, required: true, autofocus: true, class: 'form-control'
.form-group
.col-md-4.control-label
= f.label :eeid_subject, nil, class: 'required'
.col-md-8
= f.text_field :eeid_subject, required: true, class: 'form-control'
.form-group
.col-md-4.control-label
= f.label :organization
.col-md-8
= f.text_field :organization, class: 'form-control'
.form-group
.col-md-4.control-label
= f.label :category, nil, class: 'required'
.col-md-8
= f.select :category, RdapPrivilegeGrant::CATEGORIES.map { |c| [t("rdap_privilege_grant_category.#{c}", default: c), c] }, { include_blank: true }, required: true, class: 'form-control'
%hr
.form-group
.col-md-4.control-label
= f.label :valid_from, nil, class: 'required'
.col-md-8
= f.datetime_field :valid_from, required: true, class: 'form-control'
.form-group
.col-md-4.control-label
= f.label :valid_until
.col-md-8
= f.datetime_field :valid_until, class: 'form-control'
%hr
.form-group
.col-md-4.control-label
= f.label :legal_basis_ref, nil, class: 'required'
.col-md-8
= f.text_field :legal_basis_ref, required: true, class: 'form-control'
.form-group
.col-md-4.control-label
= f.label :personal_id_code
.col-md-8
= f.text_field :personal_id_code, class: 'form-control'
.form-group
.col-md-4.control-label
= f.label :notes
.col-md-8
= f.text_area :notes, rows: 3, class: 'form-control'

%hr
.row
.col-md-8.text-right
= button_tag(t(:save), class: 'btn btn-primary')
5 changes: 5 additions & 0 deletions app/views/admin/rdap_privilege_grants/edit.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
- content_for :actions do
= link_to(t(:back), [:admin, @rdap_privilege_grant], class: 'btn btn-default')
= render 'shared/title', name: "#{t(:edit)}: #{@rdap_privilege_grant.full_name}"

= render 'form'
40 changes: 40 additions & 0 deletions app/views/admin/rdap_privilege_grants/index.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
- content_for :actions do
= link_to(t('.new_btn'), new_admin_rdap_privilege_grant_path, class: 'btn btn-primary')
= render 'shared/title', name: t('.title')
= render 'application/pagination'

.row
.col-md-12
.table-responsive
%table.table.table-hover.table-bordered.table-condensed
%thead
%tr
%th{class: 'col-xs-2'}
= sort_link(@q, 'full_name', RdapPrivilegeGrant.human_attribute_name(:full_name))
%th{class: 'col-xs-2'}
= sort_link(@q, 'eeid_subject', RdapPrivilegeGrant.human_attribute_name(:eeid_subject))
%th{class: 'col-xs-2'}
= sort_link(@q, 'organization', RdapPrivilegeGrant.human_attribute_name(:organization))
%th{class: 'col-xs-1'}
= sort_link(@q, 'category', RdapPrivilegeGrant.human_attribute_name(:category))
%th{class: 'col-xs-1'}
= sort_link(@q, 'status', RdapPrivilegeGrant.human_attribute_name(:status))
%th{class: 'col-xs-2'}
= sort_link(@q, 'valid_from', RdapPrivilegeGrant.human_attribute_name(:valid_from))
%th{class: 'col-xs-2'}
= sort_link(@q, 'valid_until', RdapPrivilegeGrant.human_attribute_name(:valid_until))
%tbody
- @rdap_privilege_grants.each do |grant|
%tr
%td= link_to(grant.full_name, [:admin, grant])
%td= grant.eeid_subject
%td= grant.organization
%td= grant.category
%td= grant.display_status
%td= grant.valid_from
%td= grant.valid_until
.row
.col-md-6
= paginate @rdap_privilege_grants
.col-md-6.text-right
= t(:result_count, count: @count)
3 changes: 3 additions & 0 deletions app/views/admin/rdap_privilege_grants/new.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
= render 'shared/title', name: t('.title')

= render 'form'
68 changes: 68 additions & 0 deletions app/views/admin/rdap_privilege_grants/show.haml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
- content_for :actions do
= link_to(t(:edit), edit_admin_rdap_privilege_grant_path(@rdap_privilege_grant), class: 'btn btn-primary')
- if @rdap_privilege_grant.status == 'active'
= link_to(t('.suspend'), suspend_admin_rdap_privilege_grant_path(@rdap_privilege_grant), method: :post, data: { confirm: t(:are_you_sure) }, class: 'btn btn-warning')
- unless @rdap_privilege_grant.status == 'revoked'
= link_to(t('.revoke'), revoke_admin_rdap_privilege_grant_path(@rdap_privilege_grant), method: :post, data: { confirm: t(:are_you_sure) }, class: 'btn btn-danger')
= render 'shared/title', name: @rdap_privilege_grant.full_name

.row
.col-md-8
.panel.panel-default
.panel-heading
%h3.panel-title= t(:general)
.panel-body
%dl.dl-horizontal
%dt= RdapPrivilegeGrant.human_attribute_name :full_name
%dd= @rdap_privilege_grant.full_name

%dt= RdapPrivilegeGrant.human_attribute_name :eeid_subject
%dd= @rdap_privilege_grant.eeid_subject

%dt= RdapPrivilegeGrant.human_attribute_name :organization
%dd= @rdap_privilege_grant.organization

%dt= RdapPrivilegeGrant.human_attribute_name :category
%dd= @rdap_privilege_grant.category

%dt= RdapPrivilegeGrant.human_attribute_name :status
%dd= @rdap_privilege_grant.display_status

%dt= RdapPrivilegeGrant.human_attribute_name :valid_from
%dd= @rdap_privilege_grant.valid_from

%dt= RdapPrivilegeGrant.human_attribute_name :valid_until
%dd= @rdap_privilege_grant.valid_until

%dt= RdapPrivilegeGrant.human_attribute_name :legal_basis_ref
%dd= @rdap_privilege_grant.legal_basis_ref

%dt= RdapPrivilegeGrant.human_attribute_name :notes
%dd= @rdap_privilege_grant.notes

.row
.col-md-12
.panel.panel-default
.panel-heading
%h3.panel-title= t('.audit_history')
.panel-body
.table-responsive
%table.table.table-hover.table-bordered.table-condensed
%thead
%tr
%th= t('.event')
%th= t('.whodunnit')
%th= t('.changed_at')
%th= t('.changes')
%tbody
- @rdap_privilege_grant.versions.reorder(created_at: :desc).each do |version|
%tr
%td= version.event
%td= version.whodunnit
%td= version.created_at
%td
-# personal_id_code is captured but not surfaced in the rendered change table (PII).
- (version.object_changes || {}).except('personal_id_code').each do |attribute, change|
%div
%strong= attribute
= Array(change).join(' → ')
3 changes: 3 additions & 0 deletions config/initializers/filter_parameter_logging.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
# Internal RDAP data API: `subject` is a national id (sensitive PII) and
# `token_hash` is an RDAP-issued-token lookup key — neither may appear in logs.
config.filter_parameters += %i[subject token_hash]
# Privileged RDAP grant admin: personal_id_code is sensitive PII (RPD §9);
# it is capture-only and must never reach application logs.
config.filter_parameters += %i[personal_id_code]
config.filter_parameters << lambda do |key, value|
if key == 'raw_frame' && value.respond_to?(:gsub!)
value.gsub!(/pw>.+<\//, 'pw>[FILTERED]</')
Expand Down
1 change: 1 addition & 0 deletions config/locales/admin/menu.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ en:
users: Users
api_users: API users
admin_users: Admin users
rdap_privilege_grants: RDAP privilege grants
prices: Prices
archive: Archive
domain_history: Domain history
Expand Down
36 changes: 36 additions & 0 deletions config/locales/admin/rdap_privilege_grants.en.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
en:
admin:
rdap_privilege_grants:
index:
title: RDAP privilege grants
new_btn: New privilege grant
new:
title: New privilege grant
show:
suspend: Suspend
revoke: Revoke
audit_history: Audit history
event: Event
whodunnit: Changed by
changed_at: Changed at
changes: Changes
grant_suspended: Grant suspended
grant_revoked: Grant revoked
activerecord:
attributes:
rdap_privilege_grant:
full_name: Full name
eeid_subject: eeID subject
organization: Organization
category: Category
status: Status
valid_from: Valid from
valid_until: Valid until
legal_basis_ref: Legal basis / agreement reference
personal_id_code: Personal identification code
notes: Purpose / notes
rdap_privilege_grant_category:
police: Police
cert: CERT
ria: RIA
eis_internal: EIS internal
10 changes: 10 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,16 @@
end

resources :admin_users

# Privileged RDAP grants (RPD §9). No destroy: grants end via suspend/revoke,
# never a hard delete, consistent with the immutable-audit framing.
resources :rdap_privilege_grants, except: %i[destroy] do
member do
post :suspend
post :revoke
end
end

# /admin/api_users is mainly for manual testing
resources :api_users, only: %i[index show] do
resources :certificates do
Expand Down
Loading
Loading