SMQ-2751 - Add search to PATs (#2753)

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>
This commit is contained in:
Steve Munene
2025-03-14 12:06:03 +03:00
committed by GitHub
parent 2bee2efae5
commit 1e4341a6ec
8 changed files with 207 additions and 81 deletions
+3
View File
@@ -84,6 +84,9 @@ func listPATSEndpoint(svc auth.Service) endpoint.Endpoint {
pm := auth.PATSPageMeta{
Limit: req.limit,
Offset: req.offset,
Name: req.name,
ID: req.id,
Status: req.status,
}
patsPage, err := svc.ListPATS(ctx, req.token, pm)
if err != nil {
+3
View File
@@ -108,6 +108,9 @@ type listPatsReq struct {
token string
offset uint64
limit uint64
name string
id string
status auth.Status
}
func (req listPatsReq) validate() (err error) {
+21
View File
@@ -204,15 +204,36 @@ func decodeListPATSRequest(_ context.Context, r *http.Request) (interface{}, err
if err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, err)
}
n, err := apiutil.ReadStringQuery(r, api.NameKey, "")
if err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, err)
}
i, err := apiutil.ReadStringQuery(r, api.IDOrder, "")
if err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, err)
}
token := apiutil.ExtractBearerToken(r)
if strings.HasPrefix(token, patPrefix) {
return nil, apiutil.ErrUnsupportedTokenType
}
s, err := apiutil.ReadStringQuery(r, api.StatusKey, "")
if err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, err)
}
patStatus, err := auth.ToStatus(s)
if err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, err)
}
req := listPatsReq{
token: token,
limit: l,
offset: o,
name: n,
id: i,
status: patStatus,
}
return req, nil
}
+14 -10
View File
@@ -236,12 +236,12 @@ func (et *EntityType) UnmarshalText(data []byte) (err error) {
// ]
type Scope struct {
ID string `json:"id,omitempty"`
PatID string `json:"pat_id,omitempty"`
OptionalDomainID string `json:"optional_domain_id,omitempty"`
EntityType EntityType `json:"entity_type,omitempty"`
EntityID string `json:"entity_id,omitempty"`
Operation Operation `json:"operation,omitempty"`
ID string `json:"id"`
PatID string `json:"pat_id"`
OptionalDomainID string `json:"optional_domain_id"`
EntityType EntityType `json:"entity_type"`
EntityID string `json:"entity_id"`
Operation Operation `json:"operation"`
}
func (s *Scope) Authorized(entityType EntityType, optionalDomainID string, operation Operation, entityID string) bool {
@@ -302,17 +302,21 @@ type PAT struct {
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,omitempty"`
PATS []PAT `json:"pats"`
}
type ScopesPageMeta struct {
@@ -324,9 +328,9 @@ type ScopesPageMeta struct {
type ScopesPage struct {
Total uint64 `json:"total"`
Offset uint64 `json:"offset,omitempty"`
Limit uint64 `json:"limit,omitempy"`
Scopes []Scope `json:"scopes,omitempty"`
Offset uint64 `json:"offset"`
Limit uint64 `json:"limit"`
Scopes []Scope `json:"scopes"`
}
func (pat PAT) MarshalBinary() ([]byte, error) {
+7 -10
View File
@@ -8,7 +8,6 @@ import (
"time"
"github.com/absmach/supermq/auth"
repoerr "github.com/absmach/supermq/pkg/errors/repository"
)
type dbPat struct {
@@ -23,6 +22,7 @@ type dbPat struct {
LastUsedAt sql.NullTime `db:"last_used_at,omitempty"`
Revoked bool `db:"revoked,omitempty"`
RevokedAt sql.NullTime `db:"revoked_at,omitempty"`
Status auth.Status `db:"status,omitempty"`
}
type dbScope struct {
@@ -47,13 +47,11 @@ type dbPagemeta struct {
RevokedAt sql.NullTime `db:"revoked_at"`
Description string `db:"description"`
Secret string `db:"secret"`
Status auth.Status `db:"status"`
Timestamp time.Time `db:"timestamp,omitempty"`
}
func toAuthPat(db dbPat) (auth.PAT, error) {
if db.ID == "" {
return auth.PAT{}, repoerr.ErrNotFound
}
func toAuthPat(db dbPat) auth.PAT {
updatedAt := time.Time{}
lastUsedAt := time.Time{}
revokedAt := time.Time{}
@@ -70,7 +68,7 @@ func toAuthPat(db dbPat) (auth.PAT, error) {
revokedAt = db.RevokedAt.Time
}
pat := auth.PAT{
return auth.PAT{
ID: db.ID,
User: db.User,
Name: db.Name,
@@ -82,9 +80,8 @@ func toAuthPat(db dbPat) (auth.PAT, error) {
LastUsedAt: lastUsedAt,
Revoked: db.Revoked,
RevokedAt: revokedAt,
Status: db.Status,
}
return pat, nil
}
func toAuthScope(dsc []dbScope) ([]auth.Scope, error) {
@@ -144,9 +141,9 @@ func toDBPats(pat auth.PAT) (dbPat, error) {
Secret: pat.Secret,
IssuedAt: pat.IssuedAt,
ExpiresAt: pat.ExpiresAt,
Revoked: pat.Revoked,
UpdatedAt: updatedAt,
LastUsedAt: lastUsedAt,
Revoked: pat.Revoked,
RevokedAt: revokedAt,
}, nil
}
+76 -60
View File
@@ -63,18 +63,32 @@ func (pr *patRepo) Retrieve(ctx context.Context, userID, patID string) (auth.PAT
}
func (pr *patRepo) RetrieveAll(ctx context.Context, userID string, pm auth.PATSPageMeta) (auth.PATSPage, error) {
q := `
pageQuery, err := PageQuery(pm)
if err != nil {
return auth.PATSPage{}, errors.Wrap(repoerr.ErrViewEntity, err)
}
q := fmt.Sprintf(`
SELECT
p.id, p.user_id, p.name, p.description, p.issued_at, p.expires_at,
p.updated_at, p.revoked, p.revoked_at
FROM pats p WHERE user_id = :user_id
p.id, p.user_id, p.name, p.description, p.issued_at, p.expires_at,
p.updated_at, p.revoked, p.revoked_at,
CASE
WHEN p.revoked = TRUE THEN %d
WHEN expires_at IS NOT NULL AND expires_at < :timestamp THEN %d
ELSE %d
END AS status
FROM pats p WHERE user_id = :user_id %s
ORDER BY issued_at DESC
LIMIT :limit OFFSET :offset`
LIMIT :limit OFFSET :offset`, auth.RevokedStatus, auth.ExpiredStatus, auth.ActiveStatus, pageQuery)
dbPage := dbPagemeta{
Limit: pm.Limit,
Offset: pm.Offset,
User: userID,
Limit: pm.Limit,
Offset: pm.Offset,
User: userID,
Name: pm.Name,
ID: pm.ID,
Status: pm.Status,
Timestamp: time.Now(),
}
rows, err := pr.db.NamedQueryContext(ctx, q, dbPage)
@@ -83,35 +97,17 @@ func (pr *patRepo) RetrieveAll(ctx context.Context, userID string, pm auth.PATSP
}
defer rows.Close()
var items []auth.PAT
items := []auth.PAT{}
for rows.Next() {
var pat dbPat
if err := rows.StructScan(&pat); err != nil {
return auth.PATSPage{}, errors.Wrap(repoerr.ErrViewEntity, err)
}
var updatedAt, revokedAt time.Time
if pat.UpdatedAt.Valid {
updatedAt = pat.UpdatedAt.Time
}
if pat.RevokedAt.Valid {
revokedAt = pat.RevokedAt.Time
}
items = append(items, auth.PAT{
ID: pat.ID,
User: pat.User,
Name: pat.Name,
Description: pat.Description,
IssuedAt: pat.IssuedAt,
ExpiresAt: pat.ExpiresAt,
UpdatedAt: updatedAt,
Revoked: pat.Revoked,
RevokedAt: revokedAt,
})
items = append(items, toAuthPat(pat))
}
cq := `SELECT COUNT(*) FROM pats p WHERE user_id = :user_id`
cq := fmt.Sprintf(`SELECT COUNT(*) FROM pats p WHERE user_id = :user_id %s`, pageQuery)
total, err := postgres.Total(ctx, pr.db, cq, dbPage)
if err != nil {
@@ -127,13 +123,47 @@ func (pr *patRepo) RetrieveAll(ctx context.Context, userID string, pm auth.PATSP
return page, nil
}
func PageQuery(pm auth.PATSPageMeta) (string, error) {
var query []string
if pm.Name != "" {
query = append(query, "p.name ILIKE '%' || :name || '%'")
}
if pm.ID != "" {
query = append(query, "p.id = :id")
}
if pm.Status != auth.AllStatus {
switch pm.Status {
case auth.RevokedStatus:
query = append(query, "p.revoked = TRUE")
case auth.ExpiredStatus:
query = append(query, "p.revoked = FALSE AND p.expires_at IS NOT NULL AND p.expires_at < :timestamp")
case auth.ActiveStatus:
query = append(query, "p.revoked = FALSE AND (p.expires_at IS NULL OR p.expires_at >= :timestamp)")
}
}
var emq string
if len(query) > 0 {
emq = fmt.Sprintf("AND %s", strings.Join(query, " AND "))
}
return emq, nil
}
func (pr *patRepo) RetrieveSecretAndRevokeStatus(ctx context.Context, userID, patID string) (string, bool, bool, error) {
q := `
SELECT p.secret, p.revoked, p.expires_at
FROM pats p
WHERE user_id = $1 AND id = $2`
WHERE p.user_id = :user_id AND p.id = :pat_id`
rows, err := pr.db.QueryContext(ctx, q, userID, patID)
dbPage := dbPagemeta{
User: userID,
PatID: patID,
Timestamp: time.Now(),
}
rows, err := pr.db.NamedQueryContext(ctx, q, dbPage)
if err != nil {
return "", true, true, postgres.HandleError(repoerr.ErrNotFound, err)
}
@@ -186,12 +216,7 @@ func (pr *patRepo) UpdateName(ctx context.Context, userID, patID, name string) (
return auth.PAT{}, errors.Wrap(repoerr.ErrUpdateEntity, err)
}
res, err := toAuthPat(pat)
if err != nil {
return auth.PAT{}, errors.Wrap(repoerr.ErrUpdateEntity, err)
}
return res, nil
return toAuthPat(pat), nil
}
func (pr *patRepo) UpdateDescription(ctx context.Context, userID, patID, description string) (auth.PAT, error) {
@@ -225,12 +250,7 @@ func (pr *patRepo) UpdateDescription(ctx context.Context, userID, patID, descrip
return auth.PAT{}, errors.Wrap(repoerr.ErrUpdateEntity, err)
}
res, err := toAuthPat(pat)
if err != nil {
return auth.PAT{}, errors.Wrap(repoerr.ErrUpdateEntity, err)
}
return res, nil
return toAuthPat(pat), nil
}
func (pr *patRepo) UpdateTokenHash(ctx context.Context, userID, patID, tokenHash string, expiryAt time.Time) (auth.PAT, error) {
@@ -265,12 +285,7 @@ func (pr *patRepo) UpdateTokenHash(ctx context.Context, userID, patID, tokenHash
return auth.PAT{}, errors.Wrap(repoerr.ErrUpdateEntity, err)
}
res, err := toAuthPat(pat)
if err != nil {
return auth.PAT{}, errors.Wrap(repoerr.ErrUpdateEntity, err)
}
return res, nil
return toAuthPat(pat), nil
}
func (pr *patRepo) Revoke(ctx context.Context, userID, patID string) error {
@@ -624,15 +639,21 @@ func (pr *patRepo) retrieveScopeFromDB(ctx context.Context, pm dbPagemeta) ([]au
}
func (pr *patRepo) retrievePATFromDB(ctx context.Context, userID, patID string) (auth.PAT, error) {
q := `
q := fmt.Sprintf(`
SELECT
id, user_id, name, description, secret, issued_at, expires_at,
updated_at, last_used_at, revoked, revoked_at
FROM pats WHERE user_id = :user_id AND id = :id`
updated_at, last_used_at, revoked, revoked_at,
CASE
WHEN revoked = TRUE THEN %d
WHEN expires_at IS NOT NULL AND expires_at < :timestamp THEN %d
ELSE %d
END AS status
FROM pats WHERE user_id = :user_id AND id = :id`, auth.RevokedStatus, auth.ExpiredStatus, auth.ActiveStatus)
dbp := dbPagemeta{
ID: patID,
User: userID,
ID: patID,
User: userID,
Timestamp: time.Now(),
}
rows, err := pr.db.NamedQueryContext(ctx, q, dbp)
@@ -648,10 +669,5 @@ func (pr *patRepo) retrievePATFromDB(ctx context.Context, userID, patID string)
}
}
pat, err := toAuthPat(record)
if err != nil {
return auth.PAT{}, err
}
return pat, nil
return toAuthPat(record), nil
}
+3 -1
View File
@@ -489,6 +489,8 @@ func (svc service) CreatePAT(ctx context.Context, token, name, description strin
Secret: hash,
IssuedAt: now,
ExpiresAt: now.Add(duration),
Status: ActiveStatus,
Revoked: false,
}
if err := svc.pats.Save(ctx, pat); err != nil {
@@ -579,7 +581,7 @@ func (svc service) ResetPATSecret(ctx context.Context, token, patID string, dura
return PAT{}, errors.Wrap(svcerr.ErrUpdateEntity, err)
}
pat.Secret = secret
pat.Revoked = false
pat.Status = ActiveStatus
pat.RevokedAt = time.Time{}
return pat, nil
}
+80
View File
@@ -0,0 +1,80 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package auth
import (
"encoding/json"
"strings"
svcerr "github.com/absmach/supermq/pkg/errors/service"
)
type Status uint8
const (
ActiveStatus Status = iota
RevokedStatus
ExpiredStatus
AllStatus
)
const (
Active = "active"
Revoked = "revoked"
Expired = "expired"
All = "all"
Unknown = "unknown"
)
func (s Status) String() string {
switch s {
case ActiveStatus:
return Active
case RevokedStatus:
return Revoked
case ExpiredStatus:
return Expired
case AllStatus:
return All
default:
return Unknown
}
}
// ToStatus converts string value to a valid Client status.
func ToStatus(status string) (Status, error) {
switch status {
case "", Active:
return ActiveStatus, nil
case Revoked:
return RevokedStatus, nil
case All:
return AllStatus, nil
case Expired:
return ExpiredStatus, nil
}
return Status(0), svcerr.ErrInvalidStatus
}
func (s Status) MarshalJSON() ([]byte, error) {
return json.Marshal(s.String())
}
func (p PAT) MarshalJSON() ([]byte, error) {
type Alias PAT
return json.Marshal(&struct {
Alias
Status string `json:"status,omitempty"`
}{
Alias: (Alias)(p),
Status: p.Status.String(),
})
}
func (s *Status) UnmarshalJSON(data []byte) error {
str := strings.Trim(string(data), "\"")
val, err := ToStatus(str)
*s = val
return err
}