mirror of
https://github.com/absmach/magistrala.git
synced 2026-08-07 07:14:46 +00:00
09a56575c1
Property Based Tests / api-test (push) Has been cancelled
Continuous Delivery / lint-and-build (push) Has been cancelled
Deploy GitHub Pages / swagger-ui (push) Has been cancelled
CI Pipeline / Lint Proto (push) Has been cancelled
CI Pipeline / Detect Changes (push) Has been cancelled
Continuous Delivery / Build and Push Docker Images (push) Has been cancelled
CI Pipeline / lint-and-build (push) Has been cancelled
CI Pipeline / Test ${{ matrix.module }} (push) Has been cancelled
CI Pipeline / Upload Coverage (push) Has been cancelled
Signed-off-by: dusan <borovcanindusan1@gmail.com>
274 lines
7.3 KiB
Go
274 lines
7.3 KiB
Go
// Copyright (c) Abstract Machines
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/jmoiron/sqlx"
|
|
)
|
|
|
|
// Verify reconciles a completed migration: every source row that should have
|
|
// migrated must exist in Atom, and a sample of reconstructed authz edges
|
|
// (device→channel publish/subscribe) must be present. Read-only.
|
|
func (m *migrator) Verify(ctx context.Context, rep *report) error {
|
|
domSet, err := m.domainSet(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
atomTenants, err := idSet(ctx, m.atom, `SELECT id::text FROM tenants`)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
atomEntities, err := idSet(ctx, m.atom, `SELECT id::text FROM entities`)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
atomAuthenticatedUsers, err := idSet(ctx, m.atom,
|
|
`SELECT entity_id::text FROM principal_group_members WHERE group_id = $1`, authenticatedUsersGroupID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
atomResources, err := idSet(ctx, m.atom, `SELECT id::text FROM resources`)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
atomGroups, err := idSet(ctx, m.atom, `SELECT id::text FROM object_groups`)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
atomSharedKeys, err := idSet(ctx, m.atom, `SELECT id::text FROM credentials WHERE kind = 'shared_key'`)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
atomAccessTokens, err := idSet(ctx, m.atom, `SELECT id::text FROM credentials WHERE kind = 'access_token'`)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// 1. tenants
|
|
doms, err := readDomains(ctx, m.domainsDB)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
m.reconcile(rep, "tenants", idsOf(len(doms), func(i int) (string, bool) { return doms[i].ID, true }), atomTenants)
|
|
|
|
// 2. human entities
|
|
users, err := readUsers(ctx, m.usersDB)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
userIDs := idsOf(len(users), func(i int) (string, bool) { return users[i].ID, true })
|
|
m.reconcile(rep, "entities.users", userIDs, atomEntities)
|
|
m.reconcile(rep, "principal_group_members.authenticated_users", userIDs, atomAuthenticatedUsers)
|
|
|
|
// 3. device entities (only those with a valid domain were migrated)
|
|
clients, err := readClients(ctx, m.clientsDB)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
m.reconcile(rep, "entities.clients", idsOf(len(clients), func(i int) (string, bool) {
|
|
return clients[i].ID, domSet[clients[i].DomainID]
|
|
}), atomEntities)
|
|
m.reconcile(rep, "credentials.device_shared_keys", idsOf(len(clients), func(i int) (string, bool) {
|
|
return clientSharedKeyCredentialID(clients[i].ID), domSet[clients[i].DomainID] && clients[i].Secret.Valid && clients[i].Secret.String != ""
|
|
}), atomSharedKeys)
|
|
|
|
pats, err := readPATs(ctx, m.authDB)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
userSet := map[string]bool{}
|
|
for _, u := range users {
|
|
userSet[u.ID] = true
|
|
}
|
|
m.reconcile(rep, "credentials.pats", idsOf(len(pats), func(i int) (string, bool) {
|
|
return pats[i].ID, pats[i].UserID.Valid && userSet[pats[i].UserID.String]
|
|
}), atomAccessTokens)
|
|
|
|
// 4. resources
|
|
chans, err := readChannels(ctx, m.channelsDB)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
m.reconcile(rep, "resources.channels", idsOf(len(chans), func(i int) (string, bool) {
|
|
return chans[i].ID, domSet[chans[i].DomainID]
|
|
}), atomResources)
|
|
|
|
// 4b. resources: rules, reports
|
|
rules, err := readRules(ctx, m.reDB)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
m.reconcile(rep, "resources.rules", idsOf(len(rules), func(i int) (string, bool) {
|
|
return rules[i].ID, domSet[rules[i].DomainID]
|
|
}), atomResources)
|
|
reports, err := readReports(ctx, m.reportsDB)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
m.reconcile(rep, "resources.reports", idsOf(len(reports), func(i int) (string, bool) {
|
|
return reports[i].ID, domSet[reports[i].DomainID]
|
|
}), atomResources)
|
|
|
|
// 5. object_groups
|
|
grps, err := readGroups(ctx, m.groupsDB)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
m.reconcile(rep, "object_groups", idsOf(len(grps), func(i int) (string, bool) {
|
|
return grps[i].ID, domSet[grps[i].DomainID]
|
|
}), atomGroups)
|
|
|
|
// 6. authz spot-check: every connection must have a device->channel policy.
|
|
if err := m.verifyConnections(ctx, rep, domSet); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *migrator) domainSet(ctx context.Context) (map[string]bool, error) {
|
|
doms, err := readDomains(ctx, m.domainsDB)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
s := map[string]bool{}
|
|
for _, d := range doms {
|
|
s[d.ID] = true
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
// reconcile counts how many expected ids are missing from the atom set.
|
|
func (m *migrator) reconcile(rep *report, label string, expected []string, atom map[string]bool) {
|
|
missing := 0
|
|
for _, id := range expected {
|
|
if !atom[id] {
|
|
missing++
|
|
}
|
|
}
|
|
rep.count("verify."+label+".expected", len(expected))
|
|
if missing > 0 {
|
|
rep.blockf("verify %s: %d of %d expected rows missing from Atom", label, missing, len(expected))
|
|
} else {
|
|
rep.count("verify."+label+".ok", len(expected))
|
|
}
|
|
}
|
|
|
|
func (m *migrator) verifyConnections(ctx context.Context, rep *report, domSet map[string]bool) error {
|
|
cli, err := readConnections(ctx, m.clientsDB)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
ch, err := readConnections(ctx, m.channelsDB)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
clients, err := readClients(ctx, m.clientsDB)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
clientDomain := map[string]string{}
|
|
for _, c := range clients {
|
|
if domSet[c.DomainID] {
|
|
clientDomain[c.ID] = c.DomainID
|
|
}
|
|
}
|
|
channels, err := readChannels(ctx, m.channelsDB)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
channelDomain := map[string]string{}
|
|
for _, c := range channels {
|
|
if domSet[c.DomainID] {
|
|
channelDomain[c.ID] = c.DomainID
|
|
}
|
|
}
|
|
// Build atom edge set: subject_id | channel(object_id) | action.
|
|
edges := map[string]bool{}
|
|
rows, err := m.atom.QueryxContext(ctx, `
|
|
SELECT dp.subject_id::text, pb.object_id::text, a.name
|
|
FROM direct_policies dp
|
|
JOIN permission_blocks pb ON pb.id = dp.permission_block_id
|
|
JOIN permission_block_actions pba ON pba.permission_block_id = pb.id
|
|
JOIN actions a ON a.id = pba.action_id
|
|
WHERE pb.scope_mode = 'object' AND pb.object_kind = 'resource'`)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for rows.Next() {
|
|
var s, o, act string
|
|
if err := rows.Scan(&s, &o, &act); err != nil {
|
|
rows.Close()
|
|
return err
|
|
}
|
|
edges[s+"|"+o+"|"+act] = true
|
|
}
|
|
rows.Close()
|
|
|
|
seen := map[string]bool{}
|
|
expected, missing := 0, 0
|
|
for _, c := range append(cli, ch...) {
|
|
act, ok := connectionAction(c.Type)
|
|
if !ok {
|
|
continue
|
|
}
|
|
chDom, ok := channelDomain[c.ChannelID]
|
|
if !ok {
|
|
continue
|
|
}
|
|
clDom, ok := clientDomain[c.ClientID]
|
|
if !ok || clDom != chDom || c.DomainID != chDom {
|
|
continue
|
|
}
|
|
k := c.ClientID + "|" + c.ChannelID + "|" + act
|
|
if seen[k] {
|
|
continue
|
|
}
|
|
seen[k] = true
|
|
expected++
|
|
if !edges[k] {
|
|
missing++
|
|
}
|
|
}
|
|
rep.count("verify.connections.expected", expected)
|
|
if missing > 0 {
|
|
rep.blockf("verify connections: %d of %d device->channel edges missing", missing, expected)
|
|
} else {
|
|
rep.count("verify.connections.ok", expected)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// --- helpers ---
|
|
|
|
func idSet(ctx context.Context, db *sqlx.DB, query string, args ...any) (map[string]bool, error) {
|
|
rows, err := db.QueryxContext(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
s := map[string]bool{}
|
|
for rows.Next() {
|
|
var id string
|
|
if err := rows.Scan(&id); err != nil {
|
|
return nil, err
|
|
}
|
|
s[id] = true
|
|
}
|
|
return s, rows.Err()
|
|
}
|
|
|
|
// idsOf collects ids for indices 0..n-1 where the picker's second return is true.
|
|
func idsOf(n int, pick func(i int) (string, bool)) []string {
|
|
out := make([]string, 0, n)
|
|
for i := range n {
|
|
if id, ok := pick(i); ok {
|
|
out = append(out, id)
|
|
}
|
|
}
|
|
return out
|
|
}
|