mirror of
https://github.com/absmach/magistrala.git
synced 2026-06-23 04:10:28 +00:00
SMQ-2751 - Add search to PATs (#2753)
Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user