mirror of
https://github.com/absmach/magistrala.git
synced 2026-06-23 04:10:28 +00:00
@@ -4,7 +4,7 @@
|
||||
override MG_DOCKER_IMAGE_NAME_PREFIX := ghcr.io/absmach/magistrala
|
||||
MG_DOCKER_VOLUME_NAME_PREFIX ?= magistrala
|
||||
BUILD_DIR ?= build
|
||||
SERVICES = notifications certs re postgres-writer postgres-reader timescale-writer timescale-reader alarms reports journal fluxmq
|
||||
SERVICES = atom-bootstrap notifications certs re postgres-writer postgres-reader timescale-writer timescale-reader alarms reports journal fluxmq
|
||||
TEST_API_SERVICES = journal certs clients users channels groups domains
|
||||
TEST_API = $(addprefix test_api_,$(TEST_API_SERVICES))
|
||||
DOCKERS = $(addprefix docker_,$(SERVICES))
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package main contains the one-shot Magistrala Atom bootstrap command.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/absmach/magistrala/internal/atom"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultRetries = 30
|
||||
defaultRetryInterval = 2 * time.Second
|
||||
defaultTimeout = 30 * time.Second
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
|
||||
|
||||
cfg := atom.LoadConfig()
|
||||
if cfg.URL == "" {
|
||||
log.Fatal("ATOM_URL is required")
|
||||
}
|
||||
|
||||
client := atom.NewClient(cfg)
|
||||
retries := envInt("MG_ATOM_BOOTSTRAP_RETRIES", defaultRetries)
|
||||
retryInterval := envDuration("MG_ATOM_BOOTSTRAP_RETRY_INTERVAL", defaultRetryInterval)
|
||||
timeout := envDuration("MG_ATOM_BOOTSTRAP_TIMEOUT", defaultTimeout)
|
||||
|
||||
var lastErr error
|
||||
for attempt := 1; attempt <= retries; attempt++ {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
err := atom.BootstrapMagistralaActions(ctx, client)
|
||||
cancel()
|
||||
if err == nil {
|
||||
log.Printf("Magistrala Atom action bootstrap completed")
|
||||
return
|
||||
}
|
||||
lastErr = err
|
||||
if attempt < retries {
|
||||
log.Printf("Magistrala Atom action bootstrap attempt %d/%d failed: %v; retrying in %s", attempt, retries, err, retryInterval)
|
||||
time.Sleep(retryInterval)
|
||||
}
|
||||
}
|
||||
|
||||
log.Fatalf("Magistrala Atom action bootstrap failed after %d attempts: %v", retries, lastErr)
|
||||
}
|
||||
|
||||
func envInt(key string, fallback int) int {
|
||||
raw := strings.TrimSpace(os.Getenv(key))
|
||||
if raw == "" {
|
||||
return fallback
|
||||
}
|
||||
value, err := strconv.Atoi(raw)
|
||||
if err != nil || value <= 0 {
|
||||
return fallback
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func envDuration(key string, fallback time.Duration) time.Duration {
|
||||
raw := strings.TrimSpace(os.Getenv(key))
|
||||
if raw == "" {
|
||||
return fallback
|
||||
}
|
||||
value, err := time.ParseDuration(raw)
|
||||
if err == nil && value > 0 {
|
||||
return value
|
||||
}
|
||||
seconds, err := strconv.Atoi(raw)
|
||||
if err == nil && seconds > 0 {
|
||||
return time.Duration(seconds) * time.Second
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "invalid %s=%q, using %s\n", key, raw, fallback)
|
||||
return fallback
|
||||
}
|
||||
+66
-14
@@ -113,6 +113,27 @@ services:
|
||||
networks:
|
||||
- magistrala-base-net
|
||||
|
||||
atom-bootstrap:
|
||||
image: ghcr.io/absmach/magistrala/atom-bootstrap:${MG_RELEASE_TAG}
|
||||
container_name: magistrala-atom-bootstrap
|
||||
restart: on-failure
|
||||
depends_on:
|
||||
- atom
|
||||
environment:
|
||||
ATOM_URL: ${ATOM_URL}
|
||||
ATOM_SERVICE_TOKEN: ${ATOM_SERVICE_TOKEN}
|
||||
ATOM_SERVICE_USERNAME: ${ATOM_SERVICE_USERNAME}
|
||||
ATOM_SERVICE_SECRET: ${ATOM_SERVICE_SECRET}
|
||||
ATOM_ADMIN_TOKEN: ${ATOM_ADMIN_TOKEN}
|
||||
ATOM_ADMIN_USERNAME: ${ATOM_ADMIN_USERNAME}
|
||||
ATOM_ADMIN_SECRET: ${ATOM_ADMIN_SECRET}
|
||||
ATOM_TIMEOUT: ${ATOM_TIMEOUT}
|
||||
MG_ATOM_BOOTSTRAP_RETRIES: ${MG_ATOM_BOOTSTRAP_RETRIES:-30}
|
||||
MG_ATOM_BOOTSTRAP_RETRY_INTERVAL: ${MG_ATOM_BOOTSTRAP_RETRY_INTERVAL:-2s}
|
||||
MG_ATOM_BOOTSTRAP_TIMEOUT: ${MG_ATOM_BOOTSTRAP_TIMEOUT:-30s}
|
||||
networks:
|
||||
- magistrala-base-net
|
||||
|
||||
domains-db:
|
||||
image: docker.io/postgres:18.0-alpine3.22
|
||||
container_name: magistrala-domains-db
|
||||
@@ -323,9 +344,12 @@ services:
|
||||
image: ghcr.io/absmach/magistrala/journal:${MG_RELEASE_TAG}
|
||||
container_name: magistrala-journal
|
||||
depends_on:
|
||||
- journal-db
|
||||
- atom
|
||||
- nginx
|
||||
journal-db:
|
||||
condition: service_started
|
||||
atom-bootstrap:
|
||||
condition: service_completed_successfully
|
||||
nginx:
|
||||
condition: service_started
|
||||
restart: on-failure
|
||||
environment:
|
||||
MG_JOURNAL_LOG_LEVEL: ${MG_JOURNAL_LOG_LEVEL}
|
||||
@@ -495,9 +519,14 @@ services:
|
||||
container_name: magistrala-clients
|
||||
profiles: ["legacy-core"]
|
||||
depends_on:
|
||||
- clients-db
|
||||
- users
|
||||
- nginx
|
||||
clients-db:
|
||||
condition: service_started
|
||||
users:
|
||||
condition: service_started
|
||||
atom-bootstrap:
|
||||
condition: service_completed_successfully
|
||||
nginx:
|
||||
condition: service_started
|
||||
restart: on-failure
|
||||
environment:
|
||||
MG_CLIENTS_LOG_LEVEL: ${MG_CLIENTS_LOG_LEVEL}
|
||||
@@ -1011,7 +1040,10 @@ services:
|
||||
image: ghcr.io/absmach/magistrala/notifications:${MG_RELEASE_TAG}
|
||||
container_name: magistrala-notifications
|
||||
depends_on:
|
||||
- nginx
|
||||
atom-bootstrap:
|
||||
condition: service_completed_successfully
|
||||
nginx:
|
||||
condition: service_started
|
||||
restart: on-failure
|
||||
environment:
|
||||
MG_NOTIFICATIONS_LOG_LEVEL: ${MG_NOTIFICATIONS_LOG_LEVEL}
|
||||
@@ -1294,6 +1326,9 @@ services:
|
||||
fluxmq-auth:
|
||||
image: ghcr.io/absmach/magistrala/fluxmq:${MG_RELEASE_TAG}
|
||||
container_name: magistrala-fluxmq-auth
|
||||
depends_on:
|
||||
atom-bootstrap:
|
||||
condition: service_completed_successfully
|
||||
restart: on-failure
|
||||
environment:
|
||||
MG_FLUXMQ_LOG_LEVEL: ${MG_FLUXMQ_LOG_LEVEL}
|
||||
@@ -1438,6 +1473,8 @@ services:
|
||||
MG_JAEGER_URL: ${MG_JAEGER_URL}
|
||||
MG_JAEGER_TRACE_RATIO: ${MG_JAEGER_TRACE_RATIO}
|
||||
depends_on:
|
||||
atom-bootstrap:
|
||||
condition: service_completed_successfully
|
||||
ui-backend-db:
|
||||
condition: service_healthy
|
||||
seaweedfs-s3:
|
||||
@@ -1578,7 +1615,10 @@ services:
|
||||
image: ghcr.io/absmach/magistrala/timescale-reader:${MG_RELEASE_TAG}
|
||||
container_name: magistrala-timescale-reader
|
||||
depends_on:
|
||||
- timescale
|
||||
timescale:
|
||||
condition: service_started
|
||||
atom-bootstrap:
|
||||
condition: service_completed_successfully
|
||||
restart: on-failure
|
||||
environment:
|
||||
MG_TIMESCALE_READER_LOG_LEVEL: ${MG_TIMESCALE_READER_LOG_LEVEL}
|
||||
@@ -1709,8 +1749,12 @@ services:
|
||||
image: ghcr.io/absmach/magistrala/re:${MG_RELEASE_TAG}
|
||||
container_name: magistrala-re
|
||||
depends_on:
|
||||
- re-db
|
||||
- nginx
|
||||
re-db:
|
||||
condition: service_started
|
||||
atom-bootstrap:
|
||||
condition: service_completed_successfully
|
||||
nginx:
|
||||
condition: service_started
|
||||
restart: on-failure
|
||||
environment:
|
||||
MG_RE_LOG_LEVEL: ${MG_RE_LOG_LEVEL}
|
||||
@@ -1794,8 +1838,12 @@ services:
|
||||
image: ghcr.io/absmach/magistrala/alarms:${MG_RELEASE_TAG}
|
||||
container_name: magistrala-alarms
|
||||
depends_on:
|
||||
- alarms-db
|
||||
- nginx
|
||||
alarms-db:
|
||||
condition: service_started
|
||||
atom-bootstrap:
|
||||
condition: service_completed_successfully
|
||||
nginx:
|
||||
condition: service_started
|
||||
restart: on-failure
|
||||
environment:
|
||||
MG_ALARMS_LOG_LEVEL: ${MG_ALARMS_LOG_LEVEL}
|
||||
@@ -1858,8 +1906,12 @@ services:
|
||||
image: ghcr.io/absmach/magistrala/reports:${MG_RELEASE_TAG}
|
||||
container_name: magistrala-reports
|
||||
depends_on:
|
||||
- reports-db
|
||||
- nginx
|
||||
reports-db:
|
||||
condition: service_started
|
||||
atom-bootstrap:
|
||||
condition: service_completed_successfully
|
||||
nginx:
|
||||
condition: service_started
|
||||
restart: on-failure
|
||||
environment:
|
||||
MG_REPORTS_LOG_LEVEL: ${MG_REPORTS_LOG_LEVEL}
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package atom
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var magistralaActionDescriptions = map[string]string{
|
||||
"read": "Read / view an object",
|
||||
"write": "Create or update an object",
|
||||
"delete": "Delete an object",
|
||||
"manage": "Full administrative control",
|
||||
"publish": "Publish messages to a channel",
|
||||
"subscribe": "Subscribe to channel messages",
|
||||
"execute": "Execute a command or action",
|
||||
}
|
||||
|
||||
var magistralaActionApplicability = []CapabilityApplicabilitySpec{
|
||||
{ActionName: "read", ObjectKind: "resource", ObjectType: "resource:channel"},
|
||||
{ActionName: "write", ObjectKind: "resource", ObjectType: "resource:channel"},
|
||||
{ActionName: "delete", ObjectKind: "resource", ObjectType: "resource:channel"},
|
||||
{ActionName: "manage", ObjectKind: "resource", ObjectType: "resource:channel"},
|
||||
{ActionName: "publish", ObjectKind: "resource", ObjectType: "resource:channel"},
|
||||
{ActionName: "subscribe", ObjectKind: "resource", ObjectType: "resource:channel"},
|
||||
|
||||
{ActionName: "read", ObjectKind: "resource", ObjectType: "resource:rule"},
|
||||
{ActionName: "write", ObjectKind: "resource", ObjectType: "resource:rule"},
|
||||
{ActionName: "delete", ObjectKind: "resource", ObjectType: "resource:rule"},
|
||||
{ActionName: "manage", ObjectKind: "resource", ObjectType: "resource:rule"},
|
||||
{ActionName: "execute", ObjectKind: "resource", ObjectType: "resource:rule"},
|
||||
|
||||
{ActionName: "read", ObjectKind: "resource", ObjectType: "resource:report"},
|
||||
{ActionName: "write", ObjectKind: "resource", ObjectType: "resource:report"},
|
||||
{ActionName: "delete", ObjectKind: "resource", ObjectType: "resource:report"},
|
||||
{ActionName: "manage", ObjectKind: "resource", ObjectType: "resource:report"},
|
||||
{ActionName: "execute", ObjectKind: "resource", ObjectType: "resource:report"},
|
||||
|
||||
{ActionName: "read", ObjectKind: "resource", ObjectType: "resource:alarm"},
|
||||
{ActionName: "write", ObjectKind: "resource", ObjectType: "resource:alarm"},
|
||||
{ActionName: "delete", ObjectKind: "resource", ObjectType: "resource:alarm"},
|
||||
{ActionName: "manage", ObjectKind: "resource", ObjectType: "resource:alarm"},
|
||||
}
|
||||
|
||||
var magistralaActionAssignmentRules = []ActionAssignmentRuleSpec{
|
||||
{
|
||||
EntityKind: "device",
|
||||
ActionName: "publish",
|
||||
ObjectKind: "resource",
|
||||
ObjectType: "resource:channel",
|
||||
Decision: "allow",
|
||||
},
|
||||
{
|
||||
EntityKind: "device",
|
||||
ActionName: "subscribe",
|
||||
ObjectKind: "resource",
|
||||
ObjectType: "resource:channel",
|
||||
Decision: "allow",
|
||||
},
|
||||
}
|
||||
|
||||
// BootstrapMagistralaActions installs Magistrala-specific action applicability in Atom.
|
||||
// It is safe to call repeatedly during startup.
|
||||
func BootstrapMagistralaActions(ctx context.Context, client *Client) error {
|
||||
if client == nil {
|
||||
return fmt.Errorf("atom client is nil")
|
||||
}
|
||||
capabilities, err := client.ListCapabilities(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("list atom actions: %w", err)
|
||||
}
|
||||
byName := map[string]Capability{}
|
||||
for _, capability := range capabilities.Items {
|
||||
byName[capability.Name] = capability
|
||||
}
|
||||
|
||||
for _, spec := range magistralaActionApplicability {
|
||||
capability, ok := byName[spec.ActionName]
|
||||
if !ok {
|
||||
description := spec.Description
|
||||
if description == "" {
|
||||
description = magistralaActionDescriptions[spec.ActionName]
|
||||
}
|
||||
capability, err = client.CreateCapability(ctx, spec.ActionName, description)
|
||||
if err != nil {
|
||||
if !IsConflict(err) {
|
||||
return fmt.Errorf("create atom action %q: %w", spec.ActionName, err)
|
||||
}
|
||||
id, lookupErr := client.CapabilityID(ctx, spec.ActionName)
|
||||
if lookupErr != nil {
|
||||
return fmt.Errorf("lookup existing atom action %q after conflict: %w", spec.ActionName, lookupErr)
|
||||
}
|
||||
capability = Capability{ID: id, Name: spec.ActionName, Description: description}
|
||||
}
|
||||
byName[spec.ActionName] = capability
|
||||
}
|
||||
if _, err := client.AddCapabilityApplicability(ctx, capability.ID, spec.ObjectKind, spec.ObjectType); err != nil {
|
||||
return fmt.Errorf("add atom applicability %s -> %s:%s: %w", spec.ActionName, spec.ObjectKind, spec.ObjectType, err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, spec := range magistralaActionAssignmentRules {
|
||||
if err := ensureActionAssignmentRule(ctx, client, spec); err != nil {
|
||||
return fmt.Errorf("ensure atom assignment guardrail %s %s %s:%s: %w", spec.EntityKind, spec.ActionName, spec.ObjectKind, spec.ObjectType, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureActionAssignmentRule(ctx context.Context, client *Client, spec ActionAssignmentRuleSpec) error {
|
||||
rules, err := client.ListActionAssignmentRules(ctx, spec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if actionAssignmentRuleExists(rules.Items, spec) {
|
||||
return nil
|
||||
}
|
||||
if _, err := client.CreateActionAssignmentRule(ctx, spec); err != nil {
|
||||
if !IsConflict(err) {
|
||||
return err
|
||||
}
|
||||
rules, lookupErr := client.ListActionAssignmentRules(ctx, spec)
|
||||
if lookupErr != nil {
|
||||
return fmt.Errorf("lookup existing rule after conflict: %w", lookupErr)
|
||||
}
|
||||
if actionAssignmentRuleExists(rules.Items, spec) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func actionAssignmentRuleExists(rules []ActionAssignmentRule, spec ActionAssignmentRuleSpec) bool {
|
||||
for _, rule := range rules {
|
||||
if rule.TenantID == spec.TenantID &&
|
||||
rule.EntityKind == spec.EntityKind &&
|
||||
rule.ActionName == spec.ActionName &&
|
||||
rule.ObjectKind == spec.ObjectKind &&
|
||||
rule.ObjectType == spec.ObjectType &&
|
||||
rule.Decision == spec.Decision &&
|
||||
rule.IsAbsolute == spec.IsAbsolute {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
// Copyright (c) Abstract Machines
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package atom
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestBootstrapMagistralaActionsCreatesMissingActionsAndApplicability(t *testing.T) {
|
||||
actions := map[string]Capability{
|
||||
"read": {ID: "read-id", Name: "read"},
|
||||
"write": {ID: "write-id", Name: "write"},
|
||||
"delete": {ID: "delete-id", Name: "delete"},
|
||||
"manage": {ID: "manage-id", Name: "manage"},
|
||||
}
|
||||
var applicability []map[string]any
|
||||
var assignmentRules []map[string]any
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost || r.URL.Path != "/graphql" {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
var payload struct {
|
||||
Query string `json:"query"`
|
||||
Variables map[string]any `json:"variables"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
t.Fatalf("decode request: %v", err)
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.Contains(payload.Query, "query Actions"):
|
||||
items := make([]Capability, 0, len(actions))
|
||||
for _, action := range actions {
|
||||
items = append(items, action)
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"data": map[string]any{
|
||||
"actions": map[string]any{"items": items, "total": len(items)},
|
||||
},
|
||||
})
|
||||
case strings.Contains(payload.Query, "query ActionAssignmentRules"):
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"data": map[string]any{
|
||||
"actionAssignmentRules": map[string]any{"items": []map[string]any{}, "total": 0},
|
||||
},
|
||||
})
|
||||
case strings.Contains(payload.Query, "createActionAssignmentRule"):
|
||||
input := payload.Variables["input"].(map[string]any)
|
||||
assignmentRules = append(assignmentRules, input)
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"data": map[string]any{
|
||||
"createActionAssignmentRule": map[string]any{
|
||||
"id": input["actionName"].(string) + "-rule-id",
|
||||
"tenant_id": "",
|
||||
"entity_kind": input["entityKind"],
|
||||
"action_name": input["actionName"],
|
||||
"object_kind": input["objectKind"],
|
||||
"object_type": input["objectType"],
|
||||
"decision": input["decision"],
|
||||
"is_absolute": input["isAbsolute"],
|
||||
"created_at": "2026-06-18T00:00:00Z",
|
||||
},
|
||||
},
|
||||
})
|
||||
case strings.Contains(payload.Query, "createAction"):
|
||||
input := payload.Variables["input"].(map[string]any)
|
||||
name := input["name"].(string)
|
||||
action := Capability{ID: name + "-id", Name: name}
|
||||
actions[name] = action
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"data": map[string]any{"createAction": action},
|
||||
})
|
||||
case strings.Contains(payload.Query, "addActionApplicability"):
|
||||
input := payload.Variables["input"].(map[string]any)
|
||||
applicability = append(applicability, input)
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"data": map[string]any{
|
||||
"addActionApplicability": map[string]any{
|
||||
"action_id": input["actionId"],
|
||||
"action_name": "action",
|
||||
"object_kind": input["objectKind"],
|
||||
"object_type": input["objectType"],
|
||||
"description": "",
|
||||
},
|
||||
},
|
||||
})
|
||||
default:
|
||||
t.Fatalf("unexpected GraphQL payload: %s", payload.Query)
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
client := NewClient(Config{URL: srv.URL, Timeout: time.Second})
|
||||
if err := BootstrapMagistralaActions(context.Background(), client); err != nil {
|
||||
t.Fatalf("bootstrap failed: %v", err)
|
||||
}
|
||||
|
||||
for _, name := range []string{"read", "write", "delete", "manage", "publish", "subscribe", "execute"} {
|
||||
if _, ok := actions[name]; !ok {
|
||||
t.Fatalf("action %q was not ensured", name)
|
||||
}
|
||||
}
|
||||
if len(applicability) != len(magistralaActionApplicability) {
|
||||
t.Fatalf("unexpected applicability count: got %d want %d", len(applicability), len(magistralaActionApplicability))
|
||||
}
|
||||
assertApplicability(t, applicability, "publish-id", "resource:channel")
|
||||
assertApplicability(t, applicability, "execute-id", "resource:rule")
|
||||
assertApplicability(t, applicability, "execute-id", "resource:report")
|
||||
assertApplicability(t, applicability, "manage-id", "resource:alarm")
|
||||
if len(assignmentRules) != len(magistralaActionAssignmentRules) {
|
||||
t.Fatalf("unexpected assignment guardrail count: got %d want %d", len(assignmentRules), len(magistralaActionAssignmentRules))
|
||||
}
|
||||
assertAssignmentRule(t, assignmentRules, "device", "publish", "resource", "resource:channel", "allow")
|
||||
assertAssignmentRule(t, assignmentRules, "device", "subscribe", "resource", "resource:channel", "allow")
|
||||
}
|
||||
|
||||
func assertApplicability(t *testing.T, entries []map[string]any, actionID, objectType string) {
|
||||
t.Helper()
|
||||
for _, entry := range entries {
|
||||
if entry["actionId"] == actionID && entry["objectKind"] == "resource" && entry["objectType"] == objectType {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatalf("missing applicability action=%s object_type=%s", actionID, objectType)
|
||||
}
|
||||
|
||||
func assertAssignmentRule(t *testing.T, entries []map[string]any, entityKind, actionName, objectKind, objectType, decision string) {
|
||||
t.Helper()
|
||||
for _, entry := range entries {
|
||||
if entry["entityKind"] == entityKind &&
|
||||
entry["actionName"] == actionName &&
|
||||
entry["objectKind"] == objectKind &&
|
||||
entry["objectType"] == objectType &&
|
||||
entry["decision"] == decision &&
|
||||
entry["isAbsolute"] == false {
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatalf("missing assignment guardrail entity=%s action=%s object=%s:%s decision=%s", entityKind, actionName, objectKind, objectType, decision)
|
||||
}
|
||||
+113
-3
@@ -267,9 +267,9 @@ func (c *Client) ListCapabilities(ctx context.Context) (CapabilityList, error) {
|
||||
var out struct {
|
||||
Actions CapabilityList `json:"actions"`
|
||||
}
|
||||
err := c.graphQL(ctx, `query Actions {
|
||||
actions { items { id name description } }
|
||||
}`, nil, &out)
|
||||
err := c.graphQL(ctx, `query Actions($limit: Int!) {
|
||||
actions(limit: $limit) { total items { id name description } }
|
||||
}`, map[string]any{"limit": 100}, &out)
|
||||
return out.Actions, err
|
||||
}
|
||||
|
||||
@@ -286,6 +286,116 @@ func (c *Client) CapabilityID(ctx context.Context, name string) (string, error)
|
||||
return "", Error{StatusCode: http.StatusNotFound, Message: "capability " + name + " not found"}
|
||||
}
|
||||
|
||||
func (c *Client) CreateCapability(ctx context.Context, name, description string) (Capability, error) {
|
||||
var out struct {
|
||||
CreateAction Capability `json:"createAction"`
|
||||
}
|
||||
input := map[string]any{"name": name}
|
||||
setIfNotEmpty(input, "description", description)
|
||||
err := c.graphQL(ctx, `mutation CreateAction($input: CreateActionInput!) {
|
||||
createAction(input: $input) { id name description }
|
||||
}`, map[string]any{"input": input}, &out)
|
||||
return out.CreateAction, err
|
||||
}
|
||||
|
||||
func (c *Client) AddCapabilityApplicability(ctx context.Context, actionID, objectKind, objectType string) (CapabilityApplicability, error) {
|
||||
var out struct {
|
||||
AddActionApplicability CapabilityApplicability `json:"addActionApplicability"`
|
||||
}
|
||||
input := map[string]any{
|
||||
"actionId": actionID,
|
||||
"objectKind": objectKind,
|
||||
}
|
||||
setIfNotEmpty(input, "objectType", objectType)
|
||||
err := c.graphQL(ctx, `mutation AddActionApplicability($input: AddActionApplicabilityInput!) {
|
||||
addActionApplicability(input: $input) {
|
||||
action_id: actionId
|
||||
action_name: actionName
|
||||
description
|
||||
object_kind: objectKind
|
||||
object_type: objectType
|
||||
}
|
||||
}`, map[string]any{"input": input}, &out)
|
||||
return out.AddActionApplicability, err
|
||||
}
|
||||
|
||||
func (c *Client) ListActionAssignmentRules(ctx context.Context, spec ActionAssignmentRuleSpec) (ActionAssignmentRuleList, error) {
|
||||
var out struct {
|
||||
ActionAssignmentRules ActionAssignmentRuleList `json:"actionAssignmentRules"`
|
||||
}
|
||||
vars := map[string]any{"limit": 100, "offset": 0}
|
||||
setIfNotEmpty(vars, "tenantId", spec.TenantID)
|
||||
setIfNotEmpty(vars, "entityKind", spec.EntityKind)
|
||||
setIfNotEmpty(vars, "actionName", spec.ActionName)
|
||||
setIfNotEmpty(vars, "objectKind", spec.ObjectKind)
|
||||
setIfNotEmpty(vars, "objectType", spec.ObjectType)
|
||||
setIfNotEmpty(vars, "decision", spec.Decision)
|
||||
err := c.graphQL(ctx, `query ActionAssignmentRules(
|
||||
$tenantId: ID,
|
||||
$entityKind: EntityKind,
|
||||
$actionName: String,
|
||||
$objectKind: String,
|
||||
$objectType: String,
|
||||
$decision: ActionAssignmentRuleDecision,
|
||||
$limit: Int!,
|
||||
$offset: Int!
|
||||
) {
|
||||
actionAssignmentRules(
|
||||
tenantId: $tenantId,
|
||||
entityKind: $entityKind,
|
||||
actionName: $actionName,
|
||||
objectKind: $objectKind,
|
||||
objectType: $objectType,
|
||||
decision: $decision,
|
||||
limit: $limit,
|
||||
offset: $offset
|
||||
) {
|
||||
total
|
||||
items {
|
||||
id
|
||||
tenant_id: tenantId
|
||||
entity_kind: entityKind
|
||||
action_name: actionName
|
||||
object_kind: objectKind
|
||||
object_type: objectType
|
||||
decision
|
||||
is_absolute: isAbsolute
|
||||
created_at: createdAt
|
||||
}
|
||||
}
|
||||
}`, vars, &out)
|
||||
return out.ActionAssignmentRules, err
|
||||
}
|
||||
|
||||
func (c *Client) CreateActionAssignmentRule(ctx context.Context, spec ActionAssignmentRuleSpec) (ActionAssignmentRule, error) {
|
||||
var out struct {
|
||||
CreateActionAssignmentRule ActionAssignmentRule `json:"createActionAssignmentRule"`
|
||||
}
|
||||
input := map[string]any{
|
||||
"entityKind": spec.EntityKind,
|
||||
"actionName": spec.ActionName,
|
||||
"objectKind": spec.ObjectKind,
|
||||
"decision": spec.Decision,
|
||||
"isAbsolute": spec.IsAbsolute,
|
||||
}
|
||||
setIfNotEmpty(input, "tenantId", spec.TenantID)
|
||||
setIfNotEmpty(input, "objectType", spec.ObjectType)
|
||||
err := c.graphQL(ctx, `mutation CreateActionAssignmentRule($input: CreateActionAssignmentRuleInput!) {
|
||||
createActionAssignmentRule(input: $input) {
|
||||
id
|
||||
tenant_id: tenantId
|
||||
entity_kind: entityKind
|
||||
action_name: actionName
|
||||
object_kind: objectKind
|
||||
object_type: objectType
|
||||
decision
|
||||
is_absolute: isAbsolute
|
||||
created_at: createdAt
|
||||
}
|
||||
}`, map[string]any{"input": input}, &out)
|
||||
return out.CreateActionAssignmentRule, err
|
||||
}
|
||||
|
||||
func (c *Client) CreatePermissionBlock(ctx context.Context, block CreatePermissionBlock) (PermissionBlock, error) {
|
||||
var out struct {
|
||||
CreatePermissionBlock PermissionBlock `json:"createPermissionBlock"`
|
||||
|
||||
@@ -88,6 +88,49 @@ type Capability struct {
|
||||
|
||||
type CapabilityList struct {
|
||||
Items []Capability `json:"items"`
|
||||
Total int64 `json:"total,omitempty"`
|
||||
}
|
||||
|
||||
type CapabilityApplicability struct {
|
||||
ActionID string `json:"action_id"`
|
||||
ActionName string `json:"action_name"`
|
||||
Description string `json:"description,omitempty"`
|
||||
ObjectKind string `json:"object_kind"`
|
||||
ObjectType string `json:"object_type,omitempty"`
|
||||
}
|
||||
|
||||
type CapabilityApplicabilitySpec struct {
|
||||
ActionName string
|
||||
Description string
|
||||
ObjectKind string
|
||||
ObjectType string
|
||||
}
|
||||
|
||||
type ActionAssignmentRule struct {
|
||||
ID string `json:"id"`
|
||||
TenantID string `json:"tenant_id,omitempty"`
|
||||
EntityKind string `json:"entity_kind"`
|
||||
ActionName string `json:"action_name"`
|
||||
ObjectKind string `json:"object_kind"`
|
||||
ObjectType string `json:"object_type,omitempty"`
|
||||
Decision string `json:"decision"`
|
||||
IsAbsolute bool `json:"is_absolute"`
|
||||
CreatedAt string `json:"created_at,omitempty"`
|
||||
}
|
||||
|
||||
type ActionAssignmentRuleList struct {
|
||||
Items []ActionAssignmentRule `json:"items"`
|
||||
Total int64 `json:"total,omitempty"`
|
||||
}
|
||||
|
||||
type ActionAssignmentRuleSpec struct {
|
||||
TenantID string
|
||||
EntityKind string
|
||||
ActionName string
|
||||
ObjectKind string
|
||||
ObjectType string
|
||||
Decision string
|
||||
IsAbsolute bool
|
||||
}
|
||||
|
||||
type PermissionBlock struct {
|
||||
|
||||
Reference in New Issue
Block a user