feat(device-core): add restricted identity references
This commit is contained in:
+233
@@ -0,0 +1,233 @@
|
||||
begin;
|
||||
|
||||
do $$
|
||||
begin
|
||||
if not exists (
|
||||
select 1 from pg_constraint
|
||||
where conname = 'device_instances_direct_legacy_credential_check'
|
||||
and conrelid = 'device_instances'::regclass
|
||||
) then
|
||||
alter table device_instances
|
||||
add constraint device_instances_direct_legacy_credential_check
|
||||
check (owner_scope_id is null or credential_ref is null)
|
||||
not valid;
|
||||
end if;
|
||||
end
|
||||
$$;
|
||||
|
||||
create table if not exists device_restricted_identifiers (
|
||||
id uuid primary key,
|
||||
device_id uuid not null references device_instances(id),
|
||||
owner_scope_id uuid not null,
|
||||
project_id uuid not null,
|
||||
identifier_kind text not null
|
||||
check (identifier_kind ~ '^[a-z][a-z0-9._:-]{1,63}$'),
|
||||
identifier_digest text not null
|
||||
check (identifier_digest ~ '^hmac-sha256:[a-f0-9]{64}$'),
|
||||
identifier_masked text not null
|
||||
check (
|
||||
length(identifier_masked) between 5 and 128
|
||||
and position('*' in identifier_masked) > 0
|
||||
and identifier_masked !~ '[[:cntrl:]]'
|
||||
and identifier_masked !~ '(^|[^0-9])[0-9]{15}([^0-9]|$)'
|
||||
),
|
||||
provenance_kind text not null
|
||||
check (provenance_kind in ('claim', 'adapter_observation')),
|
||||
is_primary boolean not null default false,
|
||||
lifecycle_state text not null default 'active'
|
||||
check (lifecycle_state in ('active', 'revoked')),
|
||||
created_by_ref text not null
|
||||
check (length(btrim(created_by_ref)) between 3 and 256),
|
||||
revoked_at timestamptz,
|
||||
revoked_by_ref text
|
||||
check (
|
||||
revoked_by_ref is null
|
||||
or length(btrim(revoked_by_ref)) between 3 and 256
|
||||
),
|
||||
revocation_code text
|
||||
check (
|
||||
revocation_code is null
|
||||
or revocation_code ~ '^[a-z][a-z0-9._-]{1,63}$'
|
||||
),
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
foreign key (project_id, owner_scope_id)
|
||||
references device_projects(id, owner_scope_id),
|
||||
check (
|
||||
(lifecycle_state = 'active' and revoked_at is null and revoked_by_ref is null and revocation_code is null)
|
||||
or
|
||||
(lifecycle_state = 'revoked' and revoked_at is not null and revoked_by_ref is not null and revocation_code is not null)
|
||||
)
|
||||
);
|
||||
|
||||
create unique index if not exists device_restricted_identifiers_active_identity_idx
|
||||
on device_restricted_identifiers (identifier_kind, identifier_digest)
|
||||
where lifecycle_state = 'active';
|
||||
|
||||
create unique index if not exists device_restricted_identifiers_primary_idx
|
||||
on device_restricted_identifiers (device_id)
|
||||
where lifecycle_state = 'active' and is_primary;
|
||||
|
||||
create index if not exists device_restricted_identifiers_device_idx
|
||||
on device_restricted_identifiers (device_id, lifecycle_state, created_at);
|
||||
|
||||
create or replace function device_assert_identifier_current_owner()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
begin
|
||||
if new.lifecycle_state = 'active' and not exists (
|
||||
select 1 from device_instances di
|
||||
where di.id = new.device_id
|
||||
and di.owner_scope_id = new.owner_scope_id
|
||||
and di.project_id = new.project_id
|
||||
) then
|
||||
raise foreign_key_violation using
|
||||
message = 'device_identifier_ownership_mismatch';
|
||||
end if;
|
||||
return new;
|
||||
end
|
||||
$$;
|
||||
|
||||
drop trigger if exists device_restricted_identifiers_owner_guard
|
||||
on device_restricted_identifiers;
|
||||
|
||||
create trigger device_restricted_identifiers_owner_guard
|
||||
before insert or update of device_id, owner_scope_id, project_id, lifecycle_state
|
||||
on device_restricted_identifiers
|
||||
for each row
|
||||
execute function device_assert_identifier_current_owner();
|
||||
|
||||
create or replace function device_assert_active_identifiers_follow_owner()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
begin
|
||||
if exists (
|
||||
select 1 from device_restricted_identifiers dri
|
||||
where dri.device_id = new.id
|
||||
and dri.lifecycle_state = 'active'
|
||||
and (
|
||||
dri.owner_scope_id is distinct from new.owner_scope_id
|
||||
or dri.project_id is distinct from new.project_id
|
||||
)
|
||||
) then
|
||||
raise foreign_key_violation using
|
||||
message = 'device_active_identifier_ownership_mismatch';
|
||||
end if;
|
||||
return new;
|
||||
end
|
||||
$$;
|
||||
|
||||
drop trigger if exists device_instances_identifier_owner_guard
|
||||
on device_instances;
|
||||
|
||||
create constraint trigger device_instances_identifier_owner_guard
|
||||
after update
|
||||
on device_instances
|
||||
deferrable initially deferred
|
||||
for each row
|
||||
execute function device_assert_active_identifiers_follow_owner();
|
||||
|
||||
create table if not exists device_credential_bindings (
|
||||
id uuid primary key,
|
||||
device_id uuid not null references device_instances(id),
|
||||
owner_scope_id uuid not null,
|
||||
project_id uuid not null,
|
||||
purpose text not null
|
||||
check (purpose ~ '^[a-z][a-z0-9._-]{1,63}$'),
|
||||
credential_owner text not null
|
||||
check (credential_owner = 'ndc_l2_credentials'),
|
||||
credential_ref text not null
|
||||
check (credential_ref ~ '^ndc-credref:[A-Za-z0-9][A-Za-z0-9._:-]{7,240}$'),
|
||||
lifecycle_state text not null default 'active'
|
||||
check (lifecycle_state in ('active', 'revoked')),
|
||||
bound_by_ref text not null
|
||||
check (length(btrim(bound_by_ref)) between 3 and 256),
|
||||
revoked_at timestamptz,
|
||||
revoked_by_ref text
|
||||
check (
|
||||
revoked_by_ref is null
|
||||
or length(btrim(revoked_by_ref)) between 3 and 256
|
||||
),
|
||||
revocation_code text
|
||||
check (
|
||||
revocation_code is null
|
||||
or revocation_code ~ '^[a-z][a-z0-9._-]{1,63}$'
|
||||
),
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
foreign key (project_id, owner_scope_id)
|
||||
references device_projects(id, owner_scope_id),
|
||||
check (
|
||||
(lifecycle_state = 'active' and revoked_at is null and revoked_by_ref is null and revocation_code is null)
|
||||
or
|
||||
(lifecycle_state = 'revoked' and revoked_at is not null and revoked_by_ref is not null and revocation_code is not null)
|
||||
)
|
||||
);
|
||||
|
||||
create unique index if not exists device_credential_bindings_active_purpose_idx
|
||||
on device_credential_bindings (device_id, purpose)
|
||||
where lifecycle_state = 'active';
|
||||
|
||||
create index if not exists device_credential_bindings_project_state_idx
|
||||
on device_credential_bindings (project_id, lifecycle_state, updated_at desc);
|
||||
|
||||
create or replace function device_assert_credential_binding_current_owner()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
begin
|
||||
if new.lifecycle_state = 'active' and not exists (
|
||||
select 1 from device_instances di
|
||||
where di.id = new.device_id
|
||||
and di.owner_scope_id = new.owner_scope_id
|
||||
and di.project_id = new.project_id
|
||||
) then
|
||||
raise foreign_key_violation using
|
||||
message = 'device_credential_binding_ownership_mismatch';
|
||||
end if;
|
||||
return new;
|
||||
end
|
||||
$$;
|
||||
|
||||
drop trigger if exists device_credential_bindings_owner_guard
|
||||
on device_credential_bindings;
|
||||
|
||||
create trigger device_credential_bindings_owner_guard
|
||||
before insert or update of device_id, owner_scope_id, project_id, lifecycle_state
|
||||
on device_credential_bindings
|
||||
for each row
|
||||
execute function device_assert_credential_binding_current_owner();
|
||||
|
||||
create or replace function device_require_credential_revoke_before_transfer()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
begin
|
||||
if exists (
|
||||
select 1 from device_credential_bindings dcb
|
||||
where dcb.device_id = old.id
|
||||
and dcb.lifecycle_state = 'active'
|
||||
) then
|
||||
raise check_violation using
|
||||
message = 'device_transfer_active_credential_binding';
|
||||
end if;
|
||||
return new;
|
||||
end
|
||||
$$;
|
||||
|
||||
drop trigger if exists device_instances_credential_transfer_guard
|
||||
on device_instances;
|
||||
|
||||
create trigger device_instances_credential_transfer_guard
|
||||
before update of owner_scope_id, project_id
|
||||
on device_instances
|
||||
for each row
|
||||
when (
|
||||
old.owner_scope_id is distinct from new.owner_scope_id
|
||||
or old.project_id is distinct from new.project_id
|
||||
)
|
||||
execute function device_require_credential_revoke_before_transfer();
|
||||
|
||||
commit;
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
begin;
|
||||
|
||||
alter table device_management_command_receipts
|
||||
drop constraint if exists device_management_command_receipts_command_kind_check;
|
||||
|
||||
alter table device_management_command_receipts
|
||||
add constraint device_management_command_receipts_command_kind_check
|
||||
check (command_kind in (
|
||||
'owner_scope.ensure',
|
||||
'project.ensure',
|
||||
'collection.ensure',
|
||||
'project_grant.upsert',
|
||||
'adapter_package.ensure',
|
||||
'adapter_version.register',
|
||||
'model_profile.register',
|
||||
'edge.ensure',
|
||||
'route.ensure',
|
||||
'enrollment_intent.ensure',
|
||||
'device.claim',
|
||||
'device.transfer',
|
||||
'discovery.reject',
|
||||
'discovery.expire',
|
||||
'device_credential_binding.upsert',
|
||||
'device_credential_binding.revoke'
|
||||
));
|
||||
|
||||
commit;
|
||||
Reference in New Issue
Block a user