diff --git a/auth/postgres/init.go b/auth/postgres/init.go index 90e2fb726d..525de66f79 100644 --- a/auth/postgres/init.go +++ b/auth/postgres/init.go @@ -80,14 +80,31 @@ func Migration() *migrate.MemoryMigrationSource { revoked BOOLEAN, revoked_at TIMESTAMP, entity_type TEXT, - last_used_at TIMESTAMP, - scopes_data TEXT + last_used_at TIMESTAMP )`, }, Down: []string{ `DROP TABLE IF EXISTS pats`, }, }, + { + Id: "auth_5", + Up: []string{ + `CREATE TABLE IF NOT EXISTS pat_scopes ( + id SERIAL PRIMARY KEY, + pat_id VARCHAR(36) REFERENCES pats(id) ON DELETE CASCADE, + platform_type VARCHAR(50) NOT NULL, + domain_id VARCHAR(36), + domain_type VARCHAR(50), + operation_type VARCHAR(50) NOT NULL, + entity_ids TEXT[] NOT NULL, + UNIQUE (pat_id, platform_type, domain_id, domain_type, operation_type) + )`, + }, + Down: []string{ + `DROP TABLE IF EXISTS pat_scopes`, + }, + }, }, } } diff --git a/auth/postgres/pat.go b/auth/postgres/pat.go index 6fa03f827d..0ae195b4d8 100644 --- a/auth/postgres/pat.go +++ b/auth/postgres/pat.go @@ -4,8 +4,6 @@ package postgres import ( - "encoding/json" - "fmt" "time" "github.com/absmach/supermq/auth" @@ -23,7 +21,15 @@ type dbPat struct { LastUsedAt time.Time `db:"last_used_at,omitempty"` Revoked bool `db:"revoked,omitempty"` RevokedAt time.Time `db:"revoked_at,omitempty"` - ScopesData string `db:"scopes_data,omitempty"` +} + +type dbScope struct { + PatID string `db:"pat_id,omitempty"` + Platformtype string `db:"platform_type,omitempty"` + DomainID string `db:"domain_id,omitempty"` + DomainType string `db:"domain_type,omitempty"` + OperationType string `db:"operation_type,omitempty"` + EntityIDs []string `db:"entity_ids,omitempty"` } type dbAuthPage struct { @@ -32,7 +38,7 @@ type dbAuthPage struct { User string `db:"user_id"` } -func toAuthPat(db dbPat) (auth.PAT, error) { +func toAuthPat(db dbPat, sc []dbScope) (auth.PAT, error) { pat := auth.PAT{ ID: db.ID, User: db.User, @@ -47,86 +53,130 @@ func toAuthPat(db dbPat) (auth.PAT, error) { RevokedAt: db.RevokedAt, Scope: auth.Scope{Domains: make(map[string]auth.DomainScope)}, } - - var scopeData struct { - Users auth.OperationScope `json:"users,omitempty"` - Dashboard auth.OperationScope `json:"dashboard,omitempty"` - Messaging auth.OperationScope `json:"messaging,omitempty"` - DomainScopes map[string]DomainScopeData `json:"domain_scopes,omitempty"` + scope, err := toAuthScope(sc) + if err != nil { + return auth.PAT{}, err } + pat.Scope = scope + + return pat, nil +} - if err := json.Unmarshal([]byte(db.ScopesData), &scopeData); err != nil { - return auth.PAT{}, fmt.Errorf("failed to unmarshal scopes data: %w", err) +func toAuthScope(sc []dbScope) (auth.Scope, error) { + scope := auth.Scope{ + Domains: make(map[string]auth.DomainScope), + Users: auth.OperationScope{}, + Dashboard: auth.OperationScope{}, + Messaging: auth.OperationScope{}, } - pat.Scope.Users = scopeData.Users - pat.Scope.Dashboard = scopeData.Dashboard - pat.Scope.Messaging = scopeData.Messaging + for _, t := range sc { + var platformType auth.PlatformEntityType + var operation auth.OperationType + var err error - for domainID, dsd := range scopeData.DomainScopes { - domainScope := auth.DomainScope{ - DomainManagement: dsd.DomainManagement, - Entities: make(map[auth.DomainEntityType]auth.OperationScope), + platformType, err = auth.ParsePlatformEntityType(t.Platformtype) + if err != nil { + return auth.Scope{}, err } - for entityTypeStr, ops := range dsd.Entities { - entityType, err := auth.ParseDomainEntityType(entityTypeStr) - if err != nil { - return auth.PAT{}, fmt.Errorf("invalid domain entity type %s: %w", entityTypeStr, err) - } - domainScope.Entities[entityType] = ops + operation, err = auth.ParseOperationType(t.OperationType) + if err != nil { + return auth.Scope{}, err } - pat.Scope.Domains[domainID] = domainScope + switch platformType { + case auth.PlatformUsersScope: + if err := scope.Users.Add(operation, t.EntityIDs...); err != nil { + return auth.Scope{}, err + } + case auth.PlatformDashBoardScope: + if err := scope.Dashboard.Add(operation, t.EntityIDs...); err != nil { + return auth.Scope{}, err + } + case auth.PlatformMesagingScope: + if err := scope.Messaging.Add(operation, t.EntityIDs...); err != nil { + return auth.Scope{}, err + } + case auth.PlatformDomainsScope: + var domainEntityType auth.DomainEntityType + if t.DomainType != "" { + domainEntityType, err = auth.ParseDomainEntityType(t.DomainType) + if err != nil { + return auth.Scope{}, err + } + } + + if err := scope.Add(platformType, t.DomainID, domainEntityType, operation, t.EntityIDs...); err != nil { + return auth.Scope{}, err + } + } } - return pat, nil + return scope, nil } -func patToDBRecords(pat auth.PAT) (dbPat, error) { - scopeData := struct { - Users auth.OperationScope `json:"users,omitempty"` - Dashboard auth.OperationScope `json:"dashboard,omitempty"` - Messaging auth.OperationScope `json:"messaging,omitempty"` - DomainScopes map[string]DomainScopeData `json:"domain_scopes,omitempty"` - }{ - DomainScopes: make(map[string]DomainScopeData), - } +func fromAuthScope(patID string, scope auth.Scope) []dbScope { + var dbScopes []dbScope - if len(pat.Scope.Users) > 0 { - scopeData.Users = pat.Scope.Users + for op, ids := range scope.Users { + dbScopes = append(dbScopes, dbScope{ + PatID: patID, + Platformtype: auth.PlatformUsersScope.String(), + OperationType: op.String(), + EntityIDs: ids.Values(), + }) } - if len(pat.Scope.Dashboard) > 0 { - scopeData.Dashboard = pat.Scope.Dashboard + for op, ids := range scope.Dashboard { + dbScopes = append(dbScopes, dbScope{ + PatID: patID, + Platformtype: auth.PlatformDashBoardScope.String(), + OperationType: op.String(), + EntityIDs: ids.Values(), + }) } - if len(pat.Scope.Messaging) > 0 { - scopeData.Messaging = pat.Scope.Messaging + for op, ids := range scope.Messaging { + dbScopes = append(dbScopes, dbScope{ + PatID: patID, + Platformtype: auth.PlatformMesagingScope.String(), + OperationType: op.String(), + EntityIDs: ids.Values(), + }) } - for domainID, domainScope := range pat.Scope.Domains { - dsd := DomainScopeData{ - DomainManagement: domainScope.DomainManagement, - Entities: make(map[string]auth.OperationScope), + for domainID, domainScope := range scope.Domains { + for op, ids := range domainScope.DomainManagement { + dbScopes = append(dbScopes, dbScope{ + PatID: patID, + Platformtype: auth.PlatformDomainsScope.String(), + DomainID: domainID, + DomainType: auth.DomainManagementScope.String(), + OperationType: op.String(), + EntityIDs: ids.Values(), + }) } - for entityType, ops := range domainScope.Entities { - entityTypeStr, err := entityType.ValidString() - if err != nil { - return dbPat{}, fmt.Errorf("invalid entity type: %w", err) + for entityType, entityScope := range domainScope.Entities { + for op, ids := range entityScope { + dbScopes = append(dbScopes, dbScope{ + PatID: patID, + Platformtype: auth.PlatformDomainsScope.String(), + DomainID: domainID, + DomainType: entityType.String(), + OperationType: op.String(), + EntityIDs: ids.Values(), + }) } - dsd.Entities[entityTypeStr] = ops } - - scopeData.DomainScopes[domainID] = dsd } - scopesJSON, err := json.Marshal(scopeData) - if err != nil { - return dbPat{}, fmt.Errorf("failed to marshal scopes data: %w", err) - } + return dbScopes +} +func patToDBRecords(pat auth.PAT) (dbPat, []dbScope, error) { + scopes := fromAuthScope(pat.ID, pat.Scope) return dbPat{ ID: pat.ID, User: pat.User, @@ -139,13 +189,7 @@ func patToDBRecords(pat auth.PAT) (dbPat, error) { LastUsedAt: pat.LastUsedAt, Revoked: pat.Revoked, RevokedAt: pat.RevokedAt, - ScopesData: string(scopesJSON), - }, nil -} - -type DomainScopeData struct { - DomainManagement auth.OperationScope `json:"domain_management,omitempty"` - Entities map[string]auth.OperationScope `json:"entities,omitempty"` + }, scopes, nil } func toDBAuthPage(user string, pm auth.PATSPageMeta) dbAuthPage { diff --git a/auth/postgres/repo.go b/auth/postgres/repo.go index 19347c7dc2..625d0c6206 100644 --- a/auth/postgres/repo.go +++ b/auth/postgres/repo.go @@ -11,6 +11,7 @@ import ( "github.com/absmach/supermq/pkg/errors" repoerr "github.com/absmach/supermq/pkg/errors/repository" "github.com/absmach/supermq/pkg/postgres" + "github.com/lib/pq" ) var _ auth.PATSRepository = (*patRepo)(nil) @@ -19,26 +20,49 @@ const ( saveQuery = ` INSERT INTO pats ( id, user_id, name, description, secret, issued_at, expires_at, - updated_at, last_used_at, revoked, revoked_at, - scopes_data + updated_at, last_used_at, revoked, revoked_at ) VALUES ( :id, :user_id, :name, :description, :secret, :issued_at, :expires_at, - :updated_at, :last_used_at, :revoked, :revoked_at, - :scopes_data + :updated_at, :last_used_at, :revoked, :revoked_at )` + updateQuery = ` + UPDATE pats SET + name = :name, + description = :description, + secret = :secret, + expires_at = :expires_at, + updated_at = :updated_at, + last_used_at = :last_used_at, + revoked = :revoked, + revoked_at = :revoked_at + WHERE id = :id AND user_id = :user_id` + retrieveQuery = ` SELECT id, user_id, name, description, secret, issued_at, expires_at, - updated_at, last_used_at, revoked, revoked_at, - scopes_data + updated_at, last_used_at, revoked, revoked_at FROM pats WHERE user_id = $1 AND id = $2` - updateQuery = ` + saveScopeQuery = ` + INSERT INTO pat_scopes (pat_id, platform_type, domain_id, domain_type, operation_type, entity_ids) + VALUES (:pat_id, :platform_type, :domain_id, :domain_type, :operation_type, :entity_ids)` + + updateScopeQuery = ` UPDATE pats SET - scopes_data = :scopes_data, - updated_at = :updated_at - WHERE user_id = :user_id AND id = :id` + pat_id = :pat_id, + platform_type = :platform_type, + domain_id = :domain_id, + domain_type = :domain_type, + operation_type = :operation_type, + entity_ids = :entity_ids + WHERE id = :id AND user_id = :user_id` + + retrieveScopesQuery = ` + SELECT platform_type, domain_id, domain_type, operation_type, entity_ids + FROM pat_scopes WHERE pat_id = $1` + + deleteScopesQuery = `DELETE FROM pat_scopes WHERE pat_id = $1` ) type patRepo struct { @@ -54,7 +78,7 @@ func NewPatRepo(db postgres.Database, cache auth.Cache) auth.PATSRepository { } func (pr *patRepo) Save(ctx context.Context, pat auth.PAT) error { - record, err := patToDBRecords(pat) + record, scope, err := patToDBRecords(pat) if err != nil { return errors.Wrap(repoerr.ErrCreateEntity, err) } @@ -65,6 +89,12 @@ func (pr *patRepo) Save(ctx context.Context, pat auth.PAT) error { } defer row.Close() + row1, err := pr.db.NamedQueryContext(ctx, saveScopeQuery, scope) + if err != nil { + return postgres.HandleError(repoerr.ErrCreateEntity, err) + } + defer row1.Close() + if err := pr.cache.Save(ctx, pat.ID, pat); err != nil { return errors.Wrap(repoerr.ErrCreateEntity, err) } @@ -368,12 +398,12 @@ func (pr *patRepo) AddScopeEntry(ctx context.Context, userID, patID string, plat return auth.Scope{}, errors.Wrap(repoerr.ErrCreateEntity, err) } - record, err := patToDBRecords(pat) + _, scope, err := patToDBRecords(pat) if err != nil { return auth.Scope{}, errors.Wrap(repoerr.ErrCreateEntity, err) } - res, err := pr.db.NamedExecContext(ctx, updateQuery, record) + res, err := pr.db.NamedExecContext(ctx, updateScopeQuery, scope) if err != nil { return auth.Scope{}, postgres.HandleError(repoerr.ErrUpdateEntity, err) } @@ -403,12 +433,12 @@ func (pr *patRepo) RemoveScopeEntry(ctx context.Context, userID, patID string, p return auth.Scope{}, errors.Wrap(repoerr.ErrRemoveEntity, err) } - record, err := patToDBRecords(pat) + _, scope, err := patToDBRecords(pat) if err != nil { return auth.Scope{}, errors.Wrap(repoerr.ErrRemoveEntity, err) } - res, err := pr.db.NamedExecContext(ctx, updateQuery, record) + res, err := pr.db.NamedExecContext(ctx, updateScopeQuery, scope) if err != nil { return auth.Scope{}, postgres.HandleError(repoerr.ErrUpdateEntity, err) } @@ -455,12 +485,12 @@ func (pr *patRepo) RemoveAllScopeEntry(ctx context.Context, userID, patID string pat.Scope = auth.Scope{Domains: make(map[string]auth.DomainScope)} - record, err := patToDBRecords(pat) + _, scope, err := patToDBRecords(pat) if err != nil { return errors.Wrap(repoerr.ErrRemoveEntity, err) } - res, err := pr.db.NamedExecContext(ctx, updateQuery, record) + res, err := pr.db.NamedExecContext(ctx, updateScopeQuery, scope) if err != nil { return postgres.HandleError(repoerr.ErrUpdateEntity, err) } @@ -481,6 +511,29 @@ func (pr *patRepo) RemoveAllScopeEntry(ctx context.Context, userID, patID string } func (pr *patRepo) retrieveFromDB(ctx context.Context, userID, patID string) (auth.PAT, error) { + scopeRows, err := pr.db.QueryContext(ctx, retrieveScopesQuery, patID) + if err != nil { + return auth.PAT{}, errors.Wrap(repoerr.ErrViewEntity, err) + } + defer scopeRows.Close() + + var scopes []dbScope + var entityIDs pq.StringArray + for scopeRows.Next() { + var scope dbScope + if err := scopeRows.Scan( + &scope.Platformtype, + &scope.DomainID, + &scope.DomainType, + &scope.OperationType, + &entityIDs, + ); err != nil { + return auth.PAT{}, errors.Wrap(repoerr.ErrViewEntity, err) + } + scope.EntityIDs = []string(entityIDs) + scopes = append(scopes, scope) + } + rows, err := pr.db.QueryContext(ctx, retrieveQuery, userID, patID) if err != nil { return auth.PAT{}, errors.Wrap(repoerr.ErrViewEntity, err) @@ -501,11 +554,10 @@ func (pr *patRepo) retrieveFromDB(ctx context.Context, userID, patID string) (au &record.LastUsedAt, &record.Revoked, &record.RevokedAt, - &record.ScopesData, ); err != nil { return auth.PAT{}, errors.Wrap(repoerr.ErrViewEntity, err) } - return toAuthPat(record) + return toAuthPat(record, scopes) } return auth.PAT{}, repoerr.ErrNotFound diff --git a/auth/service.go b/auth/service.go index 9217eaa5cb..d1d1d0081b 100644 --- a/auth/service.go +++ b/auth/service.go @@ -489,7 +489,7 @@ func (svc service) CreatePAT(ctx context.Context, token, name, description strin User: key.User, Name: name, Description: description, - Secret: patPrefix + patSecretSeparator + hash, + Secret: hash, IssuedAt: now, ExpiresAt: now.Add(duration), Scope: scope,