534 lines
16 KiB
Go
534 lines
16 KiB
Go
// Copyright (c) Mainflux
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package auth_test
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/mainflux/mainflux/auth"
|
|
"github.com/mainflux/mainflux/auth/jwt"
|
|
"github.com/mainflux/mainflux/auth/mocks"
|
|
"github.com/mainflux/mainflux/pkg/errors"
|
|
"github.com/mainflux/mainflux/pkg/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/mock"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
var idProvider = uuid.New()
|
|
|
|
const (
|
|
secret = "secret"
|
|
email = "test@example.com"
|
|
id = "testID"
|
|
groupName = "mfx"
|
|
description = "Description"
|
|
|
|
memberRelation = "member"
|
|
authoritiesObj = "authorities"
|
|
loginDuration = 30 * time.Minute
|
|
refreshDuration = 24 * time.Hour
|
|
accessToken = "access"
|
|
)
|
|
|
|
func newService() (auth.Service, *mocks.Keys) {
|
|
krepo := new(mocks.Keys)
|
|
prepo := new(mocks.PolicyAgent)
|
|
idProvider := uuid.NewMock()
|
|
|
|
t := jwt.New([]byte(secret))
|
|
|
|
return auth.New(krepo, idProvider, t, prepo, loginDuration, refreshDuration), krepo
|
|
}
|
|
|
|
func TestIssue(t *testing.T) {
|
|
svc, krepo := newService()
|
|
|
|
cases := []struct {
|
|
desc string
|
|
key auth.Key
|
|
token string
|
|
err error
|
|
}{
|
|
{
|
|
desc: "issue login key",
|
|
key: auth.Key{
|
|
Type: auth.AccessKey,
|
|
IssuedAt: time.Now(),
|
|
},
|
|
token: accessToken,
|
|
err: nil,
|
|
},
|
|
{
|
|
desc: "issue login key with no time",
|
|
key: auth.Key{
|
|
Type: auth.AccessKey,
|
|
},
|
|
token: accessToken,
|
|
err: auth.ErrInvalidKeyIssuedAt,
|
|
},
|
|
{
|
|
desc: "issue API key",
|
|
key: auth.Key{
|
|
Type: auth.APIKey,
|
|
IssuedAt: time.Now(),
|
|
},
|
|
token: accessToken,
|
|
err: nil,
|
|
},
|
|
{
|
|
desc: "issue API key with an invalid token",
|
|
key: auth.Key{
|
|
Type: auth.APIKey,
|
|
IssuedAt: time.Now(),
|
|
},
|
|
token: "invalid",
|
|
err: errors.ErrAuthentication,
|
|
},
|
|
{
|
|
desc: "issue API key with no time",
|
|
key: auth.Key{
|
|
Type: auth.APIKey,
|
|
},
|
|
token: accessToken,
|
|
err: auth.ErrInvalidKeyIssuedAt,
|
|
},
|
|
{
|
|
desc: "issue recovery key",
|
|
key: auth.Key{
|
|
Type: auth.RecoveryKey,
|
|
IssuedAt: time.Now(),
|
|
},
|
|
token: "",
|
|
err: nil,
|
|
},
|
|
{
|
|
desc: "issue recovery with no issue time",
|
|
key: auth.Key{
|
|
Type: auth.RecoveryKey,
|
|
},
|
|
token: accessToken,
|
|
err: auth.ErrInvalidKeyIssuedAt,
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
repocall := krepo.On("Save", mock.Anything, mock.Anything).Return(mock.Anything, tc.err)
|
|
_, err := svc.Issue(context.Background(), tc.token, tc.key)
|
|
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.err, err))
|
|
repocall.Unset()
|
|
}
|
|
}
|
|
|
|
func TestRevoke(t *testing.T) {
|
|
svc, _ := newService()
|
|
secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.AccessKey, IssuedAt: time.Now(), Subject: id})
|
|
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
|
key := auth.Key{
|
|
Type: auth.APIKey,
|
|
IssuedAt: time.Now(),
|
|
Subject: id,
|
|
}
|
|
_, err = svc.Issue(context.Background(), secret.AccessToken, key)
|
|
assert.Nil(t, err, fmt.Sprintf("Issuing user's key expected to succeed: %s", err))
|
|
|
|
cases := []struct {
|
|
desc string
|
|
id string
|
|
token string
|
|
err error
|
|
}{
|
|
{
|
|
desc: "revoke login key",
|
|
// id: newKey.ID,
|
|
token: secret.AccessToken,
|
|
err: nil,
|
|
},
|
|
{
|
|
desc: "revoke non-existing login key",
|
|
// id: newKey.ID,
|
|
token: secret.AccessToken,
|
|
err: nil,
|
|
},
|
|
{
|
|
desc: "revoke with empty login key",
|
|
// id: newKey.ID,
|
|
token: "",
|
|
err: errors.ErrAuthentication,
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
err := svc.Revoke(context.Background(), tc.token, tc.id)
|
|
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.err, err))
|
|
}
|
|
}
|
|
|
|
func TestRetrieve(t *testing.T) {
|
|
svc, _ := newService()
|
|
secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.AccessKey, IssuedAt: time.Now(), Subject: id})
|
|
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
|
key := auth.Key{
|
|
ID: "id",
|
|
Type: auth.APIKey,
|
|
Subject: id,
|
|
IssuedAt: time.Now(),
|
|
}
|
|
|
|
userToken, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.AccessKey, IssuedAt: time.Now(), Subject: id})
|
|
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
|
|
|
apiToken, err := svc.Issue(context.Background(), secret.AccessToken, key)
|
|
assert.Nil(t, err, fmt.Sprintf("Issuing login's key expected to succeed: %s", err))
|
|
|
|
resetToken, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.RecoveryKey, IssuedAt: time.Now()})
|
|
assert.Nil(t, err, fmt.Sprintf("Issuing reset key expected to succeed: %s", err))
|
|
|
|
cases := []struct {
|
|
desc string
|
|
id string
|
|
token string
|
|
err error
|
|
}{
|
|
{
|
|
desc: "retrieve login key",
|
|
// id: apiKey.ID,
|
|
token: userToken.AccessToken,
|
|
err: nil,
|
|
},
|
|
{
|
|
desc: "retrieve non-existing login key",
|
|
id: "invalid",
|
|
token: userToken.AccessToken,
|
|
err: errors.ErrNotFound,
|
|
},
|
|
{
|
|
desc: "retrieve with wrong login key",
|
|
// id: apiKey.ID,
|
|
token: "wrong",
|
|
err: errors.ErrAuthentication,
|
|
},
|
|
{
|
|
desc: "retrieve with API token",
|
|
// id: apiKey.ID,
|
|
token: apiToken.AccessToken,
|
|
err: errors.ErrAuthentication,
|
|
},
|
|
{
|
|
desc: "retrieve with reset token",
|
|
// id: apiKey.ID,
|
|
token: resetToken.AccessToken,
|
|
err: errors.ErrAuthentication,
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
_, err := svc.RetrieveKey(context.Background(), tc.token, tc.id)
|
|
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.err, err))
|
|
}
|
|
}
|
|
|
|
func TestIdentify(t *testing.T) {
|
|
svc, _ := newService()
|
|
|
|
loginSecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.AccessKey, IssuedAt: time.Now(), Subject: id})
|
|
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
|
|
|
recoverySecret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.RecoveryKey, IssuedAt: time.Now(), Subject: id})
|
|
assert.Nil(t, err, fmt.Sprintf("Issuing reset key expected to succeed: %s", err))
|
|
|
|
apiSecret, err := svc.Issue(context.Background(), loginSecret.AccessToken, auth.Key{Type: auth.APIKey, Subject: id, IssuedAt: time.Now(), ExpiresAt: time.Now().Add(time.Minute)})
|
|
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
|
|
|
exp1 := time.Now().Add(-2 * time.Second)
|
|
expSecret, err := svc.Issue(context.Background(), loginSecret.AccessToken, auth.Key{Type: auth.APIKey, IssuedAt: time.Now(), ExpiresAt: exp1})
|
|
assert.Nil(t, err, fmt.Sprintf("Issuing expired login key expected to succeed: %s", err))
|
|
|
|
invalidSecret, err := svc.Issue(context.Background(), loginSecret.AccessToken, auth.Key{Type: 22, IssuedAt: time.Now()})
|
|
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
|
|
|
cases := []struct {
|
|
desc string
|
|
key string
|
|
idt string
|
|
err error
|
|
}{
|
|
{
|
|
desc: "identify login key",
|
|
key: loginSecret.AccessToken,
|
|
idt: id,
|
|
err: nil,
|
|
},
|
|
{
|
|
desc: "identify recovery key",
|
|
key: recoverySecret.AccessToken,
|
|
idt: id,
|
|
err: nil,
|
|
},
|
|
{
|
|
desc: "identify API key",
|
|
key: apiSecret.AccessToken,
|
|
idt: id,
|
|
err: nil,
|
|
},
|
|
{
|
|
desc: "identify expired API key",
|
|
key: expSecret.AccessToken,
|
|
idt: "",
|
|
err: auth.ErrAPIKeyExpired,
|
|
},
|
|
{
|
|
desc: "identify expired key",
|
|
key: invalidSecret.AccessToken,
|
|
idt: "",
|
|
err: errors.ErrAuthentication,
|
|
},
|
|
{
|
|
desc: "identify invalid key",
|
|
key: "invalid",
|
|
idt: "",
|
|
err: errors.ErrAuthentication,
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
idt, err := svc.Identify(context.Background(), tc.key)
|
|
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.err, err))
|
|
assert.Equal(t, tc.idt, idt, fmt.Sprintf("%s expected %s got %s\n", tc.desc, tc.idt, idt))
|
|
}
|
|
}
|
|
|
|
func TestAuthorize(t *testing.T) {
|
|
svc, _ := newService()
|
|
|
|
pr := auth.PolicyReq{Object: authoritiesObj, Relation: memberRelation, Subject: id}
|
|
err := svc.Authorize(context.Background(), pr)
|
|
require.Nil(t, err, fmt.Sprintf("authorizing initial %v policy expected to succeed: %s", pr, err))
|
|
}
|
|
|
|
func TestAddPolicy(t *testing.T) {
|
|
svc, _ := newService()
|
|
|
|
pr := auth.PolicyReq{Object: "obj", Relation: "rel", Subject: "sub"}
|
|
err := svc.AddPolicy(context.Background(), pr)
|
|
require.Nil(t, err, fmt.Sprintf("adding %v policy expected to succeed: %v", pr, err))
|
|
|
|
err = svc.Authorize(context.Background(), pr)
|
|
require.Nil(t, err, fmt.Sprintf("checking shared %v policy expected to be succeed: %#v", pr, err))
|
|
}
|
|
|
|
func TestDeletePolicy(t *testing.T) {
|
|
svc, _ := newService()
|
|
|
|
pr := auth.PolicyReq{Object: authoritiesObj, Relation: memberRelation, Subject: id}
|
|
err := svc.DeletePolicy(context.Background(), pr)
|
|
require.Nil(t, err, fmt.Sprintf("deleting %v policy expected to succeed: %s", pr, err))
|
|
}
|
|
|
|
func TestAddPolicies(t *testing.T) {
|
|
svc, _ := newService()
|
|
secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.AccessKey, IssuedAt: time.Now(), Subject: id})
|
|
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
|
|
|
key := auth.Key{
|
|
ID: "id",
|
|
Type: auth.APIKey,
|
|
Subject: id,
|
|
IssuedAt: time.Now(),
|
|
}
|
|
|
|
apiToken, err := svc.Issue(context.Background(), secret.AccessToken, key)
|
|
assert.Nil(t, err, fmt.Sprintf("Issuing user's key expected to succeed: %s", err))
|
|
|
|
thingID, err := idProvider.ID()
|
|
assert.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
|
|
|
tmpID := "tmpid"
|
|
readPolicy := "read"
|
|
writePolicy := "write"
|
|
deletePolicy := "delete"
|
|
|
|
// Add read policy to users.
|
|
err = svc.AddPolicies(context.Background(), apiToken.AccessToken, thingID, []string{id, tmpID}, []string{readPolicy})
|
|
assert.Nil(t, err, fmt.Sprintf("adding policies expected to succeed: %s", err))
|
|
|
|
// Add write and delete policies to users.
|
|
err = svc.AddPolicies(context.Background(), apiToken.AccessToken, thingID, []string{id, tmpID}, []string{writePolicy, deletePolicy})
|
|
assert.Nil(t, err, fmt.Sprintf("adding multiple policies expected to succeed: %s", err))
|
|
|
|
cases := []struct {
|
|
desc string
|
|
policy auth.PolicyReq
|
|
err error
|
|
}{
|
|
{
|
|
desc: "check valid 'read' policy of user with id",
|
|
policy: auth.PolicyReq{Object: thingID, Relation: readPolicy, Subject: id},
|
|
err: nil,
|
|
},
|
|
{
|
|
desc: "check valid 'write' policy of user with id",
|
|
policy: auth.PolicyReq{Object: thingID, Relation: writePolicy, Subject: id},
|
|
err: nil,
|
|
},
|
|
{
|
|
desc: "check valid 'delete' policy of user with id",
|
|
policy: auth.PolicyReq{Object: thingID, Relation: deletePolicy, Subject: id},
|
|
err: nil,
|
|
},
|
|
{
|
|
desc: "check valid 'read' policy of user with tmpid",
|
|
policy: auth.PolicyReq{Object: thingID, Relation: readPolicy, Subject: tmpID},
|
|
err: nil,
|
|
},
|
|
{
|
|
desc: "check valid 'write' policy of user with tmpid",
|
|
policy: auth.PolicyReq{Object: thingID, Relation: writePolicy, Subject: tmpID},
|
|
err: nil,
|
|
},
|
|
{
|
|
desc: "check valid 'delete' policy of user with tmpid",
|
|
policy: auth.PolicyReq{Object: thingID, Relation: deletePolicy, Subject: tmpID},
|
|
err: nil,
|
|
},
|
|
{
|
|
desc: "check invalid 'access' policy of user with id",
|
|
policy: auth.PolicyReq{Object: thingID, Relation: "access", Subject: id},
|
|
err: errors.ErrAuthorization,
|
|
},
|
|
{
|
|
desc: "check invalid 'access' policy of user with tmpid",
|
|
policy: auth.PolicyReq{Object: thingID, Relation: "access", Subject: tmpID},
|
|
err: errors.ErrAuthorization,
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
err := svc.Authorize(context.Background(), tc.policy)
|
|
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %v, got %v", tc.desc, tc.err, err))
|
|
}
|
|
}
|
|
|
|
func TestDeletePolicies(t *testing.T) {
|
|
svc, _ := newService()
|
|
secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.AccessKey, IssuedAt: time.Now(), Subject: id})
|
|
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
|
|
|
key := auth.Key{
|
|
ID: "id",
|
|
Type: auth.APIKey,
|
|
Subject: id,
|
|
IssuedAt: time.Now(),
|
|
}
|
|
|
|
apiToken, err := svc.Issue(context.Background(), secret.AccessToken, key)
|
|
assert.Nil(t, err, fmt.Sprintf("Issuing user's key expected to succeed: %s", err))
|
|
|
|
thingID, err := idProvider.ID()
|
|
assert.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))
|
|
|
|
tmpID := "tmpid"
|
|
readPolicy := "read"
|
|
writePolicy := "write"
|
|
deletePolicy := "delete"
|
|
memberPolicy := "member"
|
|
|
|
// Add read, write and delete policies to users.
|
|
err = svc.AddPolicies(context.Background(), apiToken.AccessToken, thingID, []string{id, tmpID}, []string{readPolicy, writePolicy, deletePolicy, memberPolicy})
|
|
assert.Nil(t, err, fmt.Sprintf("adding policies expected to succeed: %s", err))
|
|
|
|
// Delete multiple policies from single user.
|
|
err = svc.DeletePolicies(context.Background(), apiToken.AccessToken, thingID, []string{id}, []string{readPolicy, writePolicy})
|
|
assert.Nil(t, err, fmt.Sprintf("deleting policies from single user expected to succeed: %s", err))
|
|
|
|
// Delete multiple policies from multiple user.
|
|
err = svc.DeletePolicies(context.Background(), apiToken.AccessToken, thingID, []string{id, tmpID}, []string{deletePolicy, memberPolicy})
|
|
assert.Nil(t, err, fmt.Sprintf("deleting policies from multiple user expected to succeed: %s", err))
|
|
|
|
cases := []struct {
|
|
desc string
|
|
policy auth.PolicyReq
|
|
err error
|
|
}{
|
|
{
|
|
desc: "check non-existing 'read' policy of user with id",
|
|
policy: auth.PolicyReq{Object: thingID, Relation: readPolicy, Subject: id},
|
|
err: errors.ErrAuthorization,
|
|
},
|
|
{
|
|
desc: "check non-existing 'write' policy of user with id",
|
|
policy: auth.PolicyReq{Object: thingID, Relation: writePolicy, Subject: id},
|
|
err: errors.ErrAuthorization,
|
|
},
|
|
{
|
|
desc: "check non-existing 'delete' policy of user with id",
|
|
policy: auth.PolicyReq{Object: thingID, Relation: deletePolicy, Subject: id},
|
|
err: errors.ErrAuthorization,
|
|
},
|
|
{
|
|
desc: "check non-existing 'member' policy of user with id",
|
|
policy: auth.PolicyReq{Object: thingID, Relation: memberPolicy, Subject: id},
|
|
err: errors.ErrAuthorization,
|
|
},
|
|
{
|
|
desc: "check non-existing 'delete' policy of user with tmpid",
|
|
policy: auth.PolicyReq{Object: thingID, Relation: deletePolicy, Subject: tmpID},
|
|
err: errors.ErrAuthorization,
|
|
},
|
|
{
|
|
desc: "check non-existing 'member' policy of user with tmpid",
|
|
policy: auth.PolicyReq{Object: thingID, Relation: memberPolicy, Subject: tmpID},
|
|
err: errors.ErrAuthorization,
|
|
},
|
|
{
|
|
desc: "check valid 'read' policy of user with tmpid",
|
|
policy: auth.PolicyReq{Object: thingID, Relation: readPolicy, Subject: tmpID},
|
|
err: nil,
|
|
},
|
|
{
|
|
desc: "check valid 'write' policy of user with tmpid",
|
|
policy: auth.PolicyReq{Object: thingID, Relation: writePolicy, Subject: tmpID},
|
|
err: nil,
|
|
},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
err := svc.Authorize(context.Background(), tc.policy)
|
|
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %v, got %v", tc.desc, tc.err, err))
|
|
}
|
|
}
|
|
|
|
func TestListPolicies(t *testing.T) {
|
|
svc, _ := newService()
|
|
secret, err := svc.Issue(context.Background(), "", auth.Key{Type: auth.AccessKey, IssuedAt: time.Now(), Subject: id})
|
|
assert.Nil(t, err, fmt.Sprintf("Issuing login key expected to succeed: %s", err))
|
|
|
|
key := auth.Key{
|
|
ID: "id",
|
|
Type: auth.APIKey,
|
|
Subject: id,
|
|
IssuedAt: time.Now(),
|
|
}
|
|
|
|
apiToken, err := svc.Issue(context.Background(), secret.AccessToken, key)
|
|
assert.Nil(t, err, fmt.Sprintf("Issuing user's key expected to succeed: %s", err))
|
|
|
|
readPolicy := "read"
|
|
pageLen := 15
|
|
|
|
// Add arbitrary policies to the user.
|
|
for i := 0; i < pageLen; i++ {
|
|
err = svc.AddPolicies(context.Background(), apiToken.AccessToken, fmt.Sprintf("thing-%d", i), []string{id}, []string{readPolicy})
|
|
assert.Nil(t, err, fmt.Sprintf("adding policies expected to succeed: %s", err))
|
|
}
|
|
|
|
page, err := svc.ListObjects(context.Background(), auth.PolicyReq{Subject: id, Relation: readPolicy}, "", 100)
|
|
assert.Nil(t, err, fmt.Sprintf("listing policies expected to succeed: %s", err))
|
|
assert.Equal(t, pageLen, len(page.Policies), fmt.Sprintf("unexpected listing page size, expected %d, got %d: %v", pageLen, len(page.Policies), err))
|
|
|
|
}
|