mirror of
https://github.com/mainflux/mainflux.git
synced 2025-04-27 13:48:49 +08:00

* remove owner id Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * add users endpoint for retrieving users from group Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * remove groups from things and users Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * move groups into auth Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * separate endpoints for users and things Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * fix problems with retrieving members Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * add groups test Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * remove groups from users Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * remove groups from things Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * rename constant Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * add new errors Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * remove unnecessary constants Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * fix validation Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * create groups db mock Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * adding tests Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * revert changes to docker related files Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * remove groups endpoints from users openapi Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * remove groups endpoints from users openapi Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * move constant from postgres to groups Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * move constant from postgres to groups Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * move constant from postgres to groups Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * remove testing group Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * renam typ to groupType Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * add error for max level Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * remove print Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * remove groups.Member interface Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * fix query building and add test cases Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * uncomment tests Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * move groups package Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * remove group type, add bulk assign and unassign Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * update openapi, remove parentID from create request, reorder endpoints Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * update openapi Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * update openapi for users and things Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * fix groups test Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * fix linter errors Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * resolve comments Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * rename assignReq structure Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * refactor mocks, response, remove type from endpoint Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * some refactor, renaming, errors Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * simplify check Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * remove package alias Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * fix naming and comment Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * additional comments Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * add members grpc endpoint test Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * fix retrieving members for different types Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * fix retrieving members for different types Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * remove unecessary structure Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * fix api grpc Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * rename const Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * refactore retrieve parents and children with common function Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * small changes for errors Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * fix compile error Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * fix sorting in mock Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * remove regexp for groups Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * revert as change is made by mistake Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * revert as change is made by mistake Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * refactor groups and keys package Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * fix naming Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * fix naming Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * fix test for timestamp compare Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * fix error handling Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * remove errors not being used Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * var renaming Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * resolve comments Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * minor changes Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * fix test Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * add endpoints for groups into nginx Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * reorganize endpoints, remove some errors Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * reorganize endpoints, remove some errors Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * small fix Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * fix linter errors Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * minor changes Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * resolve comments Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * fix group save path problem Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * description constant Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * rename variables Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * fix validation Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * get back return Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com> * fix compile Signed-off-by: Mirko Teodorovic <mirko.teodorovic@gmail.com>
334 lines
10 KiB
Go
334 lines
10 KiB
Go
// Copyright (c) Mainflux
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package auth
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
"github.com/mainflux/mainflux"
|
|
"github.com/mainflux/mainflux/pkg/errors"
|
|
"github.com/mainflux/mainflux/pkg/ulid"
|
|
)
|
|
|
|
const (
|
|
loginDuration = 10 * time.Hour
|
|
recoveryDuration = 5 * time.Minute
|
|
)
|
|
|
|
var (
|
|
// ErrUnauthorizedAccess represents unauthorized access.
|
|
ErrUnauthorizedAccess = errors.New("unauthorized access")
|
|
|
|
// ErrMalformedEntity indicates malformed entity specification (e.g.
|
|
// invalid owner or ID).
|
|
ErrMalformedEntity = errors.New("malformed entity specification")
|
|
|
|
// ErrNotFound indicates a non-existing entity request.
|
|
ErrNotFound = errors.New("entity not found")
|
|
|
|
// ErrGenerateGroupID indicates error in creating group.
|
|
ErrGenerateGroupID = errors.New("failed to generate group id")
|
|
|
|
// ErrConflict indicates that entity already exists.
|
|
ErrConflict = errors.New("entity already exists")
|
|
|
|
// 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 user 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) (Key, string, 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
|
|
|
|
// Retrieve 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) (Identity, error)
|
|
}
|
|
|
|
// Authz specifies an API for the authorization and will be implemented
|
|
// by evaluation of policies.
|
|
type Authz interface {
|
|
// Authorize checks access rights
|
|
Authorize(ctx context.Context, token, sub, obj, act string) (bool, error)
|
|
}
|
|
|
|
// Service 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 Service interface {
|
|
Authn
|
|
Authz
|
|
|
|
// Implements groups API, creating groups, assigning members
|
|
GroupService
|
|
}
|
|
|
|
var _ Service = (*service)(nil)
|
|
|
|
type service struct {
|
|
keys KeyRepository
|
|
groups GroupRepository
|
|
idProvider mainflux.IDProvider
|
|
ulidProvider mainflux.IDProvider
|
|
tokenizer Tokenizer
|
|
}
|
|
|
|
// New instantiates the auth service implementation.
|
|
func New(keys KeyRepository, groups GroupRepository, idp mainflux.IDProvider, tokenizer Tokenizer) Service {
|
|
return &service{
|
|
tokenizer: tokenizer,
|
|
keys: keys,
|
|
groups: groups,
|
|
idProvider: idp,
|
|
ulidProvider: ulid.New(),
|
|
}
|
|
}
|
|
|
|
func (svc service) Issue(ctx context.Context, token string, key Key) (Key, string, error) {
|
|
if key.IssuedAt.IsZero() {
|
|
return Key{}, "", ErrInvalidKeyIssuedAt
|
|
}
|
|
switch key.Type {
|
|
case APIKey:
|
|
return svc.userKey(ctx, token, key)
|
|
case RecoveryKey:
|
|
return svc.tmpKey(recoveryDuration, key)
|
|
default:
|
|
return svc.tmpKey(loginDuration, key)
|
|
}
|
|
}
|
|
|
|
func (svc service) Revoke(ctx context.Context, token, id string) error {
|
|
issuerID, _, err := svc.login(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.login(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) (Identity, error) {
|
|
key, err := svc.tokenizer.Parse(token)
|
|
if err == ErrAPIKeyExpired {
|
|
err = svc.keys.Remove(ctx, key.IssuerID, key.ID)
|
|
return Identity{}, errors.Wrap(ErrAPIKeyExpired, err)
|
|
}
|
|
if err != nil {
|
|
return Identity{}, errors.Wrap(errIdentify, err)
|
|
}
|
|
|
|
switch key.Type {
|
|
case APIKey, RecoveryKey, UserKey:
|
|
return Identity{ID: key.IssuerID, Email: key.Subject}, nil
|
|
default:
|
|
return Identity{}, ErrUnauthorizedAccess
|
|
}
|
|
}
|
|
|
|
func (svc service) Authorize(ctx context.Context, token, sub, obj, act string) (bool, error) {
|
|
return true, nil
|
|
}
|
|
|
|
func (svc service) tmpKey(duration time.Duration, key Key) (Key, string, error) {
|
|
key.ExpiresAt = key.IssuedAt.Add(duration)
|
|
secret, err := svc.tokenizer.Issue(key)
|
|
if err != nil {
|
|
return Key{}, "", errors.Wrap(errIssueTmp, err)
|
|
}
|
|
|
|
return key, secret, nil
|
|
}
|
|
|
|
func (svc service) userKey(ctx context.Context, token string, key Key) (Key, string, error) {
|
|
id, sub, err := svc.login(token)
|
|
if err != nil {
|
|
return Key{}, "", errors.Wrap(errIssueUser, err)
|
|
}
|
|
|
|
key.IssuerID = id
|
|
if key.Subject == "" {
|
|
key.Subject = sub
|
|
}
|
|
|
|
keyID, err := svc.idProvider.ID()
|
|
if err != nil {
|
|
return Key{}, "", errors.Wrap(errIssueUser, err)
|
|
}
|
|
key.ID = keyID
|
|
|
|
if _, err := svc.keys.Save(ctx, key); err != nil {
|
|
return Key{}, "", errors.Wrap(errIssueUser, err)
|
|
}
|
|
|
|
secret, err := svc.tokenizer.Issue(key)
|
|
if err != nil {
|
|
return Key{}, "", errors.Wrap(errIssueUser, err)
|
|
}
|
|
|
|
return key, secret, nil
|
|
}
|
|
|
|
func (svc service) login(token string) (string, string, error) {
|
|
key, err := svc.tokenizer.Parse(token)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
// Only user key token is valid for login.
|
|
if key.Type != UserKey || key.IssuerID == "" {
|
|
return "", "", ErrUnauthorizedAccess
|
|
}
|
|
|
|
return key.IssuerID, key.Subject, nil
|
|
}
|
|
|
|
func (svc service) CreateGroup(ctx context.Context, token string, group Group) (Group, error) {
|
|
user, err := svc.Identify(ctx, token)
|
|
if err != nil {
|
|
return Group{}, errors.Wrap(ErrUnauthorizedAccess, err)
|
|
}
|
|
|
|
ulid, err := svc.ulidProvider.ID()
|
|
if err != nil {
|
|
return Group{}, errors.Wrap(ErrGenerateGroupID, err)
|
|
}
|
|
|
|
timestamp := getTimestmap()
|
|
group.UpdatedAt = timestamp
|
|
group.CreatedAt = timestamp
|
|
|
|
group.ID = ulid
|
|
group.OwnerID = user.ID
|
|
|
|
group, err = svc.groups.Save(ctx, group)
|
|
if err != nil {
|
|
return Group{}, err
|
|
}
|
|
|
|
return group, nil
|
|
}
|
|
|
|
func (svc service) ListGroups(ctx context.Context, token string, pm PageMetadata) (GroupPage, error) {
|
|
if _, err := svc.Identify(ctx, token); err != nil {
|
|
return GroupPage{}, errors.Wrap(ErrUnauthorizedAccess, err)
|
|
}
|
|
return svc.groups.RetrieveAll(ctx, pm)
|
|
}
|
|
|
|
func (svc service) ListParents(ctx context.Context, token string, childID string, pm PageMetadata) (GroupPage, error) {
|
|
if _, err := svc.Identify(ctx, token); err != nil {
|
|
return GroupPage{}, errors.Wrap(ErrUnauthorizedAccess, err)
|
|
}
|
|
return svc.groups.RetrieveAllParents(ctx, childID, pm)
|
|
}
|
|
|
|
func (svc service) ListChildren(ctx context.Context, token string, parentID string, pm PageMetadata) (GroupPage, error) {
|
|
if _, err := svc.Identify(ctx, token); err != nil {
|
|
return GroupPage{}, errors.Wrap(ErrUnauthorizedAccess, err)
|
|
}
|
|
return svc.groups.RetrieveAllChildren(ctx, parentID, pm)
|
|
}
|
|
|
|
func (svc service) ListMembers(ctx context.Context, token string, groupID, groupType string, pm PageMetadata) (MemberPage, error) {
|
|
if _, err := svc.Identify(ctx, token); err != nil {
|
|
return MemberPage{}, errors.Wrap(ErrUnauthorizedAccess, err)
|
|
}
|
|
mp, err := svc.groups.Members(ctx, groupID, groupType, pm)
|
|
if err != nil {
|
|
return MemberPage{}, errors.Wrap(ErrFailedToRetrieveMembers, err)
|
|
}
|
|
return mp, nil
|
|
}
|
|
|
|
func (svc service) RemoveGroup(ctx context.Context, token, id string) error {
|
|
if _, err := svc.Identify(ctx, token); err != nil {
|
|
return errors.Wrap(ErrUnauthorizedAccess, err)
|
|
}
|
|
return svc.groups.Delete(ctx, id)
|
|
}
|
|
|
|
func (svc service) UpdateGroup(ctx context.Context, token string, group Group) (Group, error) {
|
|
if _, err := svc.Identify(ctx, token); err != nil {
|
|
return Group{}, errors.Wrap(ErrUnauthorizedAccess, err)
|
|
}
|
|
|
|
group.UpdatedAt = getTimestmap()
|
|
return svc.groups.Update(ctx, group)
|
|
}
|
|
|
|
func (svc service) ViewGroup(ctx context.Context, token, id string) (Group, error) {
|
|
if _, err := svc.Identify(ctx, token); err != nil {
|
|
return Group{}, errors.Wrap(ErrUnauthorizedAccess, err)
|
|
}
|
|
return svc.groups.RetrieveByID(ctx, id)
|
|
}
|
|
|
|
func (svc service) Assign(ctx context.Context, token string, groupID, groupType string, memberIDs ...string) error {
|
|
if _, err := svc.Identify(ctx, token); err != nil {
|
|
return errors.Wrap(ErrUnauthorizedAccess, err)
|
|
}
|
|
return svc.groups.Assign(ctx, groupID, groupType, memberIDs...)
|
|
}
|
|
|
|
func (svc service) Unassign(ctx context.Context, token string, groupID string, memberIDs ...string) error {
|
|
if _, err := svc.Identify(ctx, token); err != nil {
|
|
return errors.Wrap(ErrUnauthorizedAccess, err)
|
|
}
|
|
return svc.groups.Unassign(ctx, groupID, memberIDs...)
|
|
}
|
|
|
|
func (svc service) ListMemberships(ctx context.Context, token string, memberID string, pm PageMetadata) (GroupPage, error) {
|
|
if _, err := svc.Identify(ctx, token); err != nil {
|
|
return GroupPage{}, errors.Wrap(ErrUnauthorizedAccess, err)
|
|
}
|
|
return svc.groups.Memberships(ctx, memberID, pm)
|
|
}
|
|
|
|
func getTimestmap() time.Time {
|
|
return time.Now().UTC().Round(time.Millisecond)
|
|
}
|