Mainflux.mainflux/auth/service.go

423 lines
12 KiB
Go

// Copyright (c) Mainflux
// SPDX-License-Identifier: Apache-2.0
package auth
import (
"context"
"fmt"
"time"
"github.com/mainflux/mainflux"
"github.com/mainflux/mainflux/pkg/errors"
)
const (
recoveryDuration = 5 * time.Minute
thingsKind = "things"
channelsKind = "channels"
usersKind = "users"
thingType = "thing"
channelType = "channel"
userType = "user"
groupType = "group"
memberRelation = "member"
groupRelation = "group"
administratorRelation = "administrator"
parentGroupRelation = "parent_group"
viewerRelation = "viewer"
mainfluxObject = "mainflux"
refreshToken = "refresh_token"
)
const (
tokenKind = "token"
idKind = "id"
)
var (
// ErrFailedToRetrieveMembers failed to retrieve group members.
ErrFailedToRetrieveMembers = errors.New("failed to retrieve group members")
// ErrFailedToRetrieveMembership failed to retrieve memberships.
ErrFailedToRetrieveMembership = errors.New("failed to retrieve memberships")
// ErrFailedToRetrieveAll failed to retrieve groups.
ErrFailedToRetrieveAll = errors.New("failed to retrieve all groups")
// ErrFailedToRetrieveParents failed to retrieve groups.
ErrFailedToRetrieveParents = errors.New("failed to retrieve all groups")
// ErrFailedToRetrieveChildren failed to retrieve groups.
ErrFailedToRetrieveChildren = errors.New("failed to retrieve all groups")
errIssueUser = errors.New("failed to issue new login key")
errIssueTmp = errors.New("failed to issue new temporary key")
errRevoke = errors.New("failed to remove key")
errRetrieve = errors.New("failed to retrieve key data")
errIdentify = errors.New("failed to validate token")
)
// Authn specifies an API that must be fullfiled by the domain service
// implementation, and all of its decorators (e.g. logging & metrics).
// Token is a string value of the actual Key and is used to authenticate
// an Auth service request.
type Authn interface {
// Issue issues a new Key, returning its token value alongside.
Issue(ctx context.Context, token string, key Key) (Token, error)
// Revoke removes the Key with the provided id that is
// issued by the user identified by the provided key.
Revoke(ctx context.Context, token, id string) error
// RetrieveKey retrieves data for the Key identified by the provided
// ID, that is issued by the user identified by the provided key.
RetrieveKey(ctx context.Context, token, id string) (Key, error)
// Identify validates token token. If token is valid, content
// is returned. If token is invalid, or invocation failed for some
// other reason, non-nil error value is returned in response.
Identify(ctx context.Context, token string) (string, error)
}
// Service specifies an API that must be fulfilled by the domain service
// implementation, and all of its decorators (e.g. logging & metrics).
// Token is a string value of the actual Key and is used to authenticate
// an Auth service request.
type Service interface {
Authn
Authz
}
var _ Service = (*service)(nil)
type service struct {
keys KeyRepository
idProvider mainflux.IDProvider
agent PolicyAgent
tokenizer Tokenizer
loginDuration time.Duration
refreshDuration time.Duration
}
// New instantiates the auth service implementation.
func New(keys KeyRepository, idp mainflux.IDProvider, tokenizer Tokenizer, policyAgent PolicyAgent, loginDuration, refreshDuration time.Duration) Service {
return &service{
tokenizer: tokenizer,
keys: keys,
idProvider: idp,
agent: policyAgent,
loginDuration: loginDuration,
refreshDuration: refreshDuration,
}
}
func (svc service) Issue(ctx context.Context, token string, key Key) (Token, error) {
key.IssuedAt = time.Now().UTC()
switch key.Type {
case APIKey:
return svc.userKey(ctx, token, key)
case RefreshKey:
return svc.refreshKey(ctx, token, key)
case RecoveryKey:
return svc.tmpKey(recoveryDuration, key)
default:
return svc.accessKey(key)
}
}
func (svc service) Revoke(ctx context.Context, token, id string) error {
issuerID, _, err := svc.authenticate(token)
if err != nil {
return errors.Wrap(errRevoke, err)
}
if err := svc.keys.Remove(ctx, issuerID, id); err != nil {
return errors.Wrap(errRevoke, err)
}
return nil
}
func (svc service) RetrieveKey(ctx context.Context, token, id string) (Key, error) {
issuerID, _, err := svc.authenticate(token)
if err != nil {
return Key{}, errors.Wrap(errRetrieve, err)
}
return svc.keys.Retrieve(ctx, issuerID, id)
}
func (svc service) Identify(ctx context.Context, token string) (string, error) {
key, err := svc.tokenizer.Parse(token)
if err == ErrAPIKeyExpired {
err = svc.keys.Remove(ctx, key.Issuer, key.ID)
return "", errors.Wrap(ErrAPIKeyExpired, err)
}
if err != nil {
return "", errors.Wrap(errIdentify, err)
}
switch key.Type {
case RecoveryKey, AccessKey:
return key.Subject, nil
case APIKey:
_, err := svc.keys.Retrieve(ctx, key.Issuer, key.ID)
if err != nil {
return "", errors.ErrAuthentication
}
return key.Subject, nil
default:
return "", errors.ErrAuthentication
}
}
func (svc service) Authorize(ctx context.Context, pr PolicyReq) error {
if pr.SubjectKind == tokenKind {
id, err := svc.Identify(ctx, pr.Subject)
if err != nil {
return err
}
pr.Subject = id
}
if err := svc.agent.CheckPolicy(ctx, pr); err != nil {
return errors.Wrap(errors.ErrAuthorization, err)
}
return nil
}
func (svc service) AddPolicy(ctx context.Context, pr PolicyReq) error {
return svc.agent.AddPolicy(ctx, pr)
}
// Yet to do.
func (svc service) AddPolicies(ctx context.Context, token, object string, subjectIDs, relations []string) error {
user, err := svc.Identify(ctx, token)
if err != nil {
return err
}
if err := svc.Authorize(ctx, PolicyReq{Object: mainfluxObject, Subject: user}); err != nil {
return err
}
var errs error
for _, subjectID := range subjectIDs {
for _, relation := range relations {
if err := svc.AddPolicy(ctx, PolicyReq{Object: object, Relation: relation, Subject: subjectID}); err != nil {
errs = errors.Wrap(fmt.Errorf("cannot add '%s' policy on object '%s' for subject '%s': %w", relation, object, subjectID, err), errs)
}
}
}
return errs
}
func (svc service) DeletePolicy(ctx context.Context, pr PolicyReq) error {
return svc.agent.DeletePolicy(ctx, pr)
}
// Yet to do.
func (svc service) DeletePolicies(ctx context.Context, token, object string, subjectIDs, relations []string) error {
user, err := svc.Identify(ctx, token)
if err != nil {
return err
}
// Check if the user identified by token is the admin.
if err := svc.Authorize(ctx, PolicyReq{Object: mainfluxObject, Subject: user}); err != nil {
return err
}
var errs error
for _, subjectID := range subjectIDs {
for _, relation := range relations {
if err := svc.DeletePolicy(ctx, PolicyReq{Object: object, Relation: relation, Subject: subjectID}); err != nil {
errs = errors.Wrap(fmt.Errorf("cannot delete '%s' policy on object '%s' for subject '%s': %w", relation, object, subjectID, err), errs)
}
}
}
return errs
}
func (svc service) AssignGroupAccessRights(ctx context.Context, token, thingGroupID, userGroupID string) error {
if _, err := svc.Identify(ctx, token); err != nil {
return err
}
return svc.agent.AddPolicy(ctx, PolicyReq{SubjectType: groupType, Subject: userGroupID, Relation: groupRelation, ObjectType: groupType, Object: thingGroupID})
}
func (svc service) ListObjects(ctx context.Context, pr PolicyReq, nextPageToken string, limit int32) (PolicyPage, error) {
if limit <= 0 {
limit = 100
}
res, npt, err := svc.agent.RetrieveObjects(ctx, pr, nextPageToken, limit)
if err != nil {
return PolicyPage{}, err
}
var page PolicyPage
for _, tuple := range res {
page.Policies = append(page.Policies, tuple.Object)
}
page.NextPageToken = npt
return page, err
}
func (svc service) ListAllObjects(ctx context.Context, pr PolicyReq) (PolicyPage, error) {
res, err := svc.agent.RetrieveAllObjects(ctx, pr)
if err != nil {
return PolicyPage{}, err
}
var page PolicyPage
for _, tuple := range res {
page.Policies = append(page.Policies, tuple.Object)
}
return page, err
}
func (svc service) CountObjects(ctx context.Context, pr PolicyReq) (int, error) {
return svc.agent.RetrieveAllObjectsCount(ctx, pr)
}
func (svc service) ListSubjects(ctx context.Context, pr PolicyReq, nextPageToken string, limit int32) (PolicyPage, error) {
if limit <= 0 {
limit = 100
}
res, npt, err := svc.agent.RetrieveSubjects(ctx, pr, nextPageToken, limit)
if err != nil {
return PolicyPage{}, err
}
var page PolicyPage
for _, tuple := range res {
page.Policies = append(page.Policies, tuple.Subject)
}
page.NextPageToken = npt
return page, err
}
func (svc service) ListAllSubjects(ctx context.Context, pr PolicyReq) (PolicyPage, error) {
res, err := svc.agent.RetrieveAllSubjects(ctx, pr)
if err != nil {
return PolicyPage{}, err
}
var page PolicyPage
for _, tuple := range res {
page.Policies = append(page.Policies, tuple.Subject)
}
return page, err
}
func (svc service) CountSubjects(ctx context.Context, pr PolicyReq) (int, error) {
return svc.agent.RetrieveAllSubjectsCount(ctx, pr)
}
func (svc service) tmpKey(duration time.Duration, key Key) (Token, error) {
value, err := svc.tokenizer.Issue(key)
if err != nil {
return Token{}, errors.Wrap(errIssueTmp, err)
}
return Token{AccessToken: value}, nil
}
func (svc service) accessKey(key Key) (Token, error) {
key.Type = AccessKey
key.ExpiresAt = time.Now().Add(svc.loginDuration)
access, err := svc.tokenizer.Issue(key)
if err != nil {
return Token{}, errors.Wrap(errIssueTmp, err)
}
key.ExpiresAt = time.Now().Add(svc.refreshDuration)
key.Type = RefreshKey
refresh, err := svc.tokenizer.Issue(key)
if err != nil {
return Token{}, errors.Wrap(errIssueTmp, err)
}
return Token{AccessToken: access, RefreshToken: refresh}, nil
}
func (svc service) refreshKey(ctx context.Context, token string, key Key) (Token, error) {
k, err := svc.tokenizer.Parse(token)
if err != nil {
return Token{}, err
}
if k.Type != RefreshKey {
return Token{}, errIssueUser
}
key.ID = k.ID
key.Subject = k.Subject
key.Type = AccessKey
key.ExpiresAt = time.Now().Add(svc.loginDuration)
access, err := svc.tokenizer.Issue(key)
if err != nil {
return Token{}, errors.Wrap(errIssueTmp, err)
}
key.ExpiresAt = time.Now().Add(svc.refreshDuration)
key.Type = RefreshKey
refresh, err := svc.tokenizer.Issue(key)
if err != nil {
return Token{}, errors.Wrap(errIssueTmp, err)
}
return Token{AccessToken: access, RefreshToken: refresh}, nil
}
func (svc service) userKey(ctx context.Context, token string, key Key) (Token, error) {
id, sub, err := svc.authenticate(token)
if err != nil {
return Token{}, errors.Wrap(errIssueUser, err)
}
key.Issuer = id
if key.Subject == "" {
key.Subject = sub
}
keyID, err := svc.idProvider.ID()
if err != nil {
return Token{}, errors.Wrap(errIssueUser, err)
}
key.ID = keyID
if _, err := svc.keys.Save(ctx, key); err != nil {
return Token{}, errors.Wrap(errIssueUser, err)
}
tkn, err := svc.tokenizer.Issue(key)
if err != nil {
return Token{}, errors.Wrap(errIssueUser, err)
}
return Token{AccessToken: tkn}, nil
}
func (svc service) authenticate(token string) (string, string, error) {
key, err := svc.tokenizer.Parse(token)
if err != nil {
return "", "", err
}
// Only login key token is valid for login.
if key.Type != AccessKey || key.Issuer == "" {
return "", "", errors.ErrAuthentication
}
return key.Issuer, key.Subject, nil
}
// Switch the relative permission for the relation.
func SwitchToPermission(relation string) string {
switch relation {
case OwnerRelation:
return AdministratorPermission
case AdminRelation:
return AdministratorPermission
case EditorRelation:
return EditPermission
case ViewerRelation:
return ViewPermission
default:
return relation
}
}