add atom bootstrap

Signed-off-by: Arvindh <arvindh91@gmail.com>
This commit is contained in:
Arvindh
2026-06-19 15:44:24 +05:30
parent 69a280ff0f
commit 7422d6f47f
7 changed files with 604 additions and 18 deletions
+1 -1
View File
@@ -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))
+84
View File
@@ -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
View File
@@ -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}
+149
View File
@@ -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
}
+148
View File
@@ -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
View File
@@ -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"`
+43
View File
@@ -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 {