Files
Dušan Borovčanin 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
NOISSUE - Use Atom access tokens (#3540)
Signed-off-by: dusan <borovcanindusan1@gmail.com>
2026-07-01 19:41:01 +02:00

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
}