// 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 }