NOISSUE - Use Atom access tokens (#3540)
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>
This commit is contained in:
Dušan Borovčanin
2026-07-01 19:41:01 +02:00
committed by GitHub
parent 3b5f646719
commit 09a56575c1
52 changed files with 1093 additions and 512 deletions
+2 -2
View File
@@ -23,8 +23,8 @@ func Authorize(ctx context.Context, client Authorizer, session authn.Session, ac
ObjectKind: ObjectKind(legacyObjectType, resourceKind),
ObjectID: objectID,
Context: map[string]any{
"domain_id": session.DomainID,
"legacy_object_type": legacyObjectType,
atomContextDomainID: session.DomainID,
atomContextLegacyObjectType: legacyObjectType,
},
}
res, err := client.CheckAuthz(ctx, req)
+7 -7
View File
@@ -33,13 +33,13 @@ func (a AuthorizationCompat) Authorize(ctx context.Context, pr smqauthz.PolicyRe
ObjectKind: objectKind,
ObjectID: pr.Object,
Context: map[string]any{
"domain_id": pr.Domain,
"legacy_object_kind": pr.ObjectKind,
"legacy_object_type": pr.ObjectType,
"legacy_permission": pr.Permission,
"legacy_relation": pr.Relation,
"legacy_subject_kind": pr.SubjectKind,
"legacy_subject_type": pr.SubjectType,
atomContextDomainID: pr.Domain,
"legacy_object_kind": pr.ObjectKind,
atomContextLegacyObjectType: pr.ObjectType,
"legacy_permission": pr.Permission,
"legacy_relation": pr.Relation,
"legacy_subject_kind": pr.SubjectKind,
"legacy_subject_type": pr.SubjectType,
},
})
if err != nil {
+27 -27
View File
@@ -28,32 +28,32 @@ var magistralaActionApplicability = []CapabilityApplicabilitySpec{
{ActionName: atomActionManage, ObjectKind: atomObjectKindGroup},
{ActionName: atomActionList, ObjectKind: atomObjectKindGroup},
{ActionName: atomActionRead, ObjectKind: atomObjectKindResource, ObjectType: "resource:channel"},
{ActionName: atomActionWrite, ObjectKind: atomObjectKindResource, ObjectType: "resource:channel"},
{ActionName: atomActionDelete, ObjectKind: atomObjectKindResource, ObjectType: "resource:channel"},
{ActionName: atomActionManage, ObjectKind: atomObjectKindResource, ObjectType: "resource:channel"},
{ActionName: atomActionPublish, ObjectKind: atomObjectKindResource, ObjectType: "resource:channel"},
{ActionName: atomActionSubscribe, ObjectKind: atomObjectKindResource, ObjectType: "resource:channel"},
{ActionName: atomActionRead, ObjectKind: atomObjectKindResource, ObjectType: atomObjectTypeResourceChannel},
{ActionName: atomActionWrite, ObjectKind: atomObjectKindResource, ObjectType: atomObjectTypeResourceChannel},
{ActionName: atomActionDelete, ObjectKind: atomObjectKindResource, ObjectType: atomObjectTypeResourceChannel},
{ActionName: atomActionManage, ObjectKind: atomObjectKindResource, ObjectType: atomObjectTypeResourceChannel},
{ActionName: atomActionPublish, ObjectKind: atomObjectKindResource, ObjectType: atomObjectTypeResourceChannel},
{ActionName: atomActionSubscribe, ObjectKind: atomObjectKindResource, ObjectType: atomObjectTypeResourceChannel},
{ActionName: atomActionRead, ObjectKind: atomObjectKindResource, ObjectType: "resource:rule"},
{ActionName: atomActionWrite, ObjectKind: atomObjectKindResource, ObjectType: "resource:rule"},
{ActionName: atomActionDelete, ObjectKind: atomObjectKindResource, ObjectType: "resource:rule"},
{ActionName: atomActionManage, ObjectKind: atomObjectKindResource, ObjectType: "resource:rule"},
{ActionName: atomActionExecute, ObjectKind: atomObjectKindResource, ObjectType: "resource:rule"},
{ActionName: atomActionList, ObjectKind: atomObjectKindResource, ObjectType: "resource:rule"},
{ActionName: atomActionRead, ObjectKind: atomObjectKindResource, ObjectType: atomObjectTypeResourceRule},
{ActionName: atomActionWrite, ObjectKind: atomObjectKindResource, ObjectType: atomObjectTypeResourceRule},
{ActionName: atomActionDelete, ObjectKind: atomObjectKindResource, ObjectType: atomObjectTypeResourceRule},
{ActionName: atomActionManage, ObjectKind: atomObjectKindResource, ObjectType: atomObjectTypeResourceRule},
{ActionName: atomActionExecute, ObjectKind: atomObjectKindResource, ObjectType: atomObjectTypeResourceRule},
{ActionName: atomActionList, ObjectKind: atomObjectKindResource, ObjectType: atomObjectTypeResourceRule},
{ActionName: atomActionRead, ObjectKind: atomObjectKindResource, ObjectType: "resource:report"},
{ActionName: atomActionWrite, ObjectKind: atomObjectKindResource, ObjectType: "resource:report"},
{ActionName: atomActionDelete, ObjectKind: atomObjectKindResource, ObjectType: "resource:report"},
{ActionName: atomActionManage, ObjectKind: atomObjectKindResource, ObjectType: "resource:report"},
{ActionName: atomActionExecute, ObjectKind: atomObjectKindResource, ObjectType: "resource:report"},
{ActionName: atomActionList, ObjectKind: atomObjectKindResource, ObjectType: "resource:report"},
{ActionName: atomActionRead, ObjectKind: atomObjectKindResource, ObjectType: atomObjectTypeResourceReport},
{ActionName: atomActionWrite, ObjectKind: atomObjectKindResource, ObjectType: atomObjectTypeResourceReport},
{ActionName: atomActionDelete, ObjectKind: atomObjectKindResource, ObjectType: atomObjectTypeResourceReport},
{ActionName: atomActionManage, ObjectKind: atomObjectKindResource, ObjectType: atomObjectTypeResourceReport},
{ActionName: atomActionExecute, ObjectKind: atomObjectKindResource, ObjectType: atomObjectTypeResourceReport},
{ActionName: atomActionList, ObjectKind: atomObjectKindResource, ObjectType: atomObjectTypeResourceReport},
{ActionName: atomActionRead, ObjectKind: atomObjectKindResource, ObjectType: "resource:alarm"},
{ActionName: atomActionWrite, ObjectKind: atomObjectKindResource, ObjectType: "resource:alarm"},
{ActionName: atomActionDelete, ObjectKind: atomObjectKindResource, ObjectType: "resource:alarm"},
{ActionName: atomActionManage, ObjectKind: atomObjectKindResource, ObjectType: "resource:alarm"},
{ActionName: atomActionList, ObjectKind: atomObjectKindResource, ObjectType: "resource:alarm"},
{ActionName: atomActionRead, ObjectKind: atomObjectKindResource, ObjectType: atomObjectTypeResourceAlarm},
{ActionName: atomActionWrite, ObjectKind: atomObjectKindResource, ObjectType: atomObjectTypeResourceAlarm},
{ActionName: atomActionDelete, ObjectKind: atomObjectKindResource, ObjectType: atomObjectTypeResourceAlarm},
{ActionName: atomActionManage, ObjectKind: atomObjectKindResource, ObjectType: atomObjectTypeResourceAlarm},
{ActionName: atomActionList, ObjectKind: atomObjectKindResource, ObjectType: atomObjectTypeResourceAlarm},
}
var magistralaActionAssignmentRules = []ActionAssignmentRuleSpec{
@@ -61,15 +61,15 @@ var magistralaActionAssignmentRules = []ActionAssignmentRuleSpec{
EntityKind: atomKindDevice,
ActionName: atomActionPublish,
ObjectKind: atomObjectKindResource,
ObjectType: "resource:channel",
Decision: "allow",
ObjectType: atomObjectTypeResourceChannel,
Decision: atomDecisionAllow,
},
{
EntityKind: atomKindDevice,
ActionName: atomActionSubscribe,
ObjectKind: atomObjectKindResource,
ObjectType: "resource:channel",
Decision: "allow",
ObjectType: atomObjectTypeResourceChannel,
Decision: atomDecisionAllow,
},
}
+85 -73
View File
@@ -57,7 +57,7 @@ func (c *Client) CreateTenant(ctx context.Context, tenant Tenant) (Tenant, error
}
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{"input": tenantCreateInput(tenant)}, &out)
}`, map[string]any{atomInputKeyInput: tenantCreateInput(tenant)}, &out)
return out.CreateTenant, err
}
@@ -77,7 +77,7 @@ func (c *Client) UpdateTenant(ctx context.Context, id string, tenant Tenant) (Te
}
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, "input": tenantUpdateInput(tenant)}, &out)
}`, map[string]any{"id": id, atomInputKeyInput: tenantUpdateInput(tenant)}, &out)
return out.UpdateTenant, err
}
@@ -118,7 +118,7 @@ func (c *Client) CreateEntity(ctx context.Context, entity Entity) (Entity, error
}
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{"input": entityCreateInput(entity)}, &out)
}`, map[string]any{atomInputKeyInput: entityCreateInput(entity)}, &out)
return out.CreateEntity, err
}
@@ -138,7 +138,7 @@ func (c *Client) UpdateEntity(ctx context.Context, id string, entity Entity) (En
}
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, "input": entityUpdateInput(entity)}, &out)
}`, map[string]any{"id": id, atomInputKeyInput: entityUpdateInput(entity)}, &out)
return out.UpdateEntity, err
}
@@ -160,7 +160,7 @@ func (c *Client) CreateGroup(ctx context.Context, group Group) (Group, error) {
}
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{"input": groupCreateInput(group)}, &out)
}`, map[string]any{atomInputKeyInput: groupCreateInput(group)}, &out)
return out.CreateGroup, err
}
@@ -180,7 +180,7 @@ func (c *Client) UpdateGroup(ctx context.Context, id string, group Group) (Group
}
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, "input": groupUpdateInput(group)}, &out)
}`, map[string]any{"id": id, atomInputKeyInput: groupUpdateInput(group)}, &out)
return out.UpdateGroup, err
}
@@ -202,7 +202,7 @@ func (c *Client) CreateResource(ctx context.Context, resource Resource) (Resourc
}
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{"input": resourceCreateInput(resource)}, &out)
}`, map[string]any{atomInputKeyInput: resourceCreateInput(resource)}, &out)
return out.CreateResource, err
}
@@ -222,7 +222,7 @@ func (c *Client) UpdateResource(ctx context.Context, id string, resource Resourc
}
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, "input": resourceUpdateInput(resource)}, &out)
}`, map[string]any{"id": id, atomInputKeyInput: resourceUpdateInput(resource)}, &out)
return out.UpdateResource, err
}
@@ -249,7 +249,7 @@ func (c *Client) CheckAuthz(ctx context.Context, req AuthzRequest) (AuthzRespons
}
err := c.graphQL(ctx, `mutation AuthzCheck($input: AuthzCheckInput!) {
authzCheck(input: $input) { allowed reason }
}`, map[string]any{"input": authzInput(req)}, &out)
}`, map[string]any{atomInputKeyInput: authzInput(req)}, &out)
return out.AuthzCheck, err
}
@@ -259,7 +259,7 @@ func (c *Client) CheckAuthzWithToken(ctx context.Context, token string, req Auth
}
err := c.graphQLWithToken(ctx, `mutation AuthzCheck($input: AuthzCheckInput!) {
authzCheck(input: $input) { allowed reason }
}`, map[string]any{"input": authzInput(req)}, &out, token)
}`, map[string]any{atomInputKeyInput: authzInput(req)}, &out, token)
return out.AuthzCheck, err
}
@@ -290,11 +290,11 @@ func (c *Client) CreateCapability(ctx context.Context, name, description string)
var out struct {
CreateAction Capability `json:"createAction"`
}
input := map[string]any{"name": name}
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{"input": input}, &out)
}`, map[string]any{atomInputKeyInput: input}, &out)
return out.CreateAction, err
}
@@ -303,8 +303,8 @@ func (c *Client) AddCapabilityApplicability(ctx context.Context, actionID, objec
AddActionApplicability CapabilityApplicability `json:"addActionApplicability"`
}
input := map[string]any{
"actionId": actionID,
"objectKind": objectKind,
"actionId": actionID,
atomInputKeyObjectKind: objectKind,
}
setIfNotEmpty(input, "objectType", objectType)
err := c.graphQL(ctx, `mutation AddActionApplicability($input: AddActionApplicabilityInput!) {
@@ -315,7 +315,7 @@ func (c *Client) AddCapabilityApplicability(ctx context.Context, actionID, objec
object_kind: objectKind
object_type: objectType
}
}`, map[string]any{"input": input}, &out)
}`, map[string]any{atomInputKeyInput: input}, &out)
return out.AddActionApplicability, err
}
@@ -327,7 +327,7 @@ func (c *Client) ListActionAssignmentRules(ctx context.Context, spec ActionAssig
setIfNotEmpty(vars, "tenantId", spec.TenantID)
setIfNotEmpty(vars, "entityKind", spec.EntityKind)
setIfNotEmpty(vars, "actionName", spec.ActionName)
setIfNotEmpty(vars, "objectKind", spec.ObjectKind)
setIfNotEmpty(vars, atomInputKeyObjectKind, spec.ObjectKind)
setIfNotEmpty(vars, "objectType", spec.ObjectType)
setIfNotEmpty(vars, "decision", spec.Decision)
err := c.graphQL(ctx, `query ActionAssignmentRules(
@@ -372,11 +372,11 @@ func (c *Client) CreateActionAssignmentRule(ctx context.Context, spec ActionAssi
CreateActionAssignmentRule ActionAssignmentRule `json:"createActionAssignmentRule"`
}
input := map[string]any{
"entityKind": spec.EntityKind,
"actionName": spec.ActionName,
"objectKind": spec.ObjectKind,
"decision": spec.Decision,
"isAbsolute": spec.IsAbsolute,
"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)
@@ -392,7 +392,7 @@ func (c *Client) CreateActionAssignmentRule(ctx context.Context, spec ActionAssi
is_absolute: isAbsolute
created_at: createdAt
}
}`, map[string]any{"input": input}, &out)
}`, map[string]any{atomInputKeyInput: input}, &out)
return out.CreateActionAssignmentRule, err
}
@@ -405,7 +405,7 @@ func (c *Client) CreatePermissionBlock(ctx context.Context, block CreatePermissi
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{"input": permissionBlockInput(block)}, &out)
}`, map[string]any{atomInputKeyInput: permissionBlockInput(block)}, &out)
return out.CreatePermissionBlock, err
}
@@ -421,7 +421,7 @@ func (c *Client) CreateDirectPolicy(ctx context.Context, policy CreateDirectPoli
actions { id name description }
}
}
}`, map[string]any{"input": directPolicyInput(policy)}, &out)
}`, map[string]any{atomInputKeyInput: directPolicyInput(policy)}, &out)
return out.CreateDirectPolicy, err
}
@@ -462,18 +462,26 @@ func (c *Client) AuthorizedObjectIDs(ctx context.Context, q AuthorizedObjectIDsQ
}
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: "password",
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, "/auth/introspect", nil, &out, token)
err := c.doWithToken(ctx, http.MethodGet, atomAuthIntrospectPath, nil, &out, token)
return out, err
}
@@ -484,26 +492,30 @@ func (c *Client) DeleteEntity(ctx context.Context, id string) error {
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{"entityId": entityID, "password": password}, nil)
}`, map[string]any{atomInputKeyEntityID: entityID, "password": password}, nil)
}
func (c *Client) CreateAPIKey(ctx context.Context, entityID, description string) (APIKeyResponse, error) {
func (c *Client) CreateUnscopedAccessToken(ctx context.Context, entityID, name, description string) (AccessTokenResponse, error) {
var out struct {
CreateAPIKey APIKeyResponse `json:"createApiKey"`
CreateAccessToken AccessTokenResponse `json:"createAccessToken"`
}
err := c.graphQL(ctx, `mutation CreateAPIKey($entityId: ID!, $input: CreateApiKeyInput!) {
createApiKey(entityId: $entityId, input: $input) {
err := c.graphQL(ctx, `mutation CreateAccessToken($input: CreateAccessTokenInput!) {
createAccessToken(input: $input) {
credentialId
key
token
name
expiresAt
}
}`, map[string]any{
"entityId": entityID,
"input": map[string]any{
"description": description,
atomInputKeyInput: map[string]any{
atomInputKeyName: name,
"description": description,
atomInputKeySubjectID: entityID,
"scoped": false,
"permissions": []any{},
},
}, &out)
return out.CreateAPIKey, err
return out.CreateAccessToken, err
}
func (c *Client) CreateSharedKey(ctx context.Context, entityID, key, description string) (SharedKeyResponse, error) {
@@ -520,8 +532,8 @@ func (c *Client) CreateSharedKey(ctx context.Context, entityID, key, description
expiresAt
}
}`, map[string]any{
"entityId": entityID,
"input": input,
atomInputKeyEntityID: entityID,
atomInputKeyInput: input,
}, &out)
return out.CreateSharedKey, err
}
@@ -537,8 +549,8 @@ func (c *Client) RevealSharedKey(ctx context.Context, entityID, credentialID str
expiresAt
}
}`, map[string]any{
"entityId": entityID,
"credentialId": credentialID,
atomInputKeyEntityID: entityID,
atomInputKeyCredentialID: credentialID,
}, &out)
return out.RevealSharedKey, err
}
@@ -560,14 +572,14 @@ func (c *Client) ListCredentials(ctx context.Context, entityID string) (Credenti
created_at: createdAt
}
}
}`, map[string]any{"entityId": entityID}, &out)
}`, 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{"entityId": entityID, "credentialId": credentialID}, nil)
}`, map[string]any{atomInputKeyEntityID: entityID, atomInputKeyCredentialID: credentialID}, nil)
}
func (c *Client) ListEntities(ctx context.Context, q Query) (EntityList, error) {
@@ -688,11 +700,11 @@ func graphQLErr(errors []graphQLErrorItem) error {
}
func tenantCreateInput(tenant Tenant) map[string]any {
input := map[string]any{"name": tenant.Name}
input := map[string]any{atomInputKeyName: tenant.Name}
setIfNotEmpty(input, "id", tenant.ID)
setIfNotEmpty(input, "alias", tenant.Route)
if tenant.Tags != nil {
input["tags"] = tenant.Tags
input[atomAttributeTags] = tenant.Tags
}
if tenant.Attributes != nil {
input["attributes"] = tenant.Attributes
@@ -702,10 +714,10 @@ func tenantCreateInput(tenant Tenant) map[string]any {
func tenantUpdateInput(tenant Tenant) map[string]any {
input := map[string]any{}
setIfNotEmpty(input, "name", tenant.Name)
setIfNotEmpty(input, atomInputKeyName, tenant.Name)
setIfNotEmpty(input, "alias", tenant.Route)
if tenant.Tags != nil {
input["tags"] = tenant.Tags
input[atomAttributeTags] = tenant.Tags
}
if tenant.Attributes != nil {
input["attributes"] = tenant.Attributes
@@ -714,9 +726,9 @@ func tenantUpdateInput(tenant Tenant) map[string]any {
}
func entityCreateInput(entity Entity) map[string]any {
input := map[string]any{"name": entity.Name}
input := map[string]any{atomInputKeyName: entity.Name}
setIfNotEmpty(input, "id", entity.ID)
setIfNotEmpty(input, "kind", entity.Kind)
setIfNotEmpty(input, atomInputKeyKind, entity.Kind)
setIfNotEmpty(input, "tenantId", entity.TenantID)
if entity.Attributes != nil {
input["attributes"] = entity.Attributes
@@ -728,8 +740,8 @@ func entityCreateInput(entity Entity) map[string]any {
func entityUpdateInput(entity Entity) map[string]any {
input := map[string]any{}
setIfNotEmpty(input, "name", entity.Name)
setIfNotEmpty(input, "status", entity.Status)
setIfNotEmpty(input, atomInputKeyName, entity.Name)
setIfNotEmpty(input, atomAttributeStatus, entity.Status)
if entity.Attributes != nil {
input["attributes"] = entity.Attributes
}
@@ -737,7 +749,7 @@ func entityUpdateInput(entity Entity) map[string]any {
}
func groupCreateInput(group Group) map[string]any {
input := map[string]any{"name": group.Name}
input := map[string]any{atomInputKeyName: group.Name}
setIfNotEmpty(input, "id", group.ID)
setIfNotEmpty(input, "tenantId", group.TenantID)
setIfNotEmpty(input, "description", group.Description)
@@ -749,9 +761,9 @@ func groupCreateInput(group Group) map[string]any {
func groupUpdateInput(group Group) map[string]any {
input := map[string]any{}
setIfNotEmpty(input, "name", group.Name)
setIfNotEmpty(input, atomInputKeyName, group.Name)
setIfNotEmpty(input, "description", group.Description)
setIfNotEmpty(input, "status", group.Status)
setIfNotEmpty(input, atomAttributeStatus, group.Status)
if group.Attributes != nil {
input["attributes"] = group.Attributes
}
@@ -759,9 +771,9 @@ func groupUpdateInput(group Group) map[string]any {
}
func resourceCreateInput(resource Resource) map[string]any {
input := map[string]any{"kind": resource.Kind}
input := map[string]any{atomInputKeyKind: resource.Kind}
setIfNotEmpty(input, "id", resource.ID)
setIfNotEmpty(input, "name", resource.Name)
setIfNotEmpty(input, atomInputKeyName, resource.Name)
setIfNotEmpty(input, "tenantId", resource.TenantID)
setIfNotEmpty(input, "ownerId", resource.OwnerID)
if resource.Attributes != nil {
@@ -772,7 +784,7 @@ func resourceCreateInput(resource Resource) map[string]any {
func resourceUpdateInput(resource Resource) map[string]any {
input := map[string]any{}
setIfNotEmpty(input, "name", resource.Name)
setIfNotEmpty(input, atomInputKeyName, resource.Name)
if resource.Attributes != nil {
input["attributes"] = resource.Attributes
}
@@ -781,11 +793,11 @@ func resourceUpdateInput(resource Resource) map[string]any {
func authzInput(req AuthzRequest) map[string]any {
input := map[string]any{
"subjectId": req.SubjectID,
"action": req.Action,
atomInputKeySubjectID: req.SubjectID,
atomInputKeyAction: req.Action,
}
setIfNotEmpty(input, "resourceId", req.ResourceID)
setIfNotEmpty(input, "objectKind", req.ObjectKind)
setIfNotEmpty(input, atomInputKeyObjectKind, req.ObjectKind)
setIfNotEmpty(input, "objectId", req.ObjectID)
if req.Context != nil {
input["context"] = req.Context
@@ -799,7 +811,7 @@ func permissionBlockInput(block CreatePermissionBlock) map[string]any {
"actionIds": block.ActionIDs,
}
setIfNotEmpty(input, "tenantId", block.TenantID)
setIfNotEmpty(input, "objectKind", block.ObjectKind)
setIfNotEmpty(input, atomInputKeyObjectKind, block.ObjectKind)
setIfNotEmpty(input, "objectType", block.ObjectType)
setIfNotEmpty(input, "objectId", block.ObjectID)
setIfNotEmpty(input, "groupId", block.GroupID)
@@ -812,9 +824,9 @@ func permissionBlockInput(block CreatePermissionBlock) map[string]any {
func directPolicyInput(policy CreateDirectPolicy) map[string]any {
input := map[string]any{
"subjectKind": policy.SubjectKind,
"subjectId": policy.SubjectID,
"permissionBlockId": policy.PermissionBlockID,
"subjectKind": policy.SubjectKind,
atomInputKeySubjectID: policy.SubjectID,
"permissionBlockId": policy.PermissionBlockID,
}
setIfNotEmpty(input, "tenantId", policy.TenantID)
return input
@@ -824,7 +836,7 @@ func directPolicyQueryVariables(q DirectPolicyQuery) map[string]any {
vars := map[string]any{}
setIfNotEmpty(vars, "tenantId", q.TenantID)
setIfNotEmpty(vars, "subjectKind", q.SubjectKind)
setIfNotEmpty(vars, "subjectId", q.SubjectID)
setIfNotEmpty(vars, atomInputKeySubjectID, q.SubjectID)
if q.Limit > 0 {
vars["limit"] = int(q.Limit)
}
@@ -836,9 +848,9 @@ func directPolicyQueryVariables(q DirectPolicyQuery) map[string]any {
func authorizedObjectIDVariables(q AuthorizedObjectIDsQuery) map[string]any {
input := map[string]any{
"subjectId": q.SubjectID,
"action": q.Action,
"objectKind": q.ObjectKind,
atomInputKeySubjectID: q.SubjectID,
atomInputKeyAction: q.Action,
atomInputKeyObjectKind: q.ObjectKind,
}
setIfNotEmpty(input, "objectType", q.ObjectType)
setIfNotEmpty(input, "tenantId", q.TenantID)
@@ -849,17 +861,17 @@ func authorizedObjectIDVariables(q AuthorizedObjectIDsQuery) map[string]any {
if q.Offset > 0 {
input["offset"] = int(q.Offset)
}
return map[string]any{"input": input}
return map[string]any{atomInputKeyInput: input}
}
func queryVariables(q Query) map[string]any {
vars := map[string]any{}
setIfNotEmpty(vars, "q", q.Q)
setIfNotEmpty(vars, "name", q.Name)
setIfNotEmpty(vars, atomInputKeyName, q.Name)
setIfNotEmpty(vars, "alias", q.Route)
setIfNotEmpty(vars, "kind", q.Kind)
setIfNotEmpty(vars, atomInputKeyKind, q.Kind)
setIfNotEmpty(vars, "tenantId", q.TenantID)
setIfNotEmpty(vars, "status", q.Status)
setIfNotEmpty(vars, atomAttributeStatus, q.Status)
if q.Limit > 0 {
vars["limit"] = int(q.Limit)
}
@@ -872,9 +884,9 @@ func queryVariables(q Query) map[string]any {
func objectQueryVariables(q Query) map[string]any {
vars := map[string]any{}
setIfNotEmpty(vars, "q", q.Q)
setIfNotEmpty(vars, "kind", q.Kind)
setIfNotEmpty(vars, atomInputKeyKind, q.Kind)
setIfNotEmpty(vars, "tenantId", q.TenantID)
setIfNotEmpty(vars, "status", q.Status)
setIfNotEmpty(vars, atomAttributeStatus, q.Status)
if q.Limit > 0 {
vars["limit"] = int(q.Limit)
}
+150
View File
@@ -94,6 +94,156 @@ func TestListResources(t *testing.T) {
}
}
func TestCurrentAtomCompatibilitySurface(t *testing.T) {
const (
serviceToken = "service-token"
runtimeToken = "runtime-token"
)
seen := map[string]bool{}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case atomGraphQLPath:
if r.Method != http.MethodPost {
t.Fatalf("unexpected GraphQL method: %s", r.Method)
}
if got := r.Header.Get("Authorization"); got != "Bearer "+serviceToken {
t.Fatalf("unexpected GraphQL authorization header: %q", got)
}
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, "authzCheck"):
seen["authzCheck"] = true
input, ok := payload.Variables["input"].(map[string]any)
if !ok {
t.Fatalf("unexpected authz input: %+v", payload.Variables["input"])
}
if input["subjectId"] != testEntityID || input["action"] != atomActionPublish || input["resourceId"] != "channel-1" {
t.Fatalf("unexpected authz input: %+v", input)
}
_ = json.NewEncoder(w).Encode(map[string]any{
"data": map[string]any{
"authzCheck": map[string]any{"allowed": true, "reason": "allowed"},
},
})
case strings.Contains(payload.Query, "authorizedObjectIds"):
seen["authorizedObjectIds"] = true
input, ok := payload.Variables["input"].(map[string]any)
if !ok {
t.Fatalf("unexpected authorized objects input: %+v", payload.Variables["input"])
}
if input["subjectId"] != testEntityID ||
input["action"] != atomActionRead ||
input["objectKind"] != atomObjectKindEntity ||
input["objectType"] != atomKindDevice ||
input["tenantId"] != testDomainID {
t.Fatalf("unexpected authorized objects input: %+v", input)
}
_ = json.NewEncoder(w).Encode(map[string]any{
"data": map[string]any{
"authorizedObjectIds": map[string]any{"ids": []string{testDeviceID}, "total": 1},
},
})
case strings.Contains(payload.Query, "createAccessToken"):
seen["createAccessToken"] = true
input, ok := payload.Variables["input"].(map[string]any)
if !ok ||
input["name"] != "magistrala-service" ||
input["description"] != "Magistrala service token" ||
input["subjectId"] != testEntityID ||
input["scoped"] != false ||
len(input["permissions"].([]any)) != 0 {
t.Fatalf("unexpected createAccessToken input: %+v", payload.Variables["input"])
}
_ = json.NewEncoder(w).Encode(map[string]any{
"data": map[string]any{
"createAccessToken": map[string]any{
"credentialId": testCredentialID,
"token": "atom_00000000000000000000000000000000_0000000000000000000000000000000000000000000000000000000000000000",
"name": "magistrala-service",
},
},
})
default:
t.Fatalf("unexpected GraphQL payload: %s", payload.Query)
}
case atomAuthIntrospectPath:
seen["introspect"] = true
if r.Method != http.MethodGet {
t.Fatalf("unexpected introspection method: %s", r.Method)
}
if got := r.Header.Get("Authorization"); got != "Bearer "+runtimeToken {
t.Fatalf("unexpected introspection authorization header: %q", got)
}
_ = json.NewEncoder(w).Encode(IntrospectionResponse{
Active: true,
EntityID: testEntityID,
TenantID: testDomainID,
SessionID: "session-1",
})
default:
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
}))
defer srv.Close()
client := NewClient(Config{URL: srv.URL, Token: serviceToken, Timeout: time.Second})
authz, err := client.CheckAuthz(context.Background(), AuthzRequest{
SubjectID: testEntityID,
Action: atomActionPublish,
ResourceID: "channel-1",
})
if err != nil {
t.Fatalf("authz check failed: %v", err)
}
if !authz.Allowed {
t.Fatalf("unexpected authz response: %+v", authz)
}
objects, err := client.AuthorizedObjectIDs(context.Background(), AuthorizedObjectIDsQuery{
SubjectID: testEntityID,
Action: atomActionRead,
ObjectKind: atomObjectKindEntity,
ObjectType: atomKindDevice,
TenantID: testDomainID,
Limit: 10,
})
if err != nil {
t.Fatalf("authorized object listing failed: %v", err)
}
if objects.Total != 1 || len(objects.IDs) != 1 || objects.IDs[0] != testDeviceID {
t.Fatalf("unexpected authorized object listing: %+v", objects)
}
created, err := client.CreateUnscopedAccessToken(context.Background(), testEntityID, "magistrala-service", "Magistrala service token")
if err != nil {
t.Fatalf("create access token failed: %v", err)
}
if created.CredentialID != testCredentialID || created.Token == "" {
t.Fatalf("unexpected access token response: %+v", created)
}
introspection, err := client.Introspect(context.Background(), runtimeToken)
if err != nil {
t.Fatalf("introspection failed: %v", err)
}
if !introspection.Active || introspection.EntityID != testEntityID || introspection.TenantID != testDomainID {
t.Fatalf("unexpected introspection response: %+v", introspection)
}
for _, operation := range []string{"authzCheck", "authorizedObjectIds", "createAccessToken", "introspect"} {
if !seen[operation] {
t.Fatalf("operation %q was not exercised", operation)
}
}
}
func TestCreateTenantMapsRouteToAlias(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != atomGraphQLPath {
+44 -1
View File
@@ -39,4 +39,47 @@ const (
const atomScopeModeObject = "object"
const atomGraphQLPath = "/graphql"
const (
atomObjectTypeResourceChannel = "resource:channel"
atomObjectTypeResourceRule = "resource:rule"
atomObjectTypeResourceReport = "resource:report"
atomObjectTypeResourceAlarm = "resource:alarm"
)
const atomDecisionAllow = "allow"
const (
atomInputKeyAction = "action"
atomInputKeyCredentialID = "credentialId"
atomInputKeyEntityID = "entityId"
atomInputKeyInput = "input"
atomInputKeyKind = "kind"
atomInputKeyName = "name"
atomInputKeyObjectKind = "objectKind"
atomInputKeySubjectID = "subjectId"
)
const (
atomContextDomainID = "domain_id"
atomContextLegacyObjectType = "legacy_object_type"
)
const (
atomAttributeCreatedAt = "created_at"
atomAttributeMetadata = "metadata"
atomAttributeRoute = "route"
atomAttributeSource = "source"
atomAttributeStatus = "status"
atomAttributeTags = "tags"
atomAttributeUpdatedAt = "updated_at"
atomAttributeUpdatedBy = "updated_by"
)
const atomAttributeSourceMagistrala = "magistrala"
const atomServiceTokenJournal = "journal"
const (
atomGraphQLPath = "/graphql"
atomAuthIntrospectPath = "/auth/introspect"
)
+61 -9
View File
@@ -5,6 +5,7 @@ package atom
import (
"context"
"fmt"
"net/http"
"strings"
@@ -14,14 +15,21 @@ import (
domainsv1 "github.com/absmach/magistrala/api/grpc/domains/v1"
smqauthn "github.com/absmach/magistrala/pkg/authn"
"github.com/absmach/magistrala/pkg/connections"
"github.com/absmach/magistrala/pkg/policies"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type clientsCompatAtomClient interface {
policyClient
policyWriter
LoginSharedKey(ctx context.Context, identifier, secret string) (LoginResponse, error)
}
type AtomClientsCompat struct {
Authn smqauthn.Authentication
Client *Client
Client clientsCompatAtomClient
}
func NewClientsCompat(authn smqauthn.Authentication, client ...*Client) clientsv1.ClientsServiceClient {
@@ -37,7 +45,7 @@ func (c AtomClientsCompat) Authenticate(ctx context.Context, in *clientsv1.Authn
if prefix, id, key, err := smqauthn.AuthUnpack(token); err == nil {
switch prefix {
case smqauthn.BasicAuth:
res, loginErr := c.Client.LoginPassword(ctx, id, key)
res, loginErr := c.Client.LoginSharedKey(ctx, id, key)
if loginErr == nil {
return &clientsv1.AuthnRes{Authenticated: true, Id: res.EntityID}, nil
}
@@ -71,12 +79,26 @@ func (c AtomClientsCompat) RetrieveEntities(context.Context, *commonv1.RetrieveE
return nil, status.Error(codes.Unimplemented, "atom clients compatibility only supports Authenticate")
}
func (c AtomClientsCompat) AddConnections(context.Context, *commonv1.AddConnectionsReq, ...grpc.CallOption) (*commonv1.AddConnectionsRes, error) {
return nil, status.Error(codes.Unimplemented, "atom clients compatibility only supports Authenticate")
func (c AtomClientsCompat) AddConnections(ctx context.Context, in *commonv1.AddConnectionsReq, _ ...grpc.CallOption) (*commonv1.AddConnectionsRes, error) {
prs, err := connectionPolicies(in.GetConnections())
if err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}
if err := NewPolicyService(c.Client).AddPolicies(ctx, prs); err != nil {
return nil, err
}
return &commonv1.AddConnectionsRes{Ok: true}, nil
}
func (c AtomClientsCompat) RemoveConnections(context.Context, *commonv1.RemoveConnectionsReq, ...grpc.CallOption) (*commonv1.RemoveConnectionsRes, error) {
return nil, status.Error(codes.Unimplemented, "atom clients compatibility only supports Authenticate")
func (c AtomClientsCompat) RemoveConnections(ctx context.Context, in *commonv1.RemoveConnectionsReq, _ ...grpc.CallOption) (*commonv1.RemoveConnectionsRes, error) {
prs, err := connectionPolicies(in.GetConnections())
if err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}
if err := NewPolicyService(c.Client).DeletePolicies(ctx, prs); err != nil {
return nil, err
}
return &commonv1.RemoveConnectionsRes{Ok: true}, nil
}
func (c AtomClientsCompat) RemoveChannelConnections(context.Context, *clientsv1.RemoveChannelConnectionsReq, ...grpc.CallOption) (*clientsv1.RemoveChannelConnectionsRes, error) {
@@ -87,6 +109,36 @@ func (c AtomClientsCompat) UnsetParentGroupFromClient(context.Context, *clientsv
return nil, status.Error(codes.Unimplemented, "atom clients compatibility only supports Authenticate")
}
func connectionPolicies(conns []*commonv1.Connection) ([]policies.Policy, error) {
prs := make([]policies.Policy, 0, len(conns))
for _, conn := range conns {
permission, err := connectionPermission(connections.ConnType(conn.GetType()))
if err != nil {
return nil, err
}
prs = append(prs, policies.Policy{
Domain: conn.GetDomainId(),
Subject: conn.GetClientId(),
SubjectType: policies.ClientType,
Object: conn.GetChannelId(),
ObjectType: policies.ChannelType,
Permission: permission,
})
}
return prs, nil
}
func connectionPermission(connType connections.ConnType) (string, error) {
switch connType {
case connections.Publish:
return policies.PublishPermission, nil
case connections.Subscribe:
return policies.SubscribePermission, nil
default:
return "", fmt.Errorf("unknown connection type %d", connType)
}
}
type AtomDomainsCompat struct {
Client *Client
}
@@ -148,7 +200,7 @@ func (c AtomChannelsCompat) Authorize(ctx context.Context, in *channelsv1.AuthzR
ObjectKind: atomObjectKindResource,
ObjectID: in.GetChannelId(),
Context: map[string]any{
"domain_id": in.GetDomainId(),
atomContextDomainID: in.GetDomainId(),
},
})
if err != nil {
@@ -183,11 +235,11 @@ func (c AtomChannelsCompat) RetrieveIDByRoute(ctx context.Context, in *commonv1.
return nil, err
}
for _, resource := range resources.Items {
if resource.Name == in.GetRoute() || attrString(resource.Attributes, "route") == in.GetRoute() {
if resource.Name == in.GetRoute() || attrString(resource.Attributes, atomAttributeRoute) == in.GetRoute() {
return &commonv1.RetrieveEntityRes{Entity: &commonv1.EntityBasic{
Id: resource.ID,
DomainId: resource.TenantID,
Status: atomStatusCode(attrString(resource.Attributes, "status")),
Status: atomStatusCode(attrString(resource.Attributes, atomAttributeStatus)),
}}, nil
}
}
+106 -6
View File
@@ -12,7 +12,9 @@ import (
"time"
clientsv1 "github.com/absmach/magistrala/api/grpc/clients/v1"
commonv1 "github.com/absmach/magistrala/api/grpc/common/v1"
smqauthn "github.com/absmach/magistrala/pkg/authn"
"github.com/absmach/magistrala/pkg/connections"
)
type recordingAuthn struct {
@@ -28,7 +30,15 @@ func (r *recordingAuthn) Authenticate(_ context.Context, token string) (smqauthn
return r.session, r.err
}
func TestAtomClientsCompatAuthenticatesBasicPasswordWithAtomLogin(t *testing.T) {
type fakeClientsCompatClient struct {
fakePolicyClient
}
func (f *fakeClientsCompatClient) LoginSharedKey(context.Context, string, string) (LoginResponse, error) {
return LoginResponse{}, nil
}
func TestAtomClientsCompatAuthenticatesBasicSharedKeyWithAtomLogin(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != "/auth/login" {
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
@@ -37,7 +47,7 @@ func TestAtomClientsCompatAuthenticatesBasicPasswordWithAtomLogin(t *testing.T)
if err := json.NewDecoder(r.Body).Decode(&got); err != nil {
t.Fatalf("decode login request: %v", err)
}
if got.Identifier != testEntityID || got.Secret != testDeviceSecret || got.Kind != "password" {
if got.Identifier != testEntityID || got.Secret != testDeviceSecret || got.Kind != "shared_key" {
t.Fatalf("unexpected login request: %+v", got)
}
_ = json.NewEncoder(w).Encode(LoginResponse{
@@ -55,17 +65,17 @@ func TestAtomClientsCompatAuthenticatesBasicPasswordWithAtomLogin(t *testing.T)
res, err := compat.Authenticate(context.Background(), &clientsv1.AuthnReq{Token: token})
if err != nil {
t.Fatalf("authenticate basic password: %v", err)
t.Fatalf("authenticate basic shared key: %v", err)
}
if !res.GetAuthenticated() || res.GetId() != testEntityID {
t.Fatalf("unexpected response: %+v", res)
}
if fallback.called {
t.Fatal("token fallback should not be called after successful Atom password login")
t.Fatal("token fallback should not be called after successful Atom shared-key login")
}
}
func TestAtomClientsCompatFallsBackToBearerTokenWhenBasicPasswordRejected(t *testing.T) {
func TestAtomClientsCompatFallsBackToBearerTokenWhenBasicSharedKeyRejected(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
http.Error(w, "invalid credentials", http.StatusUnauthorized)
}))
@@ -87,7 +97,7 @@ func TestAtomClientsCompatFallsBackToBearerTokenWhenBasicPasswordRejected(t *tes
}
}
func TestAtomClientsCompatDoesNotHideAtomPasswordLoginFailures(t *testing.T) {
func TestAtomClientsCompatDoesNotHideAtomSharedKeyLoginFailures(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
http.Error(w, "atom unavailable", http.StatusInternalServerError)
}))
@@ -105,3 +115,93 @@ func TestAtomClientsCompatDoesNotHideAtomPasswordLoginFailures(t *testing.T) {
t.Fatal("token fallback should not be called for non-authentication Atom failures")
}
}
func TestAtomClientsCompatRemoveConnectionsDeletesConnectionPolicies(t *testing.T) {
client := &fakeClientsCompatClient{
fakePolicyClient: fakePolicyClient{
capIDs: map[string]string{
atomActionPublish: "cap-publish",
atomActionSubscribe: "cap-subscribe",
},
policies: []DirectPolicy{
{
ID: "delete-publish",
TenantID: testDomainID,
SubjectKind: atomObjectKindEntity,
SubjectID: testDeviceID,
PermissionBlock: PermissionBlock{
ID: "publish-block",
ScopeMode: atomScopeModeObject,
ObjectKind: atomObjectKindResource,
ObjectType: "resource:channel",
ObjectID: "channel-1",
Actions: []Capability{{ID: "cap-publish"}},
},
},
{
ID: "delete-subscribe",
TenantID: testDomainID,
SubjectKind: atomObjectKindEntity,
SubjectID: testDeviceID,
PermissionBlock: PermissionBlock{
ID: "subscribe-block",
ScopeMode: atomScopeModeObject,
ObjectKind: atomObjectKindResource,
ObjectType: "resource:channel",
ObjectID: "channel-1",
Actions: []Capability{{ID: "cap-subscribe"}},
},
},
{
ID: "keep-other-channel",
TenantID: testDomainID,
SubjectKind: atomObjectKindEntity,
SubjectID: testDeviceID,
PermissionBlock: PermissionBlock{
ID: "other-block",
ScopeMode: atomScopeModeObject,
ObjectKind: atomObjectKindResource,
ObjectType: "resource:channel",
ObjectID: "other-channel",
Actions: []Capability{{ID: "cap-publish"}},
},
},
},
},
}
compat := AtomClientsCompat{Client: client}
res, err := compat.RemoveConnections(context.Background(), &commonv1.RemoveConnectionsReq{
Connections: []*commonv1.Connection{
{
ClientId: testDeviceID,
ChannelId: "channel-1",
DomainId: testDomainID,
Type: uint32(connections.Publish),
},
{
ClientId: testDeviceID,
ChannelId: "channel-1",
DomainId: testDomainID,
Type: uint32(connections.Subscribe),
},
},
})
if err != nil {
t.Fatalf("remove connections: %v", err)
}
if !res.GetOk() {
t.Fatal("expected ok response")
}
if len(client.directPolicyQueries) != 2 {
t.Fatalf("unexpected direct policy query count: %d", len(client.directPolicyQueries))
}
for _, q := range client.directPolicyQueries {
if q.TenantID != testDomainID || q.SubjectKind != atomObjectKindEntity || q.SubjectID != testDeviceID {
t.Fatalf("unexpected direct policy query: %+v", q)
}
}
if len(client.deleted) != 2 || client.deleted[0] != "delete-publish" || client.deleted[1] != "delete-subscribe" {
t.Fatalf("unexpected deleted policies: %+v", client.deleted)
}
}
+30 -30
View File
@@ -22,10 +22,10 @@ func TenantFromFields(f ObjectFields) Tenant {
CreatedBy: f.CreatedBy,
UpdatedBy: f.UpdatedBy,
Attributes: compact(Attributes{
"source": "magistrala",
"metadata": cloneMap(f.Metadata),
"created_at": timeString(f.CreatedAt),
"updated_at": timeString(f.UpdatedAt),
atomAttributeSource: atomAttributeSourceMagistrala,
atomAttributeMetadata: cloneMap(f.Metadata),
atomAttributeCreatedAt: timeString(f.CreatedAt),
atomAttributeUpdatedAt: timeString(f.UpdatedAt),
}),
}
}
@@ -38,15 +38,15 @@ func EntityFromFields(f ObjectFields) Entity {
TenantID: f.TenantID,
Status: entityStatus(f.Status),
Attributes: compact(Attributes{
"source": "magistrala",
"magistrala_kind": f.Kind,
"tags": cloneStrings(f.Tags),
"metadata": cloneMap(f.Metadata),
"private_metadata": cloneMap(f.Private),
"parent_group_id": f.ParentID,
"created_at": timeString(f.CreatedAt),
"updated_at": timeString(f.UpdatedAt),
"updated_by": f.UpdatedBy,
atomAttributeSource: atomAttributeSourceMagistrala,
"magistrala_kind": f.Kind,
atomAttributeTags: cloneStrings(f.Tags),
atomAttributeMetadata: cloneMap(f.Metadata),
"private_metadata": cloneMap(f.Private),
"parent_group_id": f.ParentID,
atomAttributeCreatedAt: timeString(f.CreatedAt),
atomAttributeUpdatedAt: timeString(f.UpdatedAt),
atomAttributeUpdatedBy: f.UpdatedBy,
}),
}
}
@@ -97,14 +97,14 @@ func GroupFromFields(f ObjectFields) Group {
ParentID: f.ParentID,
Status: entityStatus(f.Status),
Attributes: compact(Attributes{
"source": "magistrala",
"parent_id": f.ParentID,
"tags": cloneStrings(f.Tags),
"metadata": cloneMap(f.Metadata),
"status": f.Status,
"created_at": timeString(f.CreatedAt),
"updated_at": timeString(f.UpdatedAt),
"updated_by": f.UpdatedBy,
atomAttributeSource: atomAttributeSourceMagistrala,
"parent_id": f.ParentID,
atomAttributeTags: cloneStrings(f.Tags),
atomAttributeMetadata: cloneMap(f.Metadata),
atomAttributeStatus: f.Status,
atomAttributeCreatedAt: timeString(f.CreatedAt),
atomAttributeUpdatedAt: timeString(f.UpdatedAt),
atomAttributeUpdatedBy: f.UpdatedBy,
}),
}
}
@@ -117,15 +117,15 @@ func ResourceFromFields(f ObjectFields) Resource {
TenantID: f.TenantID,
OwnerID: f.OwnerID,
Attributes: compact(Attributes{
"source": "magistrala",
"status": f.Status,
"route": f.Route,
"parent_group_id": f.ParentID,
"tags": cloneStrings(f.Tags),
"metadata": cloneMap(f.Metadata),
"created_at": timeString(f.CreatedAt),
"updated_at": timeString(f.UpdatedAt),
"updated_by": f.UpdatedBy,
atomAttributeSource: atomAttributeSourceMagistrala,
atomAttributeStatus: f.Status,
atomAttributeRoute: f.Route,
"parent_group_id": f.ParentID,
atomAttributeTags: cloneStrings(f.Tags),
atomAttributeMetadata: cloneMap(f.Metadata),
atomAttributeCreatedAt: timeString(f.CreatedAt),
atomAttributeUpdatedAt: timeString(f.UpdatedAt),
atomAttributeUpdatedBy: f.UpdatedBy,
}),
}
}
+3 -3
View File
@@ -27,9 +27,9 @@ func (pe PolicyEvaluator) CheckPolicy(ctx context.Context, pr policies.Policy) e
ObjectKind: policyObjectKind(pr),
ObjectID: pr.Object,
Context: map[string]any{
"domain_id": pr.Domain,
"legacy_object_type": pr.ObjectType,
"legacy_relation": pr.Relation,
atomContextDomainID: pr.Domain,
atomContextLegacyObjectType: pr.ObjectType,
"legacy_relation": pr.Relation,
},
})
if err != nil {
+2 -2
View File
@@ -50,7 +50,7 @@ func (ps PolicyService) AddPolicy(ctx context.Context, pr policies.Policy) error
ObjectKind: policyGrantObjectKind(pr),
ObjectType: policyGrantObjectType(pr),
ObjectID: policyGrantObjectID(pr),
Effect: "allow",
Effect: atomDecisionAllow,
Conditions: map[string]any{},
ActionIDs: []string{capID},
})
@@ -241,7 +241,7 @@ func policyGrantObjectType(pr policies.Policy) string {
}
func policyGrantObjectID(pr policies.Policy) string {
if policyGrantScopeMode(pr) != "object" {
if policyGrantScopeMode(pr) != atomScopeModeObject {
return ""
}
return policyResourceID(pr)
+15 -9
View File
@@ -11,13 +11,15 @@ import (
)
type fakePolicyClient struct {
authorized AuthorizedObjectIDs
queries []AuthorizedObjectIDsQuery
capID string
blocks []CreatePermissionBlock
created []CreateDirectPolicy
policies []DirectPolicy
deleted []string
authorized AuthorizedObjectIDs
queries []AuthorizedObjectIDsQuery
directPolicyQueries []DirectPolicyQuery
capID string
capIDs map[string]string
blocks []CreatePermissionBlock
created []CreateDirectPolicy
policies []DirectPolicy
deleted []string
}
func (f *fakePolicyClient) AuthorizedObjectIDs(_ context.Context, q AuthorizedObjectIDsQuery) (AuthorizedObjectIDs, error) {
@@ -29,7 +31,10 @@ func (f *fakePolicyClient) CheckAuthz(context.Context, AuthzRequest) (AuthzRespo
return AuthzResponse{Allowed: true}, nil
}
func (f *fakePolicyClient) CapabilityID(context.Context, string) (string, error) {
func (f *fakePolicyClient) CapabilityID(_ context.Context, name string) (string, error) {
if f.capIDs != nil && f.capIDs[name] != "" {
return f.capIDs[name], nil
}
if f.capID == "" {
return "cap-publish", nil
}
@@ -56,7 +61,8 @@ func (f *fakePolicyClient) CreateDirectPolicy(_ context.Context, policy CreateDi
return DirectPolicy{ID: "policy-1", PermissionBlockID: policy.PermissionBlockID}, nil
}
func (f *fakePolicyClient) ListDirectPolicies(context.Context, DirectPolicyQuery) (DirectPolicyList, error) {
func (f *fakePolicyClient) ListDirectPolicies(_ context.Context, q DirectPolicyQuery) (DirectPolicyList, error) {
f.directPolicyQueries = append(f.directPolicyQueries, q)
return DirectPolicyList{Items: f.policies, Total: uint64(len(f.policies))}, nil
}
+7 -7
View File
@@ -40,7 +40,7 @@ func DefaultServiceTokenSpecs() []ServiceTokenSpec {
{Name: "fluxmq-node1", Env: "MG_ATOM_TOKEN_FLUXMQ_NODE1", Description: "Magistrala Docker Compose token for fluxmq-node1"},
{Name: "fluxmq-node2", Env: "MG_ATOM_TOKEN_FLUXMQ_NODE2", Description: "Magistrala Docker Compose token for fluxmq-node2"},
{Name: "fluxmq-node3", Env: "MG_ATOM_TOKEN_FLUXMQ_NODE3", Description: "Magistrala Docker Compose token for fluxmq-node3"},
{Name: "journal", Env: "MG_ATOM_TOKEN_JOURNAL", Description: "Magistrala Docker Compose token for journal"},
{Name: atomServiceTokenJournal, Env: "MG_ATOM_TOKEN_JOURNAL", Description: "Magistrala Docker Compose token for journal"},
{Name: "notifications", Env: "MG_ATOM_TOKEN_NOTIFICATIONS", Description: "Magistrala Docker Compose token for notifications"},
{Name: "timescale-reader", Env: "MG_ATOM_TOKEN_TIMESCALE_READER", Description: "Magistrala Docker Compose token for timescale-reader"},
{Name: "re", Env: "MG_ATOM_TOKEN_RE", Description: "Magistrala Docker Compose token for rule engine"},
@@ -89,21 +89,21 @@ func ProvisionServiceTokens(ctx context.Context, client *Client, opts TokenProvi
}
}
if token != "" && shouldRotate {
credentialID, ok := CredentialIDFromAPIKey(token)
credentialID, ok := CredentialIDFromAccessToken(token)
if ok {
if err := client.RevokeCredential(ctx, entityID, credentialID); err != nil && !IsNotFound(err) {
return TokenProvisionResult{}, fmt.Errorf("revoke %s credential %s: %w", spec.Env, credentialID, err)
}
}
}
created, err := client.CreateAPIKey(ctx, entityID, spec.Description)
created, err := client.CreateUnscopedAccessToken(ctx, entityID, spec.Name, spec.Description)
if err != nil {
return TokenProvisionResult{}, fmt.Errorf("create %s token: %w", spec.Env, err)
}
if strings.TrimSpace(created.Key) == "" {
return TokenProvisionResult{}, fmt.Errorf("create %s token: atom returned an empty key", spec.Env)
if strings.TrimSpace(created.Token) == "" {
return TokenProvisionResult{}, fmt.Errorf("create %s token: atom returned an empty token", spec.Env)
}
values[spec.Env] = created.Key
values[spec.Env] = created.Token
if shouldRotate {
result.Rotated = append(result.Rotated, spec.Env)
} else {
@@ -125,7 +125,7 @@ func (c *Client) TokenActive(ctx context.Context, token string) (bool, error) {
return res.Active, nil
}
func CredentialIDFromAPIKey(token string) (string, bool) {
func CredentialIDFromAccessToken(token string) (string, bool) {
rest, ok := strings.CutPrefix(strings.TrimSpace(token), "atom_")
if !ok {
return "", false
+27 -14
View File
@@ -52,7 +52,7 @@ func TestProvisionServiceTokensCreatesMissingToken(t *testing.T) {
}
func TestProvisionServiceTokensPreservesExistingActiveToken(t *testing.T) {
token := apiKeyForCredentialID("11111111-1111-1111-1111-111111111111")
token := accessTokenForCredentialID("11111111-1111-1111-1111-111111111111")
fake := newFakeAtomTokenServer(t, map[string]bool{token: true})
defer fake.Close()
@@ -72,7 +72,7 @@ func TestProvisionServiceTokensPreservesExistingActiveToken(t *testing.T) {
t.Fatalf("expected token to be preserved, got result %+v", result)
}
if len(fake.created) != 0 {
t.Fatalf("expected no new API key, got %d", len(fake.created))
t.Fatalf("expected no new access token, got %d", len(fake.created))
}
values, err := readTokenEnvFile(output)
if err != nil {
@@ -85,7 +85,7 @@ func TestProvisionServiceTokensPreservesExistingActiveToken(t *testing.T) {
func TestProvisionServiceTokensRotatesToken(t *testing.T) {
oldCredentialID := "11111111-1111-1111-1111-111111111111"
token := apiKeyForCredentialID(oldCredentialID)
token := accessTokenForCredentialID(oldCredentialID)
fake := newFakeAtomTokenServer(t, map[string]bool{token: true})
defer fake.Close()
@@ -117,16 +117,16 @@ func TestProvisionServiceTokensRotatesToken(t *testing.T) {
}
}
func TestCredentialIDFromAPIKey(t *testing.T) {
func TestCredentialIDFromAccessToken(t *testing.T) {
want := "11111111-2222-3333-4444-555555555555"
got, ok := CredentialIDFromAPIKey(apiKeyForCredentialID(want))
got, ok := CredentialIDFromAccessToken(accessTokenForCredentialID(want))
if !ok {
t.Fatalf("expected credential id to parse")
}
if got != want {
t.Fatalf("unexpected credential id: got %s want %s", got, want)
}
if _, ok := CredentialIDFromAPIKey("not-an-api-key"); ok {
if _, ok := CredentialIDFromAccessToken("not-an-access-token"); ok {
t.Fatalf("expected invalid token to be rejected")
}
}
@@ -164,7 +164,7 @@ func (f *fakeAtomTokenServer) Client() *Client {
func (f *fakeAtomTokenServer) handle(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/auth/introspect":
case atomAuthIntrospectPath:
token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
if err := json.NewEncoder(w).Encode(IntrospectionResponse{Active: f.active[token], EntityID: "entity-1"}); err != nil {
f.t.Fatalf("encode introspection response: %v", err)
@@ -189,22 +189,35 @@ func (f *fakeAtomTokenServer) handleGraphQL(w http.ResponseWriter, r *http.Reque
}
switch {
case strings.Contains(payload.Query, "createApiKey"):
case strings.Contains(payload.Query, "createAccessToken"):
input := payload.Variables["input"].(map[string]any)
f.created = append(f.created, input)
if input["name"] != testTokenSpec().Name || input["description"] != testTokenSpec().Description {
f.t.Fatalf("unexpected createAccessToken input: %+v", input)
}
if input["subjectId"] != DefaultServiceEntityID {
f.t.Fatalf("unexpected createAccessToken subject: %+v", input)
}
if scoped, ok := input["scoped"].(bool); !ok || scoped {
f.t.Fatalf("expected unscoped access token input, got %+v", input)
}
if permissions, ok := input["permissions"].([]any); !ok || len(permissions) != 0 {
f.t.Fatalf("expected empty permissions for unscoped access token, got %+v", input)
}
f.nextID++
credentialID := credentialIDForIndex(f.nextID)
key := apiKeyForCredentialID(credentialID)
f.active[key] = true
token := accessTokenForCredentialID(credentialID)
f.active[token] = true
if err := json.NewEncoder(w).Encode(map[string]any{
"data": map[string]any{
"createApiKey": APIKeyResponse{
"createAccessToken": AccessTokenResponse{
CredentialID: credentialID,
Key: key,
Token: token,
Name: testTokenSpec().Name,
},
},
}); err != nil {
f.t.Fatalf("encode create API key response: %v", err)
f.t.Fatalf("encode create access token response: %v", err)
}
case strings.Contains(payload.Query, "revokeCredential"):
credentialID := payload.Variables["credentialId"].(string)
@@ -223,7 +236,7 @@ func testTokenSpec() ServiceTokenSpec {
return ServiceTokenSpec{Name: "journal", Env: "MG_ATOM_TOKEN_JOURNAL", Description: "test journal token"}
}
func apiKeyForCredentialID(id string) string {
func accessTokenForCredentialID(id string) string {
return "atom_" + strings.ReplaceAll(id, "-", "") + "_" + strings.Repeat("a", 64)
}
+3 -3
View File
@@ -54,9 +54,9 @@ func TestTokenVerifierRejectsExpiredToken(t *testing.T) {
}
}
func TestTokenVerifierIntrospectsAtomAPIKey(t *testing.T) {
func TestTokenVerifierIntrospectsAtomAccessToken(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/auth/introspect" || r.Header.Get("Authorization") != "Bearer atom_test" {
if r.URL.Path != atomAuthIntrospectPath || r.Header.Get("Authorization") != "Bearer atom_test" {
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
}
_ = json.NewEncoder(w).Encode(IntrospectionResponse{
@@ -69,7 +69,7 @@ func TestTokenVerifierIntrospectsAtomAPIKey(t *testing.T) {
claims, err := NewTokenVerifier(Config{URL: srv.URL, JWKSURL: srv.URL + "/jwks", Timeout: time.Second}).VerifyTokenClaims(context.Background(), "atom_test")
if err != nil {
t.Fatalf("verify api key: %v", err)
t.Fatalf("verify access token: %v", err)
}
if claims.SubjectID != "entity-2" || claims.TenantID != "tenant-2" {
t.Fatalf("unexpected claims: %+v", claims)
+3 -2
View File
@@ -232,9 +232,10 @@ type LoginResponse struct {
ExpiresAt time.Time `json:"expires_at"`
}
type APIKeyResponse struct {
type AccessTokenResponse struct {
CredentialID string `json:"credentialId"`
Key string `json:"key"`
Token string `json:"token"`
Name string `json:"name,omitempty"`
ExpiresAt *time.Time `json:"expiresAt,omitempty"`
}