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>
995 lines
33 KiB
Go
995 lines
33 KiB
Go
// Copyright (c) Abstract Machines
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package atom
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
)
|
|
|
|
type Client struct {
|
|
baseURL string
|
|
token string
|
|
adminUsername string
|
|
adminSecret string
|
|
userAgent string
|
|
httpClient *http.Client
|
|
}
|
|
|
|
func NewClient(cfg Config) *Client {
|
|
timeout := cfg.Timeout
|
|
if timeout == 0 {
|
|
timeout = defaultTimeout
|
|
}
|
|
return &Client{
|
|
baseURL: strings.TrimRight(cfg.URL, "/"),
|
|
token: cfg.Token,
|
|
adminUsername: cfg.AdminUsername,
|
|
adminSecret: cfg.AdminSecret,
|
|
userAgent: cfg.UserAgent,
|
|
httpClient: &http.Client{
|
|
Timeout: timeout,
|
|
},
|
|
}
|
|
}
|
|
|
|
func (c *Client) UpsertTenant(ctx context.Context, tenant Tenant) error {
|
|
if tenant.ID == "" {
|
|
_, err := c.CreateTenant(ctx, tenant)
|
|
return err
|
|
}
|
|
if _, err := c.CreateTenant(ctx, tenant); err == nil || !IsConflict(err) {
|
|
return err
|
|
}
|
|
_, err := c.UpdateTenant(ctx, tenant.ID, tenant)
|
|
return err
|
|
}
|
|
|
|
func (c *Client) CreateTenant(ctx context.Context, tenant Tenant) (Tenant, error) {
|
|
var out struct {
|
|
CreateTenant Tenant `json:"createTenant"`
|
|
}
|
|
err := c.graphQL(ctx, `mutation CreateTenant($input: CreateTenantInput!) {
|
|
createTenant(input: $input) { id name route: alias status tags attributes created_by: createdBy updated_by: updatedBy created_at: createdAt updated_at: updatedAt }
|
|
}`, map[string]any{atomInputKeyInput: tenantCreateInput(tenant)}, &out)
|
|
return out.CreateTenant, err
|
|
}
|
|
|
|
func (c *Client) GetTenant(ctx context.Context, id string) (Tenant, error) {
|
|
var out struct {
|
|
Tenant Tenant `json:"tenant"`
|
|
}
|
|
err := c.graphQL(ctx, `query Tenant($id: ID!) {
|
|
tenant(id: $id) { id name route: alias status tags attributes created_by: createdBy updated_by: updatedBy created_at: createdAt updated_at: updatedAt }
|
|
}`, map[string]any{"id": id}, &out)
|
|
return out.Tenant, err
|
|
}
|
|
|
|
func (c *Client) UpdateTenant(ctx context.Context, id string, tenant Tenant) (Tenant, error) {
|
|
var out struct {
|
|
UpdateTenant Tenant `json:"updateTenant"`
|
|
}
|
|
err := c.graphQL(ctx, `mutation UpdateTenant($id: ID!, $input: UpdateTenantInput!) {
|
|
updateTenant(id: $id, input: $input) { id name route: alias status tags attributes created_by: createdBy updated_by: updatedBy created_at: createdAt updated_at: updatedAt }
|
|
}`, map[string]any{"id": id, atomInputKeyInput: tenantUpdateInput(tenant)}, &out)
|
|
return out.UpdateTenant, err
|
|
}
|
|
|
|
func (c *Client) ChangeTenantStatus(ctx context.Context, id, action string) (Tenant, error) {
|
|
field := map[string]string{
|
|
"enable": "enableTenant",
|
|
"disable": "disableTenant",
|
|
"freeze": "freezeTenant",
|
|
}[action]
|
|
if field == "" {
|
|
return Tenant{}, Error{StatusCode: http.StatusBadRequest, Message: "unsupported tenant status action: " + action}
|
|
}
|
|
var out map[string]Tenant
|
|
err := c.graphQL(ctx, fmt.Sprintf(`mutation ChangeTenantStatus($id: ID!) {
|
|
%s(id: $id) { id name route: alias status tags attributes created_by: createdBy updated_by: updatedBy created_at: createdAt updated_at: updatedAt }
|
|
}`, field), map[string]any{"id": id}, &out)
|
|
if err != nil {
|
|
return Tenant{}, err
|
|
}
|
|
return out[field], nil
|
|
}
|
|
|
|
func (c *Client) UpsertEntity(ctx context.Context, entity Entity) error {
|
|
if entity.ID == "" {
|
|
_, err := c.CreateEntity(ctx, entity)
|
|
return err
|
|
}
|
|
if _, err := c.CreateEntity(ctx, entity); err == nil || !IsConflict(err) {
|
|
return err
|
|
}
|
|
_, err := c.UpdateEntity(ctx, entity.ID, entity)
|
|
return err
|
|
}
|
|
|
|
func (c *Client) CreateEntity(ctx context.Context, entity Entity) (Entity, error) {
|
|
var out struct {
|
|
CreateEntity Entity `json:"createEntity"`
|
|
}
|
|
err := c.graphQL(ctx, `mutation CreateEntity($input: CreateEntityInput!) {
|
|
createEntity(input: $input) { id kind name tenant_id: tenantId status attributes created_at: createdAt updated_at: updatedAt }
|
|
}`, map[string]any{atomInputKeyInput: entityCreateInput(entity)}, &out)
|
|
return out.CreateEntity, err
|
|
}
|
|
|
|
func (c *Client) GetEntity(ctx context.Context, id string) (Entity, error) {
|
|
var out struct {
|
|
Entity Entity `json:"entity"`
|
|
}
|
|
err := c.graphQL(ctx, `query Entity($id: ID!) {
|
|
entity(id: $id) { id kind name tenant_id: tenantId status attributes created_at: createdAt updated_at: updatedAt }
|
|
}`, map[string]any{"id": id}, &out)
|
|
return out.Entity, err
|
|
}
|
|
|
|
func (c *Client) UpdateEntity(ctx context.Context, id string, entity Entity) (Entity, error) {
|
|
var out struct {
|
|
UpdateEntity Entity `json:"updateEntity"`
|
|
}
|
|
err := c.graphQL(ctx, `mutation UpdateEntity($id: ID!, $input: UpdateEntityInput!) {
|
|
updateEntity(id: $id, input: $input) { id kind name tenant_id: tenantId status attributes created_at: createdAt updated_at: updatedAt }
|
|
}`, map[string]any{"id": id, atomInputKeyInput: entityUpdateInput(entity)}, &out)
|
|
return out.UpdateEntity, err
|
|
}
|
|
|
|
func (c *Client) UpsertGroup(ctx context.Context, group Group) error {
|
|
if group.ID == "" {
|
|
_, err := c.CreateGroup(ctx, group)
|
|
return err
|
|
}
|
|
if _, err := c.CreateGroup(ctx, group); err == nil || !IsConflict(err) {
|
|
return err
|
|
}
|
|
_, err := c.UpdateGroup(ctx, group.ID, group)
|
|
return err
|
|
}
|
|
|
|
func (c *Client) CreateGroup(ctx context.Context, group Group) (Group, error) {
|
|
var out struct {
|
|
CreateGroup Group `json:"createGroup"`
|
|
}
|
|
err := c.graphQL(ctx, `mutation CreateGroup($input: CreateGroupInput!) {
|
|
createGroup(input: $input) { id name tenant_id: tenantId description parent_id: parentId status attributes created_at: createdAt updated_at: updatedAt }
|
|
}`, map[string]any{atomInputKeyInput: groupCreateInput(group)}, &out)
|
|
return out.CreateGroup, err
|
|
}
|
|
|
|
func (c *Client) GetGroup(ctx context.Context, id string) (Group, error) {
|
|
var out struct {
|
|
Group Group `json:"group"`
|
|
}
|
|
err := c.graphQL(ctx, `query Group($id: ID!) {
|
|
group(id: $id) { id name tenant_id: tenantId description parent_id: parentId status attributes created_at: createdAt updated_at: updatedAt }
|
|
}`, map[string]any{"id": id}, &out)
|
|
return out.Group, err
|
|
}
|
|
|
|
func (c *Client) UpdateGroup(ctx context.Context, id string, group Group) (Group, error) {
|
|
var out struct {
|
|
UpdateGroup Group `json:"updateGroup"`
|
|
}
|
|
err := c.graphQL(ctx, `mutation UpdateGroup($id: ID!, $input: UpdateGroupInput!) {
|
|
updateGroup(id: $id, input: $input) { id name tenant_id: tenantId description parent_id: parentId status attributes created_at: createdAt updated_at: updatedAt }
|
|
}`, map[string]any{"id": id, atomInputKeyInput: groupUpdateInput(group)}, &out)
|
|
return out.UpdateGroup, err
|
|
}
|
|
|
|
func (c *Client) UpsertResource(ctx context.Context, resource Resource) error {
|
|
if resource.ID == "" {
|
|
_, err := c.CreateResource(ctx, resource)
|
|
return err
|
|
}
|
|
if _, err := c.CreateResource(ctx, resource); err == nil || !IsConflict(err) {
|
|
return err
|
|
}
|
|
_, err := c.UpdateResource(ctx, resource.ID, resource)
|
|
return err
|
|
}
|
|
|
|
func (c *Client) CreateResource(ctx context.Context, resource Resource) (Resource, error) {
|
|
var out struct {
|
|
CreateResource Resource `json:"createResource"`
|
|
}
|
|
err := c.graphQL(ctx, `mutation CreateResource($input: CreateResourceInput!) {
|
|
createResource(input: $input) { id kind name tenant_id: tenantId owner_id: ownerId attributes created_at: createdAt updated_at: updatedAt }
|
|
}`, map[string]any{atomInputKeyInput: resourceCreateInput(resource)}, &out)
|
|
return out.CreateResource, err
|
|
}
|
|
|
|
func (c *Client) GetResource(ctx context.Context, id string) (Resource, error) {
|
|
var out struct {
|
|
Resource Resource `json:"resource"`
|
|
}
|
|
err := c.graphQL(ctx, `query Resource($id: ID!) {
|
|
resource(id: $id) { id kind name tenant_id: tenantId owner_id: ownerId attributes created_at: createdAt updated_at: updatedAt }
|
|
}`, map[string]any{"id": id}, &out)
|
|
return out.Resource, err
|
|
}
|
|
|
|
func (c *Client) UpdateResource(ctx context.Context, id string, resource Resource) (Resource, error) {
|
|
var out struct {
|
|
UpdateResource Resource `json:"updateResource"`
|
|
}
|
|
err := c.graphQL(ctx, `mutation UpdateResource($id: ID!, $input: UpdateResourceInput!) {
|
|
updateResource(id: $id, input: $input) { id kind name tenant_id: tenantId owner_id: ownerId attributes created_at: createdAt updated_at: updatedAt }
|
|
}`, map[string]any{"id": id, atomInputKeyInput: resourceUpdateInput(resource)}, &out)
|
|
return out.UpdateResource, err
|
|
}
|
|
|
|
func (c *Client) DeleteTenant(ctx context.Context, id string) error {
|
|
return c.graphQL(ctx, `mutation DeleteTenant($id: ID!) { deleteTenant(id: $id) }`, map[string]any{"id": id}, nil)
|
|
}
|
|
|
|
func (c *Client) ListTenants(ctx context.Context, q Query) (TenantList, error) {
|
|
var out struct {
|
|
Tenants TenantList `json:"tenants"`
|
|
}
|
|
err := c.graphQL(ctx, `query Tenants($q: String, $name: String, $alias: String, $status: TenantStatus, $limit: Int, $offset: Int) {
|
|
tenants(q: $q, name: $name, alias: $alias, status: $status, limit: $limit, offset: $offset) {
|
|
total
|
|
items { id name route: alias status tags attributes created_by: createdBy updated_by: updatedBy created_at: createdAt updated_at: updatedAt }
|
|
}
|
|
}`, queryVariables(q), &out)
|
|
return out.Tenants, err
|
|
}
|
|
|
|
func (c *Client) CheckAuthz(ctx context.Context, req AuthzRequest) (AuthzResponse, error) {
|
|
var out struct {
|
|
AuthzCheck AuthzResponse `json:"authzCheck"`
|
|
}
|
|
err := c.graphQL(ctx, `mutation AuthzCheck($input: AuthzCheckInput!) {
|
|
authzCheck(input: $input) { allowed reason }
|
|
}`, map[string]any{atomInputKeyInput: authzInput(req)}, &out)
|
|
return out.AuthzCheck, err
|
|
}
|
|
|
|
func (c *Client) CheckAuthzWithToken(ctx context.Context, token string, req AuthzRequest) (AuthzResponse, error) {
|
|
var out struct {
|
|
AuthzCheck AuthzResponse `json:"authzCheck"`
|
|
}
|
|
err := c.graphQLWithToken(ctx, `mutation AuthzCheck($input: AuthzCheckInput!) {
|
|
authzCheck(input: $input) { allowed reason }
|
|
}`, map[string]any{atomInputKeyInput: authzInput(req)}, &out, token)
|
|
return out.AuthzCheck, err
|
|
}
|
|
|
|
func (c *Client) ListCapabilities(ctx context.Context) (CapabilityList, error) {
|
|
var out struct {
|
|
Actions CapabilityList `json:"actions"`
|
|
}
|
|
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
|
|
}
|
|
|
|
func (c *Client) CapabilityID(ctx context.Context, name string) (string, error) {
|
|
list, err := c.ListCapabilities(ctx)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
for _, capability := range list.Items {
|
|
if capability.Name == name {
|
|
return capability.ID, nil
|
|
}
|
|
}
|
|
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{atomInputKeyName: name}
|
|
setIfNotEmpty(input, "description", description)
|
|
err := c.graphQL(ctx, `mutation CreateAction($input: CreateActionInput!) {
|
|
createAction(input: $input) { id name description }
|
|
}`, map[string]any{atomInputKeyInput: 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,
|
|
atomInputKeyObjectKind: 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{atomInputKeyInput: 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, atomInputKeyObjectKind, 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,
|
|
atomInputKeyObjectKind: 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{atomInputKeyInput: input}, &out)
|
|
return out.CreateActionAssignmentRule, err
|
|
}
|
|
|
|
func (c *Client) CreatePermissionBlock(ctx context.Context, block CreatePermissionBlock) (PermissionBlock, error) {
|
|
var out struct {
|
|
CreatePermissionBlock PermissionBlock `json:"createPermissionBlock"`
|
|
}
|
|
err := c.graphQL(ctx, `mutation CreatePermissionBlock($input: CreatePermissionBlockInput!) {
|
|
createPermissionBlock(input: $input) {
|
|
id tenant_id: tenantId scope_mode: scopeMode object_kind: objectKind object_type: objectType object_id: objectId group_id: groupId effect conditions
|
|
actions { id name description }
|
|
}
|
|
}`, map[string]any{atomInputKeyInput: permissionBlockInput(block)}, &out)
|
|
return out.CreatePermissionBlock, err
|
|
}
|
|
|
|
func (c *Client) CreateDirectPolicy(ctx context.Context, policy CreateDirectPolicy) (DirectPolicy, error) {
|
|
var out struct {
|
|
CreateDirectPolicy DirectPolicy `json:"createDirectPolicy"`
|
|
}
|
|
err := c.graphQL(ctx, `mutation CreateDirectPolicy($input: CreateDirectPolicyInput!) {
|
|
createDirectPolicy(input: $input) {
|
|
id tenant_id: tenantId subject_kind: subjectKind subject_id: subjectId permission_block_id: permissionBlockId created_at: createdAt
|
|
permission_block: permissionBlock {
|
|
id tenant_id: tenantId scope_mode: scopeMode object_kind: objectKind object_type: objectType object_id: objectId group_id: groupId effect conditions
|
|
actions { id name description }
|
|
}
|
|
}
|
|
}`, map[string]any{atomInputKeyInput: directPolicyInput(policy)}, &out)
|
|
return out.CreateDirectPolicy, err
|
|
}
|
|
|
|
func (c *Client) ListDirectPolicies(ctx context.Context, q DirectPolicyQuery) (DirectPolicyList, error) {
|
|
var out struct {
|
|
DirectPolicies DirectPolicyList `json:"directPolicies"`
|
|
}
|
|
err := c.graphQL(ctx, `query DirectPolicies($tenantId: ID, $subjectKind: SubjectKind, $subjectId: ID, $limit: Int, $offset: Int) {
|
|
directPolicies(tenantId: $tenantId, subjectKind: $subjectKind, subjectId: $subjectId, limit: $limit, offset: $offset) {
|
|
total
|
|
items {
|
|
id tenant_id: tenantId subject_kind: subjectKind subject_id: subjectId permission_block_id: permissionBlockId created_at: createdAt
|
|
permission_block: permissionBlock {
|
|
id tenant_id: tenantId scope_mode: scopeMode object_kind: objectKind object_type: objectType object_id: objectId group_id: groupId effect conditions
|
|
actions { id name description }
|
|
}
|
|
}
|
|
}
|
|
}`, directPolicyQueryVariables(q), &out)
|
|
return out.DirectPolicies, err
|
|
}
|
|
|
|
func (c *Client) DeleteDirectPolicy(ctx context.Context, id string) error {
|
|
return c.graphQL(ctx, `mutation DeleteDirectPolicy($id: ID!) { deleteDirectPolicy(id: $id) }`, map[string]any{"id": id}, nil)
|
|
}
|
|
|
|
func (c *Client) AuthorizedObjectIDs(ctx context.Context, q AuthorizedObjectIDsQuery) (AuthorizedObjectIDs, error) {
|
|
var out struct {
|
|
AuthorizedObjectIDs AuthorizedObjectIDs `json:"authorizedObjectIds"`
|
|
}
|
|
err := c.graphQL(ctx, `query AuthorizedObjectIDs($input: AuthorizedObjectIdsInput!) {
|
|
authorizedObjectIds(input: $input) {
|
|
ids
|
|
total
|
|
}
|
|
}`, authorizedObjectIDVariables(q), &out)
|
|
return out.AuthorizedObjectIDs, err
|
|
}
|
|
|
|
func (c *Client) LoginPassword(ctx context.Context, identifier, secret string) (LoginResponse, error) {
|
|
return c.LoginCredential(ctx, identifier, secret, "password")
|
|
}
|
|
|
|
func (c *Client) LoginSharedKey(ctx context.Context, identifier, secret string) (LoginResponse, error) {
|
|
return c.LoginCredential(ctx, identifier, secret, "shared_key")
|
|
}
|
|
|
|
func (c *Client) LoginCredential(ctx context.Context, identifier, secret, kind string) (LoginResponse, error) {
|
|
var out LoginResponse
|
|
err := c.doWithToken(ctx, http.MethodPost, "/auth/login", LoginRequest{
|
|
Identifier: identifier,
|
|
Secret: secret,
|
|
Kind: kind,
|
|
}, &out, "")
|
|
return out, err
|
|
}
|
|
|
|
func (c *Client) Introspect(ctx context.Context, token string) (IntrospectionResponse, error) {
|
|
var out IntrospectionResponse
|
|
err := c.doWithToken(ctx, http.MethodGet, atomAuthIntrospectPath, nil, &out, token)
|
|
return out, err
|
|
}
|
|
|
|
func (c *Client) DeleteEntity(ctx context.Context, id string) error {
|
|
return c.graphQL(ctx, `mutation DeleteEntity($id: ID!) { deleteEntity(id: $id) }`, map[string]any{"id": id}, nil)
|
|
}
|
|
|
|
func (c *Client) CreatePassword(ctx context.Context, entityID, password string) error {
|
|
return c.graphQL(ctx, `mutation CreatePassword($entityId: ID!, $password: String!) {
|
|
createPassword(entityId: $entityId, password: $password)
|
|
}`, map[string]any{atomInputKeyEntityID: entityID, "password": password}, nil)
|
|
}
|
|
|
|
func (c *Client) CreateUnscopedAccessToken(ctx context.Context, entityID, name, description string) (AccessTokenResponse, error) {
|
|
var out struct {
|
|
CreateAccessToken AccessTokenResponse `json:"createAccessToken"`
|
|
}
|
|
err := c.graphQL(ctx, `mutation CreateAccessToken($input: CreateAccessTokenInput!) {
|
|
createAccessToken(input: $input) {
|
|
credentialId
|
|
token
|
|
name
|
|
expiresAt
|
|
}
|
|
}`, map[string]any{
|
|
atomInputKeyInput: map[string]any{
|
|
atomInputKeyName: name,
|
|
"description": description,
|
|
atomInputKeySubjectID: entityID,
|
|
"scoped": false,
|
|
"permissions": []any{},
|
|
},
|
|
}, &out)
|
|
return out.CreateAccessToken, err
|
|
}
|
|
|
|
func (c *Client) CreateSharedKey(ctx context.Context, entityID, key, description string) (SharedKeyResponse, error) {
|
|
var out struct {
|
|
CreateSharedKey SharedKeyResponse `json:"createSharedKey"`
|
|
}
|
|
input := map[string]any{}
|
|
setIfNotEmpty(input, "key", key)
|
|
setIfNotEmpty(input, "description", description)
|
|
err := c.graphQL(ctx, `mutation CreateSharedKey($entityId: ID!, $input: CreateSharedKeyInput!) {
|
|
createSharedKey(entityId: $entityId, input: $input) {
|
|
credentialId
|
|
key
|
|
expiresAt
|
|
}
|
|
}`, map[string]any{
|
|
atomInputKeyEntityID: entityID,
|
|
atomInputKeyInput: input,
|
|
}, &out)
|
|
return out.CreateSharedKey, err
|
|
}
|
|
|
|
func (c *Client) RevealSharedKey(ctx context.Context, entityID, credentialID string) (SharedKeyResponse, error) {
|
|
var out struct {
|
|
RevealSharedKey SharedKeyResponse `json:"revealSharedKey"`
|
|
}
|
|
err := c.graphQL(ctx, `mutation RevealSharedKey($entityId: ID!, $credentialId: ID!) {
|
|
revealSharedKey(entityId: $entityId, credentialId: $credentialId) {
|
|
credentialId
|
|
key
|
|
expiresAt
|
|
}
|
|
}`, map[string]any{
|
|
atomInputKeyEntityID: entityID,
|
|
atomInputKeyCredentialID: credentialID,
|
|
}, &out)
|
|
return out.RevealSharedKey, err
|
|
}
|
|
|
|
func (c *Client) ListCredentials(ctx context.Context, entityID string) (CredentialList, error) {
|
|
var out struct {
|
|
Credentials CredentialList `json:"credentials"`
|
|
}
|
|
err := c.graphQL(ctx, `query Credentials($entityId: ID!) {
|
|
credentials(entityId: $entityId) {
|
|
total
|
|
items {
|
|
id
|
|
entity_id: entityId
|
|
kind
|
|
identifier
|
|
status
|
|
expires_at: expiresAt
|
|
created_at: createdAt
|
|
}
|
|
}
|
|
}`, map[string]any{atomInputKeyEntityID: entityID}, &out)
|
|
return out.Credentials, err
|
|
}
|
|
|
|
func (c *Client) RevokeCredential(ctx context.Context, entityID, credentialID string) error {
|
|
return c.graphQL(ctx, `mutation RevokeCredential($entityId: ID!, $credentialId: ID!) {
|
|
revokeCredential(entityId: $entityId, credentialId: $credentialId)
|
|
}`, map[string]any{atomInputKeyEntityID: entityID, atomInputKeyCredentialID: credentialID}, nil)
|
|
}
|
|
|
|
func (c *Client) ListEntities(ctx context.Context, q Query) (EntityList, error) {
|
|
var out struct {
|
|
Entities EntityList `json:"entities"`
|
|
}
|
|
err := c.graphQL(ctx, `query Entities($q: String, $kind: EntityKind, $tenantId: ID, $status: EntityStatus, $limit: Int, $offset: Int) {
|
|
entities(q: $q, kind: $kind, tenantId: $tenantId, status: $status, limit: $limit, offset: $offset) {
|
|
total
|
|
items { id kind name tenant_id: tenantId status attributes created_at: createdAt updated_at: updatedAt }
|
|
}
|
|
}`, objectQueryVariables(q), &out)
|
|
return out.Entities, err
|
|
}
|
|
|
|
func (c *Client) DeleteGroup(ctx context.Context, id string) error {
|
|
return c.graphQL(ctx, `mutation DeleteGroup($id: ID!) { deleteGroup(id: $id) }`, map[string]any{"id": id}, nil)
|
|
}
|
|
|
|
func (c *Client) ListGroups(ctx context.Context, q Query) (GroupList, error) {
|
|
var out struct {
|
|
Groups GroupList `json:"groups"`
|
|
}
|
|
err := c.graphQL(ctx, `query Groups($q: String, $tenantId: ID, $status: EntityStatus, $limit: Int, $offset: Int) {
|
|
groups(q: $q, tenantId: $tenantId, status: $status, limit: $limit, offset: $offset) {
|
|
total
|
|
items { id name tenant_id: tenantId description parent_id: parentId status attributes created_at: createdAt updated_at: updatedAt }
|
|
}
|
|
}`, objectQueryVariables(q), &out)
|
|
return out.Groups, err
|
|
}
|
|
|
|
func (c *Client) DeleteResource(ctx context.Context, id string) error {
|
|
return c.graphQL(ctx, `mutation DeleteResource($id: ID!) { deleteResource(id: $id) }`, map[string]any{"id": id}, nil)
|
|
}
|
|
|
|
func (c *Client) ListResources(ctx context.Context, q Query) (ResourceList, error) {
|
|
var out struct {
|
|
Resources ResourceList `json:"resources"`
|
|
}
|
|
vars := objectQueryVariables(q)
|
|
if q.Name != "" && q.Q == "" {
|
|
vars["q"] = q.Name
|
|
}
|
|
err := c.graphQL(ctx, `query Resources($q: String, $kind: String, $tenantId: ID, $limit: Int, $offset: Int) {
|
|
resources(q: $q, kind: $kind, tenantId: $tenantId, limit: $limit, offset: $offset) {
|
|
total
|
|
items { id kind name tenant_id: tenantId owner_id: ownerId attributes created_at: createdAt updated_at: updatedAt }
|
|
}
|
|
}`, vars, &out)
|
|
return out.Resources, err
|
|
}
|
|
|
|
type graphQLRequest struct {
|
|
Query string `json:"query"`
|
|
Variables map[string]any `json:"variables,omitempty"`
|
|
}
|
|
|
|
type graphQLErrorItem struct {
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
type graphQLResponse struct {
|
|
Data json.RawMessage `json:"data"`
|
|
Errors []graphQLErrorItem `json:"errors,omitempty"`
|
|
}
|
|
|
|
func (c *Client) graphQL(ctx context.Context, query string, variables map[string]any, out any) error {
|
|
var response graphQLResponse
|
|
if err := c.do(ctx, http.MethodPost, "/graphql", graphQLRequest{Query: query, Variables: variables}, &response); err != nil {
|
|
return err
|
|
}
|
|
return decodeGraphQLResponse(response, out)
|
|
}
|
|
|
|
func (c *Client) graphQLWithToken(ctx context.Context, query string, variables map[string]any, out any, token string) error {
|
|
var response graphQLResponse
|
|
if err := c.doWithToken(ctx, http.MethodPost, "/graphql", graphQLRequest{Query: query, Variables: variables}, &response, token); err != nil {
|
|
return err
|
|
}
|
|
return decodeGraphQLResponse(response, out)
|
|
}
|
|
|
|
func decodeGraphQLResponse(response graphQLResponse, out any) error {
|
|
if len(response.Errors) > 0 {
|
|
return graphQLErr(response.Errors)
|
|
}
|
|
if out == nil {
|
|
return nil
|
|
}
|
|
if len(response.Data) == 0 {
|
|
return Error{StatusCode: http.StatusInternalServerError, Message: "atom GraphQL response did not include data"}
|
|
}
|
|
return json.Unmarshal(response.Data, out)
|
|
}
|
|
|
|
func graphQLErr(errors []graphQLErrorItem) error {
|
|
messages := make([]string, 0, len(errors))
|
|
for _, err := range errors {
|
|
if err.Message != "" {
|
|
messages = append(messages, err.Message)
|
|
}
|
|
}
|
|
message := strings.Join(messages, "; ")
|
|
lower := strings.ToLower(message)
|
|
status := http.StatusBadRequest
|
|
switch {
|
|
case strings.Contains(lower, "duplicate") || strings.Contains(lower, "already exists") || strings.Contains(lower, "unique"):
|
|
status = http.StatusConflict
|
|
case strings.Contains(lower, "not found"):
|
|
status = http.StatusNotFound
|
|
case strings.Contains(lower, "unauthenticated") || strings.Contains(lower, "authentication"):
|
|
status = http.StatusUnauthorized
|
|
case strings.Contains(lower, "forbidden") || strings.Contains(lower, "authorization") || strings.Contains(lower, "access denied"):
|
|
status = http.StatusForbidden
|
|
}
|
|
return Error{StatusCode: status, Message: message}
|
|
}
|
|
|
|
func tenantCreateInput(tenant Tenant) map[string]any {
|
|
input := map[string]any{atomInputKeyName: tenant.Name}
|
|
setIfNotEmpty(input, "id", tenant.ID)
|
|
setIfNotEmpty(input, "alias", tenant.Route)
|
|
if tenant.Tags != nil {
|
|
input[atomAttributeTags] = tenant.Tags
|
|
}
|
|
if tenant.Attributes != nil {
|
|
input["attributes"] = tenant.Attributes
|
|
}
|
|
return input
|
|
}
|
|
|
|
func tenantUpdateInput(tenant Tenant) map[string]any {
|
|
input := map[string]any{}
|
|
setIfNotEmpty(input, atomInputKeyName, tenant.Name)
|
|
setIfNotEmpty(input, "alias", tenant.Route)
|
|
if tenant.Tags != nil {
|
|
input[atomAttributeTags] = tenant.Tags
|
|
}
|
|
if tenant.Attributes != nil {
|
|
input["attributes"] = tenant.Attributes
|
|
}
|
|
return input
|
|
}
|
|
|
|
func entityCreateInput(entity Entity) map[string]any {
|
|
input := map[string]any{atomInputKeyName: entity.Name}
|
|
setIfNotEmpty(input, "id", entity.ID)
|
|
setIfNotEmpty(input, atomInputKeyKind, entity.Kind)
|
|
setIfNotEmpty(input, "tenantId", entity.TenantID)
|
|
if entity.Attributes != nil {
|
|
input["attributes"] = entity.Attributes
|
|
} else {
|
|
input["attributes"] = map[string]any{}
|
|
}
|
|
return input
|
|
}
|
|
|
|
func entityUpdateInput(entity Entity) map[string]any {
|
|
input := map[string]any{}
|
|
setIfNotEmpty(input, atomInputKeyName, entity.Name)
|
|
setIfNotEmpty(input, atomAttributeStatus, entity.Status)
|
|
if entity.Attributes != nil {
|
|
input["attributes"] = entity.Attributes
|
|
}
|
|
return input
|
|
}
|
|
|
|
func groupCreateInput(group Group) map[string]any {
|
|
input := map[string]any{atomInputKeyName: group.Name}
|
|
setIfNotEmpty(input, "id", group.ID)
|
|
setIfNotEmpty(input, "tenantId", group.TenantID)
|
|
setIfNotEmpty(input, "description", group.Description)
|
|
if group.Attributes != nil {
|
|
input["attributes"] = group.Attributes
|
|
}
|
|
return input
|
|
}
|
|
|
|
func groupUpdateInput(group Group) map[string]any {
|
|
input := map[string]any{}
|
|
setIfNotEmpty(input, atomInputKeyName, group.Name)
|
|
setIfNotEmpty(input, "description", group.Description)
|
|
setIfNotEmpty(input, atomAttributeStatus, group.Status)
|
|
if group.Attributes != nil {
|
|
input["attributes"] = group.Attributes
|
|
}
|
|
return input
|
|
}
|
|
|
|
func resourceCreateInput(resource Resource) map[string]any {
|
|
input := map[string]any{atomInputKeyKind: resource.Kind}
|
|
setIfNotEmpty(input, "id", resource.ID)
|
|
setIfNotEmpty(input, atomInputKeyName, resource.Name)
|
|
setIfNotEmpty(input, "tenantId", resource.TenantID)
|
|
setIfNotEmpty(input, "ownerId", resource.OwnerID)
|
|
if resource.Attributes != nil {
|
|
input["attributes"] = resource.Attributes
|
|
}
|
|
return input
|
|
}
|
|
|
|
func resourceUpdateInput(resource Resource) map[string]any {
|
|
input := map[string]any{}
|
|
setIfNotEmpty(input, atomInputKeyName, resource.Name)
|
|
if resource.Attributes != nil {
|
|
input["attributes"] = resource.Attributes
|
|
}
|
|
return input
|
|
}
|
|
|
|
func authzInput(req AuthzRequest) map[string]any {
|
|
input := map[string]any{
|
|
atomInputKeySubjectID: req.SubjectID,
|
|
atomInputKeyAction: req.Action,
|
|
}
|
|
setIfNotEmpty(input, "resourceId", req.ResourceID)
|
|
setIfNotEmpty(input, atomInputKeyObjectKind, req.ObjectKind)
|
|
setIfNotEmpty(input, "objectId", req.ObjectID)
|
|
if req.Context != nil {
|
|
input["context"] = req.Context
|
|
}
|
|
return input
|
|
}
|
|
|
|
func permissionBlockInput(block CreatePermissionBlock) map[string]any {
|
|
input := map[string]any{
|
|
"scopeMode": block.ScopeMode,
|
|
"actionIds": block.ActionIDs,
|
|
}
|
|
setIfNotEmpty(input, "tenantId", block.TenantID)
|
|
setIfNotEmpty(input, atomInputKeyObjectKind, block.ObjectKind)
|
|
setIfNotEmpty(input, "objectType", block.ObjectType)
|
|
setIfNotEmpty(input, "objectId", block.ObjectID)
|
|
setIfNotEmpty(input, "groupId", block.GroupID)
|
|
setIfNotEmpty(input, "effect", block.Effect)
|
|
if block.Conditions != nil {
|
|
input["conditions"] = block.Conditions
|
|
}
|
|
return input
|
|
}
|
|
|
|
func directPolicyInput(policy CreateDirectPolicy) map[string]any {
|
|
input := map[string]any{
|
|
"subjectKind": policy.SubjectKind,
|
|
atomInputKeySubjectID: policy.SubjectID,
|
|
"permissionBlockId": policy.PermissionBlockID,
|
|
}
|
|
setIfNotEmpty(input, "tenantId", policy.TenantID)
|
|
return input
|
|
}
|
|
|
|
func directPolicyQueryVariables(q DirectPolicyQuery) map[string]any {
|
|
vars := map[string]any{}
|
|
setIfNotEmpty(vars, "tenantId", q.TenantID)
|
|
setIfNotEmpty(vars, "subjectKind", q.SubjectKind)
|
|
setIfNotEmpty(vars, atomInputKeySubjectID, q.SubjectID)
|
|
if q.Limit > 0 {
|
|
vars["limit"] = int(q.Limit)
|
|
}
|
|
if q.Offset > 0 {
|
|
vars["offset"] = int(q.Offset)
|
|
}
|
|
return vars
|
|
}
|
|
|
|
func authorizedObjectIDVariables(q AuthorizedObjectIDsQuery) map[string]any {
|
|
input := map[string]any{
|
|
atomInputKeySubjectID: q.SubjectID,
|
|
atomInputKeyAction: q.Action,
|
|
atomInputKeyObjectKind: q.ObjectKind,
|
|
}
|
|
setIfNotEmpty(input, "objectType", q.ObjectType)
|
|
setIfNotEmpty(input, "tenantId", q.TenantID)
|
|
setIfNotEmpty(input, "q", q.Q)
|
|
if q.Limit > 0 {
|
|
input["limit"] = int(q.Limit)
|
|
}
|
|
if q.Offset > 0 {
|
|
input["offset"] = int(q.Offset)
|
|
}
|
|
return map[string]any{atomInputKeyInput: input}
|
|
}
|
|
|
|
func queryVariables(q Query) map[string]any {
|
|
vars := map[string]any{}
|
|
setIfNotEmpty(vars, "q", q.Q)
|
|
setIfNotEmpty(vars, atomInputKeyName, q.Name)
|
|
setIfNotEmpty(vars, "alias", q.Route)
|
|
setIfNotEmpty(vars, atomInputKeyKind, q.Kind)
|
|
setIfNotEmpty(vars, "tenantId", q.TenantID)
|
|
setIfNotEmpty(vars, atomAttributeStatus, q.Status)
|
|
if q.Limit > 0 {
|
|
vars["limit"] = int(q.Limit)
|
|
}
|
|
if q.Offset > 0 {
|
|
vars["offset"] = int(q.Offset)
|
|
}
|
|
return vars
|
|
}
|
|
|
|
func objectQueryVariables(q Query) map[string]any {
|
|
vars := map[string]any{}
|
|
setIfNotEmpty(vars, "q", q.Q)
|
|
setIfNotEmpty(vars, atomInputKeyKind, q.Kind)
|
|
setIfNotEmpty(vars, "tenantId", q.TenantID)
|
|
setIfNotEmpty(vars, atomAttributeStatus, q.Status)
|
|
if q.Limit > 0 {
|
|
vars["limit"] = int(q.Limit)
|
|
}
|
|
if q.Offset > 0 {
|
|
vars["offset"] = int(q.Offset)
|
|
}
|
|
return vars
|
|
}
|
|
|
|
func setIfNotEmpty(values map[string]any, key, value string) {
|
|
if value != "" {
|
|
values[key] = value
|
|
}
|
|
}
|
|
|
|
func (c *Client) do(ctx context.Context, method, path string, in, out any) error {
|
|
token := c.token
|
|
if token == "" && c.adminSecret != "" {
|
|
adminToken, err := c.loginAdmin(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
token = adminToken
|
|
}
|
|
return c.doWithToken(ctx, method, path, in, out, token)
|
|
}
|
|
|
|
func (c *Client) loginAdmin(ctx context.Context) (string, error) {
|
|
username := c.adminUsername
|
|
if username == "" {
|
|
username = defaultAdminUsername
|
|
}
|
|
resp, err := c.LoginPassword(ctx, username, c.adminSecret)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return resp.Token, nil
|
|
}
|
|
|
|
func (c *Client) doWithToken(ctx context.Context, method, path string, in, out any, token string) error {
|
|
if c.baseURL == "" {
|
|
return Error{StatusCode: 0, Message: "atom URL is empty"}
|
|
}
|
|
|
|
var body io.Reader
|
|
if in != nil {
|
|
data, err := json.Marshal(in)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
body = bytes.NewReader(data)
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if in != nil {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
req.Header.Set("Accept", "application/json")
|
|
if c.userAgent != "" {
|
|
req.Header.Set("User-Agent", c.userAgent)
|
|
}
|
|
if token != "" {
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
}
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
|
msg, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
|
|
return Error{StatusCode: resp.StatusCode, Message: strings.TrimSpace(string(msg))}
|
|
}
|
|
if out == nil || resp.StatusCode == http.StatusNoContent {
|
|
return nil
|
|
}
|
|
return json.NewDecoder(resp.Body).Decode(out)
|
|
}
|
|
|
|
type Error struct {
|
|
StatusCode int
|
|
Message string
|
|
}
|
|
|
|
func (e Error) Error() string {
|
|
if e.Message == "" {
|
|
return fmt.Sprintf("atom request failed with status %d", e.StatusCode)
|
|
}
|
|
return fmt.Sprintf("atom request failed with status %d: %s", e.StatusCode, e.Message)
|
|
}
|
|
|
|
func IsConflict(err error) bool {
|
|
ae, ok := err.(Error)
|
|
return ok && ae.StatusCode == http.StatusConflict
|
|
}
|
|
|
|
func IsNotFound(err error) bool {
|
|
ae, ok := err.(Error)
|
|
return ok && ae.StatusCode == http.StatusNotFound
|
|
}
|