// 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" readPolicy = "read" writePolicy = "write" deletePolicy = "delete" ) 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" // 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" 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)) 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)) }