423 lines
12 KiB
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
|
|
}
|
|
}
|