mirror of
https://github.com/absmach/magistrala.git
synced 2026-08-07 07:14:46 +00:00
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
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:
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user