mirror of
https://github.com/absmach/magistrala.git
synced 2026-06-23 04:10:28 +00:00
7f03134d8e
Property Based Tests / api-test (push) Has been cancelled
Continuous Delivery / lint-and-build (push) Has been cancelled
Deploy GitHub Pages / swagger-ui (push) Has been cancelled
CI Pipeline / Lint Proto (push) Has been cancelled
CI Pipeline / Detect Changes (push) Has been cancelled
Continuous Delivery / Build and Push Docker Images (push) Has been cancelled
CI Pipeline / lint-and-build (push) Has been cancelled
CI Pipeline / Test ${{ matrix.module }} (push) Has been cancelled
CI Pipeline / Upload Coverage (push) Has been cancelled
Signed-off-by: nyagamunene <stevenyaga2014@gmail.com> Signed-off-by: JeffMboya <jangina.mboya@gmail.com> Co-authored-by: JeffMboya <jangina.mboya@gmail.com>
506 lines
13 KiB
Go
506 lines
13 KiB
Go
// Copyright (c) Abstract Machines
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package auth
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
apiutil "github.com/absmach/magistrala/api/http/util"
|
|
"github.com/absmach/magistrala/pkg/errors"
|
|
"github.com/absmach/magistrala/pkg/permissions"
|
|
)
|
|
|
|
const (
|
|
AnyIDs = "*"
|
|
RoleOperationPrefix = "role_"
|
|
)
|
|
|
|
const (
|
|
OpCreate = "create"
|
|
OpList = "list"
|
|
|
|
OpCreateClients = "create_clients"
|
|
OpListClients = "list_clients"
|
|
OpCreateChannels = "create_channels"
|
|
OpListChannels = "list_channels"
|
|
OpCreateGroups = "create_groups"
|
|
OpListGroups = "list_groups"
|
|
|
|
OpShare = "share"
|
|
OpUnshare = "unshare"
|
|
|
|
OpDashboardShare = "dashboard_share"
|
|
OpDashboardUnshare = "dashboard_unshare"
|
|
|
|
OpPublish = "publish"
|
|
OpSubscribe = "subscribe"
|
|
|
|
OpMessagePublish = "message_publish"
|
|
OpMessageSubscribe = "message_subscribe"
|
|
)
|
|
|
|
var (
|
|
errInvalidEntityOp = errors.NewRequestError("operation not valid for entity type")
|
|
errAlarmOpRequiresWildcardEntityID = errors.NewRequestError("alarm operations on rules entity type require wildcard entity ID")
|
|
)
|
|
|
|
// alarmOnlyOperations are RulesType operations authorized at the domain level; only wildcard entity ID is valid.
|
|
var alarmOnlyOperations = map[string]struct{}{
|
|
"alarm_assign": {},
|
|
"alarm_acknowledge": {},
|
|
"alarm_resolve": {},
|
|
}
|
|
|
|
type Operation = permissions.Operation
|
|
|
|
// Dashboard operations.
|
|
const (
|
|
DashboardShareOp Operation = iota + 400
|
|
DashboardUnshareOp
|
|
)
|
|
|
|
// Messages operations.
|
|
const (
|
|
MessagePublishOp Operation = iota + 500
|
|
MessageSubscribeOp
|
|
)
|
|
|
|
type EntityType uint32
|
|
|
|
const (
|
|
GroupsType EntityType = iota
|
|
ChannelsType
|
|
ClientsType
|
|
BootstrapType
|
|
DashboardType
|
|
MessagesType
|
|
DomainsType
|
|
UsersType
|
|
RulesType
|
|
ReportsType
|
|
)
|
|
|
|
const (
|
|
GroupsScopeStr = "groups"
|
|
ChannelsScopeStr = "channels"
|
|
ClientsScopeStr = "clients"
|
|
BootstrapStr = "bootstrap"
|
|
DashboardsStr = "dashboards"
|
|
MessagesStr = "messages"
|
|
DomainsStr = "domains"
|
|
UsersStr = "users"
|
|
RulesScopeStr = "rules"
|
|
ReportsScopeStr = "reports"
|
|
)
|
|
|
|
func (et EntityType) String() string {
|
|
switch et {
|
|
case GroupsType:
|
|
return GroupsScopeStr
|
|
case ChannelsType:
|
|
return ChannelsScopeStr
|
|
case ClientsType:
|
|
return ClientsScopeStr
|
|
case BootstrapType:
|
|
return BootstrapStr
|
|
case DashboardType:
|
|
return DashboardsStr
|
|
case MessagesType:
|
|
return MessagesStr
|
|
case DomainsType:
|
|
return DomainsStr
|
|
case UsersType:
|
|
return UsersStr
|
|
case RulesType:
|
|
return RulesScopeStr
|
|
case ReportsType:
|
|
return ReportsScopeStr
|
|
default:
|
|
return fmt.Sprintf("unknown domain entity type %d", et)
|
|
}
|
|
}
|
|
|
|
func ParseEntityType(et string) (EntityType, error) {
|
|
switch et {
|
|
case GroupsScopeStr:
|
|
return GroupsType, nil
|
|
case ChannelsScopeStr:
|
|
return ChannelsType, nil
|
|
case ClientsScopeStr:
|
|
return ClientsType, nil
|
|
case BootstrapStr:
|
|
return BootstrapType, nil
|
|
case DashboardsStr:
|
|
return DashboardType, nil
|
|
case MessagesStr:
|
|
return MessagesType, nil
|
|
case DomainsStr:
|
|
return DomainsType, nil
|
|
case UsersStr:
|
|
return UsersType, nil
|
|
case RulesScopeStr:
|
|
return RulesType, nil
|
|
case ReportsScopeStr:
|
|
return ReportsType, nil
|
|
default:
|
|
return 0, fmt.Errorf("unknown domain entity type %s", et)
|
|
}
|
|
}
|
|
|
|
func (et EntityType) MarshalJSON() ([]byte, error) {
|
|
return json.Marshal(et.String())
|
|
}
|
|
|
|
func (et *EntityType) UnmarshalJSON(data []byte) error {
|
|
str := strings.Trim(string(data), "\"")
|
|
val, err := ParseEntityType(str)
|
|
*et = val
|
|
return err
|
|
}
|
|
|
|
func (et EntityType) MarshalText() ([]byte, error) {
|
|
return []byte(et.String()), nil
|
|
}
|
|
|
|
func (et *EntityType) UnmarshalText(data []byte) (err error) {
|
|
str := strings.Trim(string(data), "\"")
|
|
*et, err = ParseEntityType(str)
|
|
return err
|
|
}
|
|
|
|
func IsValidOperationForEntity(entityType EntityType, operation string) bool {
|
|
switch entityType {
|
|
case ClientsType, ChannelsType, GroupsType, BootstrapType, DomainsType, RulesType, ReportsType:
|
|
return true
|
|
case DashboardType:
|
|
return operation == OpDashboardShare || operation == OpDashboardUnshare
|
|
case MessagesType:
|
|
return operation == OpMessagePublish || operation == OpMessageSubscribe
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// Example Scope as JSON
|
|
//
|
|
// [
|
|
// {
|
|
// "domain_id": "domain_1",
|
|
// "entity_type": "groups",
|
|
// "operation": "view",
|
|
// "entity_id": "*"
|
|
// },
|
|
// {
|
|
// "domain_id": "domain_1",
|
|
// "entity_type": "channels",
|
|
// "operation": "delete",
|
|
// "entity_id": "channel1"
|
|
// },
|
|
// {
|
|
// "domain_id": "domain_1",
|
|
// "entity_type": "clients",
|
|
// "operation": "update",
|
|
// "entity_id": "*"
|
|
// }
|
|
// ]
|
|
|
|
type Scope struct {
|
|
ID string `json:"id"`
|
|
PatID string `json:"pat_id"`
|
|
DomainID string `json:"domain_id"`
|
|
EntityType EntityType `json:"entity_type"`
|
|
EntityID string `json:"entity_id"`
|
|
Operation string `json:"operation"`
|
|
}
|
|
|
|
func (s *Scope) UnmarshalJSON(data []byte) error {
|
|
type Alias Scope
|
|
aux := (*Alias)(s)
|
|
|
|
if err := json.Unmarshal(data, aux); err != nil {
|
|
return err
|
|
}
|
|
|
|
switch s.EntityType {
|
|
case ClientsType:
|
|
switch s.Operation {
|
|
case OpCreate:
|
|
s.Operation = OpCreateClients
|
|
case OpList:
|
|
s.Operation = OpListClients
|
|
}
|
|
case ChannelsType:
|
|
switch s.Operation {
|
|
case OpCreate:
|
|
s.Operation = OpCreateChannels
|
|
case OpList:
|
|
s.Operation = OpListChannels
|
|
}
|
|
case GroupsType:
|
|
switch s.Operation {
|
|
case OpCreate:
|
|
s.Operation = OpCreateGroups
|
|
case OpList:
|
|
s.Operation = OpListGroups
|
|
}
|
|
case DashboardType:
|
|
switch s.Operation {
|
|
case OpShare:
|
|
s.Operation = OpDashboardShare
|
|
case OpUnshare:
|
|
s.Operation = OpDashboardUnshare
|
|
}
|
|
case MessagesType:
|
|
switch s.Operation {
|
|
case OpPublish:
|
|
s.Operation = OpMessagePublish
|
|
case OpSubscribe:
|
|
s.Operation = OpMessageSubscribe
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Scope) Authorized(entityType EntityType, domainID string, operation string, entityID string) bool {
|
|
if s == nil {
|
|
return false
|
|
}
|
|
|
|
if s.EntityType != entityType {
|
|
return false
|
|
}
|
|
|
|
if s.DomainID != "" && s.DomainID != domainID {
|
|
return false
|
|
}
|
|
|
|
if s.Operation != operation {
|
|
return false
|
|
}
|
|
|
|
if s.EntityID == "*" {
|
|
return true
|
|
}
|
|
|
|
if s.EntityID == entityID {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (s *Scope) Validate() error {
|
|
if s == nil {
|
|
return errInvalidScope
|
|
}
|
|
if s.EntityID == "" {
|
|
return apiutil.ErrMissingEntityID
|
|
}
|
|
|
|
if s.DomainID == "" {
|
|
return apiutil.ErrMissingDomainID
|
|
}
|
|
|
|
if !IsValidOperationForEntity(s.EntityType, s.Operation) {
|
|
return errors.Wrap(apiutil.ErrInvalidQueryParams, errInvalidEntityOp)
|
|
}
|
|
|
|
if s.EntityType == RulesType {
|
|
if _, ok := alarmOnlyOperations[s.Operation]; ok && s.EntityID != AnyIDs {
|
|
return errors.Wrap(apiutil.ErrInvalidQueryParams, errAlarmOpRequiresWildcardEntityID)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// PATAuthz represents the PAT authorization request fields.
|
|
type PATAuthz struct {
|
|
PatID string
|
|
UserID string
|
|
EntityType EntityType
|
|
EntityID string
|
|
Operation string
|
|
Domain string
|
|
}
|
|
|
|
// PAT represents Personal Access Token.
|
|
type PAT struct {
|
|
ID string `json:"id,omitempty"`
|
|
User string `json:"user_id,omitempty"`
|
|
Name string `json:"name,omitempty"`
|
|
Description string `json:"description,omitempty"`
|
|
Secret string `json:"secret,omitempty"`
|
|
Role Role `json:"role,omitempty"`
|
|
IssuedAt time.Time `json:"issued_at,omitempty"`
|
|
ExpiresAt time.Time `json:"expires_at,omitempty"`
|
|
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
|
LastUsedAt time.Time `json:"last_used_at,omitempty"`
|
|
Revoked bool `json:"revoked,omitempty"`
|
|
RevokedAt time.Time `json:"revoked_at,omitempty"`
|
|
Status Status `json:"status,omitempty"`
|
|
}
|
|
|
|
type PATSPageMeta struct {
|
|
Offset uint64 `json:"offset"`
|
|
Limit uint64 `json:"limit"`
|
|
Name string `json:"name"`
|
|
ID string `json:"id"`
|
|
Status Status `json:"status"`
|
|
}
|
|
type PATSPage struct {
|
|
Total uint64 `json:"total"`
|
|
Offset uint64 `json:"offset"`
|
|
Limit uint64 `json:"limit"`
|
|
PATS []PAT `json:"pats"`
|
|
}
|
|
|
|
type ScopesPageMeta struct {
|
|
Offset uint64 `json:"offset"`
|
|
Limit uint64 `json:"limit"`
|
|
PatID string `json:"pat_id"`
|
|
ID string `json:"id"`
|
|
}
|
|
|
|
type ScopesPage struct {
|
|
Total uint64 `json:"total"`
|
|
Offset uint64 `json:"offset"`
|
|
Limit uint64 `json:"limit"`
|
|
Scopes []Scope `json:"scopes"`
|
|
}
|
|
|
|
func (pat PAT) MarshalBinary() ([]byte, error) {
|
|
return json.Marshal(pat)
|
|
}
|
|
|
|
func (pat *PAT) UnmarshalBinary(data []byte) error {
|
|
return json.Unmarshal(data, pat)
|
|
}
|
|
|
|
// Validate checks if the PAT has valid fields.
|
|
func (pat *PAT) Validate() error {
|
|
if pat == nil {
|
|
return errors.New("PAT cannot be nil")
|
|
}
|
|
if pat.Name == "" {
|
|
return errors.New("PAT name cannot be empty")
|
|
}
|
|
if pat.User == "" {
|
|
return errors.New("PAT user cannot be empty")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// PATS specifies function which are required for Personal access Token implementation.
|
|
type PATS interface {
|
|
// Create function creates new PAT for given valid inputs.
|
|
CreatePAT(ctx context.Context, token, name, description string, duration time.Duration) (PAT, error)
|
|
|
|
// UpdateName function updates the name for the given PAT ID.
|
|
UpdatePATName(ctx context.Context, token, patID, name string) (PAT, error)
|
|
|
|
// UpdateDescription function updates the description for the given PAT ID.
|
|
UpdatePATDescription(ctx context.Context, token, patID, description string) (PAT, error)
|
|
|
|
// Retrieve function retrieves the PAT for given ID.
|
|
RetrievePAT(ctx context.Context, userID string, patID string) (PAT, error)
|
|
|
|
// RemoveAllPAT function removes all PATs of user.
|
|
RemoveAllPAT(ctx context.Context, token string) error
|
|
|
|
// ListPATS function lists all the PATs for the user.
|
|
ListPATS(ctx context.Context, token string, pm PATSPageMeta) (PATSPage, error)
|
|
|
|
// Delete function deletes the PAT for given ID.
|
|
DeletePAT(ctx context.Context, token, patID string) error
|
|
|
|
// ResetSecret function reset the secret and creates new secret for the given ID.
|
|
ResetPATSecret(ctx context.Context, token, patID string, duration time.Duration) (PAT, error)
|
|
|
|
// RevokeSecret function revokes the secret for the given ID.
|
|
RevokePATSecret(ctx context.Context, token, patID string) error
|
|
|
|
// AddScope function adds a new scope.
|
|
AddScope(ctx context.Context, token, patID string, scopes []Scope) error
|
|
|
|
// RemoveScope function removes a scope.
|
|
RemoveScope(ctx context.Context, token string, patID string, scopeIDs ...string) error
|
|
|
|
// RemovePATAllScope function removes all scope.
|
|
RemovePATAllScope(ctx context.Context, token, patID string) error
|
|
|
|
// List function lists all the Scopes for the patID.
|
|
ListScopes(ctx context.Context, token string, pm ScopesPageMeta) (ScopesPage, error)
|
|
|
|
// IdentifyPAT function will valid the secret.
|
|
IdentifyPAT(ctx context.Context, paToken string) (PAT, error)
|
|
|
|
// AuthorizePAT function will valid the secret and check the given scope exists.
|
|
AuthorizePAT(ctx context.Context, userID, patID string, entityType EntityType, domainID string, operation string, entityID string) error
|
|
}
|
|
|
|
// PATSRepository specifies PATS persistence API.
|
|
type PATSRepository interface {
|
|
// Save persists the PAT
|
|
Save(ctx context.Context, pat PAT) (err error)
|
|
|
|
// Retrieve retrieves users PAT by its unique identifier.
|
|
Retrieve(ctx context.Context, userID, patID string) (pat PAT, err error)
|
|
|
|
// RetrieveScope retrieves PAT scopes by its unique identifier.
|
|
RetrieveScope(ctx context.Context, pm ScopesPageMeta) (scopes ScopesPage, err error)
|
|
|
|
// RetrieveSecretAndRevokeStatus retrieves secret and revoke status of PAT by its unique identifier.
|
|
RetrieveSecretAndRevokeStatus(ctx context.Context, userID, patID string) (string, bool, bool, error)
|
|
|
|
// UpdateName updates the name of a PAT.
|
|
UpdateName(ctx context.Context, userID, patID, name string) (PAT, error)
|
|
|
|
// UpdateDescription updates the description of a PAT.
|
|
UpdateDescription(ctx context.Context, userID, patID, description string) (PAT, error)
|
|
|
|
// UpdateTokenHash updates the token hash of a PAT.
|
|
UpdateTokenHash(ctx context.Context, userID, patID, tokenHash string, expiryAt time.Time) (PAT, error)
|
|
|
|
// RetrieveAll retrieves all PATs belongs to userID.
|
|
RetrieveAll(ctx context.Context, userID string, pm PATSPageMeta) (pats PATSPage, err error)
|
|
|
|
// Revoke PAT with provided ID.
|
|
Revoke(ctx context.Context, userID, patID string) error
|
|
|
|
// Reactivate PAT with provided ID.
|
|
Reactivate(ctx context.Context, userID, patID string) error
|
|
|
|
// Remove removes Key with provided ID.
|
|
Remove(ctx context.Context, userID, patID string) error
|
|
|
|
// RemoveAllPAT removes all PAT for a given user.
|
|
RemoveAllPAT(ctx context.Context, userID string) error
|
|
|
|
AddScope(ctx context.Context, userID string, scopes []Scope) error
|
|
|
|
RemoveScope(ctx context.Context, userID string, scopesIDs ...string) error
|
|
|
|
CheckScope(ctx context.Context, userID, patID string, entityType EntityType, domainID string, operation string, entityID string) error
|
|
|
|
RemoveAllScope(ctx context.Context, patID string) error
|
|
}
|
|
|
|
type Cache interface {
|
|
Save(ctx context.Context, userID string, scopes []Scope) error
|
|
|
|
CheckScope(ctx context.Context, userID, patID, optionalDomainID string, entityType EntityType, operation string, entityID string) bool
|
|
|
|
Remove(ctx context.Context, userID string, scopesID []string) error
|
|
|
|
RemoveUserAllScope(ctx context.Context, userID string) error
|
|
|
|
RemoveAllScope(ctx context.Context, userID, patID string) error
|
|
}
|