You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
boundary/internal/apptoken/repository.go

815 lines
30 KiB

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package apptoken
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"slices"
"strings"
"time"
"github.com/hashicorp/boundary/globals"
"github.com/hashicorp/boundary/internal/apptoken/store"
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/db/timestamp"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/perms"
"github.com/hashicorp/boundary/internal/util"
)
// Repository is the apptoken database repository
type Repository struct {
reader db.Reader
writer db.Writer
kms *kms.Kms
defaultLimit int
}
// NewRepository creates a new apptoken Repository
func NewRepository(ctx context.Context, r db.Reader, w db.Writer, kms *kms.Kms, opt ...Option) (*Repository, error) {
const op = "apptoken.NewRepository"
if r == nil {
return nil, errors.New(ctx, errors.InvalidParameter, op, "nil reader")
}
if w == nil {
return nil, errors.New(ctx, errors.InvalidParameter, op, "nil writer")
}
if kms == nil {
return nil, errors.New(ctx, errors.InvalidParameter, op, "nil kms")
}
return &Repository{
reader: r,
writer: w,
kms: kms,
}, nil
}
// lookupAppTokenResults represents the raw result of the app token lookup queries: [getAppTokenGlobalQuery], [getAppTokenOrgQuery], and [getAppTokenProjectQuery]
type lookupAppTokenResult struct {
publicId string
scopeId string
name *string
description *string
createTime *timestamp.Timestamp
updateTime *timestamp.Timestamp
approximateLastAccessTime *timestamp.Timestamp
expirationTime *timestamp.Timestamp
timeToStaleSeconds uint32
createdByUserId string
revoked bool
tokenBytes []byte
permissionsJSON []byte
}
// appTokenPermissionResult represents the unpacked results of the [lookupAppTokenResult]'s permissionsJSON field
type appTokenPermissionResult struct {
Label string `json:"label"`
GrantThisScope bool `json:"grant_this_scope"`
Grants []string `json:"grants"`
GrantScope string `json:"grant_scope"`
ActiveGrantScopes []string `json:"active_grant_scopes"`
DeletedGrantScopes []string `json:"deleted_grant_scopes"`
DeletedScopeDetails []DeletedScope `json:"deleted_scope_details"`
}
// LookupAppToken returns an AppToken for the id. Returns nil if no AppToken is found for id.
func (r *Repository) LookupAppToken(ctx context.Context, id string, opt ...Option) (*AppToken, error) {
const op = "apptoken.(Repository).LookupAppToken"
if id == "" {
return nil, errors.New(ctx, errors.InvalidParameter, op, "no public id")
}
opts := getOpts(opt...)
var at AppToken
lookupFunc := func(reader db.Reader, w db.Writer) error {
scopeId, err := getAppTokenScopeId(ctx, reader, id)
if err != nil {
return errors.Wrap(ctx, err, op)
}
var query string
switch {
case strings.HasPrefix(scopeId, globals.GlobalPrefix):
query = getAppTokenGlobalQuery
case strings.HasPrefix(scopeId, globals.OrgPrefix):
query = getAppTokenOrgQuery
case strings.HasPrefix(scopeId, globals.ProjectPrefix):
query = getAppTokenProjectQuery
default:
return errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unknown scope type for scope id: %s", scopeId))
}
rows, err := reader.Query(ctx, query, []any{
sql.Named("app_token_id", id),
})
if err != nil {
return errors.Wrap(ctx, err, op)
}
var (
res lookupAppTokenResult
permissionsRes []appTokenPermissionResult
)
defer rows.Close()
if rows.Next() {
if err := rows.Scan(
&res.publicId,
&res.scopeId,
&res.name,
&res.description,
&res.revoked,
&res.createTime,
&res.updateTime,
&res.createdByUserId,
&res.approximateLastAccessTime,
&res.timeToStaleSeconds,
&res.expirationTime,
&res.tokenBytes,
&res.permissionsJSON,
); err != nil {
return errors.Wrap(ctx, err, op)
}
if res.publicId == "" {
return errors.New(ctx, errors.NotFound, op, "app token not found")
}
// Unpack permissions JSON from query results
if err := json.Unmarshal(res.permissionsJSON, &permissionsRes); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("failed to unmarshal permissions"))
}
}
if err := rows.Err(); err != nil {
return errors.Wrap(ctx, err, op)
}
at.PublicId = res.publicId
at.ScopeId = res.scopeId
if res.name != nil {
at.Name = *res.name
}
if res.description != nil {
at.Description = *res.description
}
at.Revoked = res.revoked
at.CreateTime = res.createTime
at.UpdateTime = res.updateTime
at.CreatedByUserId = res.createdByUserId
at.ApproximateLastAccessTime = res.approximateLastAccessTime
at.TimeToStaleSeconds = res.timeToStaleSeconds
at.ExpirationTime = res.expirationTime
// Build granted scopes list for each permission
at.Permissions = make([]AppTokenPermission, len(permissionsRes))
for i, permission := range permissionsRes {
var grantedScopes []string
// Add non-individual grant scopes (children, descendants)
if permission.GrantScope == globals.GrantScopeChildren ||
permission.GrantScope == globals.GrantScopeDescendants {
grantedScopes = append(grantedScopes, permission.GrantScope)
}
// Add 'this' if grant_this_scope is true
if permission.GrantThisScope {
grantedScopes = append(grantedScopes, globals.GrantScopeThis)
}
// Add any active, individual grant scopes
if len(permission.ActiveGrantScopes) > 0 {
grantedScopes = append(grantedScopes, permission.ActiveGrantScopes...)
}
at.Permissions[i] = AppTokenPermission{
Label: permission.Label,
Grants: permission.Grants,
GrantedScopes: grantedScopes,
DeletedScopes: permission.DeletedScopeDetails,
}
}
atc := &appTokenCipher{
AppTokenCipher: &store.AppTokenCipher{
AppTokenId: id,
CtToken: res.tokenBytes,
},
}
databaseWrapper, err := r.kms.GetWrapper(ctx, at.ScopeId, kms.KeyPurposeDatabase)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get database wrapper"))
}
if err := atc.decrypt(ctx, databaseWrapper); err != nil {
return errors.Wrap(ctx, err, op)
}
at.Token = atc.Token
return nil
}
var err error
if !util.IsNil(opts.withReader) && !util.IsNil(opts.withWriter) {
if !opts.withWriter.IsTx(ctx) {
return nil, errors.New(ctx, errors.Internal, op, "writer is not in transaction")
}
err = lookupFunc(opts.withReader, opts.withWriter)
} else {
_, err = r.writer.DoTx(
ctx,
db.StdRetryCnt,
db.ExpBackoff{},
lookupFunc,
)
}
if err != nil {
if errors.IsNotFoundError(err) {
return nil, nil
}
return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("for %s", id)))
}
return &at, nil
}
// CreateToken creates the provided app token in the repository.
func (r *Repository) CreateAppToken(ctx context.Context, token *AppToken) (*AppToken, error) {
const op = "apptoken.(Repository).CreateToken"
if token == nil {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing app token")
}
id, err := newAppTokenId(ctx)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
token.PublicId = id
// dbInserts is a slice of slices
// each inner slice contains items of the same type (appTokenPermissionGlobal, for example)
// to be batch inserted using CreateItems
var dbInserts []interface{}
switch {
case strings.HasPrefix(token.GetScopeId(), globals.GlobalPrefix):
dbInserts, err = createAppTokenGlobal(ctx, token)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
case strings.HasPrefix(token.GetScopeId(), globals.OrgPrefix):
dbInserts, err = createAppTokenOrg(ctx, token)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
case strings.HasPrefix(token.GetScopeId(), globals.ProjectPrefix):
dbInserts, err = createAppTokenProject(ctx, token)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
default:
return nil, errors.New(ctx, errors.InvalidParameter, op, "invalid scope type")
}
cipherToken, err := newToken(ctx)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("failed to generate cipher token"))
}
atc := &appTokenCipher{
AppTokenCipher: &store.AppTokenCipher{
AppTokenId: id,
Token: cipherToken,
},
}
databaseWrapper, err := r.kms.GetWrapper(ctx, token.ScopeId, kms.KeyPurposeDatabase)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get database wrapper"))
}
if err := atc.encrypt(ctx, databaseWrapper); err != nil {
return nil, errors.Wrap(ctx, err, op)
}
dbInserts = append(dbInserts, []*appTokenCipher{atc})
// batch write all collected inserts
var newAppToken *AppToken
_, err = r.writer.DoTx(
ctx,
db.StdRetryCnt,
db.ExpBackoff{},
func(reader db.Reader, writer db.Writer) error {
for _, appTokenItems := range dbInserts {
if err := writer.CreateItems(ctx, appTokenItems); err != nil {
return err
}
}
// Do a fresh lookup to get all return values
newAppToken, err = r.LookupAppToken(ctx, id, WithReaderWriter(reader, writer))
if err != nil {
return errors.Wrap(ctx, err, op)
}
return nil
},
)
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("creating app token in database"))
}
return newAppToken, nil
}
func createAppTokenGlobal(ctx context.Context, token *AppToken) ([]interface{}, error) {
const op = "apptoken.(Repository).createAppTokenGlobal"
var globalInserts []interface{}
// we collect inserts in their own slices so that we can use w.CreateItems above
// to batch insert by type (say 10,000 permissions at once)
var permissionInserts []*appTokenPermissionGlobal
var permissionGrantInserts []*appTokenPermissionGrant
var individualOrgInserts []*appTokenPermissionGlobalIndividualOrgGrantScope
var individualProjInserts []*appTokenPermissionGlobalIndividualProjectGrantScope
tokenToCreate := &appTokenGlobal{
AppTokenGlobal: &store.AppTokenGlobal{
PublicId: token.PublicId,
ScopeId: token.ScopeId,
Name: token.Name,
Description: token.Description,
Revoked: token.Revoked,
CreatedByUserId: token.CreatedByUserId,
TimeToStaleSeconds: token.TimeToStaleSeconds,
ExpirationTime: token.ExpirationTime,
},
}
globalInserts = append(globalInserts, []*appTokenGlobal{tokenToCreate})
// each permission uses the same permission ID for its grants and scopes
// they're a composite key in the app_token_permission_grant table
for _, perm := range token.Permissions {
if slices.Contains(perm.GrantedScopes, globals.GrantScopeDescendants) && slices.Contains(perm.GrantedScopes, globals.GrantScopeChildren) {
return nil, errors.New(ctx, errors.InvalidParameter, op, "only one of descendants or children grant scope can be specified")
}
// perm.GrantedScopes cannot contain globals.GrantScopeDescendants and also contain an individual project or org scope
if slices.Contains(perm.GrantedScopes, globals.GrantScopeDescendants) && slices.ContainsFunc(perm.GrantedScopes, func(s string) bool {
return strings.HasPrefix(s, globals.ProjectPrefix) || strings.HasPrefix(s, globals.OrgPrefix)
}) {
return nil, errors.New(ctx, errors.InvalidParameter, op, "descendants grant scope cannot be combined with individual project grant scopes")
}
// perm.GrantedScopes cannot contain globals.GrantScopeChildren and also contain an individual org scope
if slices.Contains(perm.GrantedScopes, globals.GrantScopeChildren) && slices.ContainsFunc(perm.GrantedScopes, func(s string) bool {
return strings.HasPrefix(s, globals.OrgPrefix)
}) {
return nil, errors.New(ctx, errors.InvalidParameter, op, "children grant scope cannot be combined with individual org grant scopes")
}
// generate new permission ID
permId, err := newAppTokenPermissionId(ctx)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
grantThisScope := slices.Contains(perm.GrantedScopes, globals.GrantScopeThis) || slices.Contains(perm.GrantedScopes, globals.GlobalPrefix)
globalPermGrantScope := determineGrantScope(perm.GrantedScopes)
globalPermToCreate := &appTokenPermissionGlobal{
AppTokenPermissionGlobal: &store.AppTokenPermissionGlobal{
PrivateId: permId,
AppTokenId: token.PublicId,
GrantThisScope: grantThisScope,
GrantScope: globalPermGrantScope,
Description: perm.Label,
},
}
permissionInserts = append(permissionInserts, globalPermToCreate)
grantInserts, err := processPermissionGrants(ctx, permId, perm.Grants)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
permissionGrantInserts = append(permissionGrantInserts, grantInserts...)
// Create a copy of GrantedScopes before filtering to avoid mutating it
grantedScopes := slices.Clone(perm.GrantedScopes)
trimmedScopes := slices.DeleteFunc(grantedScopes, func(s string) bool {
return s == globals.GrantScopeThis ||
s == globals.GrantScopeChildren ||
s == globals.GrantScopeDescendants ||
s == globals.GlobalPrefix
})
for _, gs := range trimmedScopes {
switch {
case strings.HasPrefix(gs, globals.OrgPrefix):
individualOrgGlobalPermToCreate := &appTokenPermissionGlobalIndividualOrgGrantScope{
AppTokenPermissionGlobalIndividualOrgGrantScope: &store.AppTokenPermissionGlobalIndividualOrgGrantScope{
PermissionId: permId,
GrantScope: globalPermGrantScope,
ScopeId: gs,
},
}
individualOrgInserts = append(individualOrgInserts, individualOrgGlobalPermToCreate)
case strings.HasPrefix(gs, globals.ProjectPrefix):
individualProjGlobalPermToCreate := &appTokenPermissionGlobalIndividualProjectGrantScope{
AppTokenPermissionGlobalIndividualProjectGrantScope: &store.AppTokenPermissionGlobalIndividualProjectGrantScope{
PermissionId: permId,
GrantScope: globalPermGrantScope,
ScopeId: gs,
},
}
individualProjInserts = append(individualProjInserts, individualProjGlobalPermToCreate)
default:
return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("invalid grant scope %s", gs))
}
}
}
// avoid appending empty slices
if len(permissionInserts) > 0 {
globalInserts = append(globalInserts, permissionInserts)
}
if len(permissionGrantInserts) > 0 {
globalInserts = append(globalInserts, permissionGrantInserts)
}
if len(individualOrgInserts) > 0 {
globalInserts = append(globalInserts, individualOrgInserts)
}
if len(individualProjInserts) > 0 {
globalInserts = append(globalInserts, individualProjInserts)
}
return globalInserts, nil
}
func createAppTokenOrg(ctx context.Context, token *AppToken) ([]interface{}, error) {
const op = "apptoken.(Repository).createAppTokenOrg"
var orgInserts []interface{}
// we collect inserts in their own slices so that we can use w.CreateItems above
// to batch insert by type (say 10,000 permissions at once)
var permissionInserts []*appTokenPermissionOrg
var permissionGrantInserts []*appTokenPermissionGrant
var individualProjInserts []*appTokenPermissionOrgIndividualGrantScope
tokenToCreate := &appTokenOrg{
AppTokenOrg: &store.AppTokenOrg{
PublicId: token.PublicId,
ScopeId: token.ScopeId,
Name: token.Name,
Description: token.Description,
Revoked: token.Revoked,
CreatedByUserId: token.CreatedByUserId,
TimeToStaleSeconds: token.TimeToStaleSeconds,
ExpirationTime: token.ExpirationTime,
},
}
orgInserts = append(orgInserts, []*appTokenOrg{tokenToCreate})
for _, perm := range token.Permissions {
if slices.Contains(perm.GrantedScopes, globals.GlobalPrefix) {
return nil, errors.New(ctx, errors.InvalidParameter, op, "org cannot have global grant scope")
}
if slices.Contains(perm.GrantedScopes, globals.GrantScopeDescendants) {
return nil, errors.New(ctx, errors.InvalidParameter, op, "org cannot have descendants grant scope")
}
if slices.Contains(perm.GrantedScopes, globals.GrantScopeChildren) && slices.ContainsFunc(perm.GrantedScopes, func(s string) bool {
return strings.HasPrefix(s, globals.ProjectPrefix)
}) {
return nil, errors.New(ctx, errors.InvalidParameter, op, "children grant scope cannot be combined with individual project grant scopes")
}
permId, err := newAppTokenPermissionId(ctx)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
grantThisScope := slices.Contains(perm.GrantedScopes, globals.GrantScopeThis) || slices.Contains(perm.GrantedScopes, token.GetScopeId())
orgPermGrantScope := determineGrantScope(perm.GrantedScopes)
orgPermToCreate := &appTokenPermissionOrg{
AppTokenPermissionOrg: &store.AppTokenPermissionOrg{
PrivateId: permId,
AppTokenId: token.PublicId,
GrantThisScope: grantThisScope,
GrantScope: orgPermGrantScope,
Description: perm.Label,
},
}
permissionInserts = append(permissionInserts, orgPermToCreate)
grantInserts, err := processPermissionGrants(ctx, permId, perm.Grants)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
permissionGrantInserts = append(permissionGrantInserts, grantInserts...)
// Create a copy of GrantedScopes before filtering to avoid mutating it
grantedScopes := slices.Clone(perm.GrantedScopes)
// remove GrantScopeThis and GrantScopeChildren from perm.GrantedScopes as they've already been processed
trimmedScopes := slices.DeleteFunc(grantedScopes, func(s string) bool {
return s == globals.GrantScopeThis || s == globals.GrantScopeChildren || s == token.GetScopeId()
})
for _, gs := range trimmedScopes {
if strings.HasPrefix(gs, globals.ProjectPrefix) {
individualProjOrgPermToCreate := &appTokenPermissionOrgIndividualGrantScope{
AppTokenPermissionOrgIndividualGrantScope: &store.AppTokenPermissionOrgIndividualGrantScope{
PermissionId: permId,
GrantScope: orgPermGrantScope,
ScopeId: gs,
},
}
individualProjInserts = append(individualProjInserts, individualProjOrgPermToCreate)
} else {
return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("invalid grant scope %s", gs))
}
}
}
if len(permissionInserts) > 0 {
orgInserts = append(orgInserts, permissionInserts)
}
if len(permissionGrantInserts) > 0 {
orgInserts = append(orgInserts, permissionGrantInserts)
}
if len(individualProjInserts) > 0 {
orgInserts = append(orgInserts, individualProjInserts)
}
return orgInserts, nil
}
func createAppTokenProject(ctx context.Context, token *AppToken) ([]interface{}, error) {
const op = "apptoken.(Repository).createAppTokenProject"
var projectInserts []interface{}
// we collect inserts in their own slices so that we can use w.CreateItems above
// to batch insert by type (say 10,000 permissions at once)
var permissionInserts []*appTokenPermissionProject
var permissionGrantInserts []*appTokenPermissionGrant
tokenToCreate := &appTokenProject{
AppTokenProject: &store.AppTokenProject{
PublicId: token.PublicId,
ScopeId: token.ScopeId,
Name: token.Name,
Description: token.Description,
Revoked: token.Revoked,
CreatedByUserId: token.CreatedByUserId,
TimeToStaleSeconds: token.TimeToStaleSeconds,
ExpirationTime: token.ExpirationTime,
},
}
projectInserts = append(projectInserts, []*appTokenProject{tokenToCreate})
for _, perm := range token.Permissions {
if slices.Contains(perm.GrantedScopes, globals.GrantScopeDescendants) ||
slices.Contains(perm.GrantedScopes, globals.GrantScopeChildren) ||
slices.Contains(perm.GrantedScopes, globals.GlobalPrefix) ||
slices.ContainsFunc(perm.GrantedScopes, func(s string) bool { return strings.HasPrefix(s, globals.OrgPrefix) }) {
return nil, errors.New(ctx, errors.InvalidParameter, op, "project can only contain individual project grant scopes")
}
if slices.ContainsFunc(perm.GrantedScopes, func(s string) bool {
return strings.HasPrefix(s, globals.ProjectPrefix) && s != token.GetScopeId()
}) {
return nil, errors.New(ctx, errors.InvalidParameter, op, "project cannot contain individual grant scopes for other projects")
}
permId, err := newAppTokenPermissionId(ctx)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
// true if slices contains only the individual project scope that matches the token's scope ID or `this`
grantThisScope := slices.Contains(perm.GrantedScopes, globals.GrantScopeThis) || slices.Contains(perm.GrantedScopes, token.GetScopeId())
projPermToCreate := &appTokenPermissionProject{
AppTokenPermissionProject: &store.AppTokenPermissionProject{
PrivateId: permId,
AppTokenId: token.PublicId,
GrantThisScope: grantThisScope,
Description: perm.Label,
},
}
permissionInserts = append(permissionInserts, projPermToCreate)
grantInserts, err := processPermissionGrants(ctx, permId, perm.Grants)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
permissionGrantInserts = append(permissionGrantInserts, grantInserts...)
}
if len(permissionInserts) > 0 {
projectInserts = append(projectInserts, permissionInserts)
}
if len(permissionGrantInserts) > 0 {
projectInserts = append(projectInserts, permissionGrantInserts)
}
return projectInserts, nil
}
// processPermissionGrants validates grants and creates grant objects for insertion
func processPermissionGrants(ctx context.Context, permId string, grants []string) ([]*appTokenPermissionGrant, error) {
const op = "apptoken.processPermissionGrants"
var grantInserts []*appTokenPermissionGrant
for _, grant := range grants {
// Validate that the grant parses successfully. Note that we fake the scope
// here to avoid a lookup as the scope is only relevant at actual ACL
// checking time and we just care that it parses correctly.
parsedGrant, err := perms.Parse(ctx, perms.GrantTuple{RoleScopeId: "o_abcd1234", GrantScopeId: "o_abcd1234", Grant: grant})
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("parsing grant string"))
}
permissionGrantToCreate := &appTokenPermissionGrant{
AppTokenPermissionGrant: &store.AppTokenPermissionGrant{
PermissionId: permId,
CanonicalGrant: parsedGrant.CanonicalString(),
RawGrant: grant,
},
}
grantInserts = append(grantInserts, permissionGrantToCreate)
}
return grantInserts, nil
}
// determineGrantScope determines the appropriate grant scope value based on granted scopes
func determineGrantScope(grantedScopes []string) string {
if slices.Contains(grantedScopes, globals.GrantScopeChildren) {
return globals.GrantScopeChildren
} else if slices.Contains(grantedScopes, globals.GrantScopeDescendants) {
return globals.GrantScopeDescendants
}
return globals.GrantScopeIndividual
}
// listAppTokens lists tokens across all three token subtypes (global, org, proj).
// Cipher information and permissions are not included when listing a token.
func (r *Repository) listAppTokens(ctx context.Context, withScopeIds []string, opt ...Option) ([]*AppToken, time.Time, error) {
const op = "apptoken.(Repository).listAppTokens"
if len(withScopeIds) == 0 {
return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "missing scope id")
}
opts := getOpts(opt...)
limit := r.defaultLimit
if opts.withLimit != 0 {
limit = opts.withLimit
}
args := []any{sql.Named("scope_ids", withScopeIds)}
whereClause := "scope_id in @scope_ids"
if opts.withStartPageAfterItem != nil {
whereClause = fmt.Sprintf("(create_time, public_id) < (@last_item_create_time, @last_item_id) and %s", whereClause)
args = append(args,
sql.Named("last_item_create_time", opts.withStartPageAfterItem.GetCreateTime()),
sql.Named("last_item_id", opts.withStartPageAfterItem.GetPublicId()),
)
}
dbOpts := []db.Option{db.WithLimit(limit), db.WithOrder("create_time desc, public_id desc")}
return r.queryAppTokens(ctx, whereClause, args, dbOpts...)
}
// listAppTokenRefresh lists tokens across all three token subtypes (global, org, proj) that have been
// updated after the provided time. Cipher information and permissions are not included when listing a token.
// App Tokens are considered updated when
// - update_time is after updatedAfter
// - expiration_time is after updatedAfter but before now
// - last_approximate_access_time + time_to_stale_seconds is (before now and before expiration_time) and after updatedAfter
func (r *Repository) listAppTokensRefresh(ctx context.Context, updatedAfter time.Time, withScopeIds []string, opt ...Option) ([]*AppToken, time.Time, error) {
const op = "apptoken.(Repository).listAppTokenRefresh"
switch {
case updatedAfter.IsZero():
return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "missing updatedAfter time")
case len(withScopeIds) == 0:
return nil, time.Time{}, errors.New(ctx, errors.InvalidParameter, op, "missing scope ids")
}
opts := getOpts(opt...)
limit := r.defaultLimit
if opts.withLimit != 0 {
limit = opts.withLimit
}
args := []any{
sql.Named("scope_ids", withScopeIds),
sql.Named("updated_after_time", timestamp.New(updatedAfter)),
}
whereClause := "scope_id in @scope_ids and " +
"(update_time > @updated_after_time or " +
"(expiration_time > @updated_after_time and expiration_time <= CURRENT_TIMESTAMP) or " +
"( (approximate_last_access_time is not null and time_to_stale_seconds is not null) and " +
"( (approximate_last_access_time + (time_to_stale_seconds || ' seconds')::interval) <= CURRENT_TIMESTAMP) and " +
"( (approximate_last_access_time + (time_to_stale_seconds || ' seconds')::interval) > @updated_after_time) and " +
"(expiration_time is null or (approximate_last_access_time + (time_to_stale_seconds || ' seconds')::interval) < expiration_time) ))"
if opts.withStartPageAfterItem != nil {
whereClause = fmt.Sprintf("(update_time, public_id) < (@last_item_update_time, @last_item_id) and %s", whereClause)
args = append(args,
sql.Named("last_item_update_time", opts.withStartPageAfterItem.GetUpdateTime()),
sql.Named("last_item_id", opts.withStartPageAfterItem.GetPublicId()),
)
}
dbOpts := []db.Option{db.WithLimit(limit), db.WithOrder("update_time desc, public_id desc")}
return r.queryAppTokens(ctx, whereClause, args, dbOpts...)
}
func (r *Repository) queryAppTokens(ctx context.Context, whereClause string, args []any, opt ...db.Option) ([]*AppToken, time.Time, error) {
const op = "apptoken.(Repository).queryAppTokens"
var transactionTimestamp time.Time
var appTokens []*AppToken
if _, err := r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(rd db.Reader, w db.Writer) error {
var atvs []*appTokenView
err := rd.SearchWhere(ctx, &atvs, whereClause, args, opt...)
if err != nil {
return errors.Wrap(ctx, err, op)
}
appTokens = make([]*AppToken, 0, len(atvs))
for _, atv := range atvs {
appTokens = append(appTokens, atv.toAppToken())
}
transactionTimestamp, err = rd.Now(ctx)
return err
}); err != nil {
return nil, time.Time{}, err
}
return appTokens, transactionTimestamp, nil
}
// listDeletedIds lists the public IDs of any app tokens deleted since the timestamp provided.
func (r *Repository) listDeletedIds(ctx context.Context, since time.Time) ([]string, time.Time, error) {
const op = "apptoken.(Repository).listDeletedIds"
var deletedAppTokens []*deletedAppToken
var transactionTimestamp time.Time
if _, err := r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, func(r db.Reader, _ db.Writer) error {
if err := r.SearchWhere(ctx, &deletedAppTokens, "delete_time >= ?", []any{since}); err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("failed to query deleted app tokens"))
}
var err error
transactionTimestamp, err = r.Now(ctx)
if err != nil {
return errors.Wrap(ctx, err, op, errors.WithMsg("failed to get transaction timestamp"))
}
return nil
}); err != nil {
return nil, time.Time{}, err
}
var deletedIds []string
for _, at := range deletedAppTokens {
deletedIds = append(deletedIds, at.PublicId)
}
return deletedIds, transactionTimestamp, nil
}
// estimatedCount returns an estimate of the total number of items in the global, org, and project app token tables.
func (r *Repository) estimatedCount(ctx context.Context) (int, error) {
const op = "apptoken.(Repository).estimatedCount"
rows, err := r.reader.Query(ctx, estimateCountAppTokens, nil)
if err != nil {
return 0, errors.Wrap(ctx, err, op, errors.WithMsg("failed to query total app tokens"))
}
var count int
for rows.Next() {
if err := r.reader.ScanRows(ctx, rows, &count); err != nil {
return 0, errors.Wrap(ctx, err, op, errors.WithMsg("failed to query total app tokens"))
}
}
if err := rows.Err(); err != nil {
return 0, errors.Wrap(ctx, err, op, errors.WithMsg("failed to query total app tokens"))
}
return count, nil
}
// getAppTokenScopeId returns the scope id of the app token
func getAppTokenScopeId(ctx context.Context, reader db.Reader, id string) (string, error) {
const op = "apptoken.getAppTokenScopeId"
if id == "" {
return "", errors.New(ctx, errors.InvalidParameter, op, "missing app token id")
}
if reader == nil {
return "", errors.New(ctx, errors.InvalidParameter, op, "missing db.Reader")
}
rows, err := reader.Query(ctx, scopeIdFromAppTokenIdQuery, []any{sql.Named("public_id", id)})
if err != nil {
return "", errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed to lookup app token scope for id: %s", id)))
}
defer rows.Close()
var scopeId string
for rows.Next() {
if err := reader.ScanRows(ctx, rows, &scopeId); err != nil {
return "", errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed scan results from querying app token scope for id: %s", id)))
}
}
if err := rows.Err(); err != nil {
return "", errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unexpected error scanning results from querying app token scope for id: %s", id)))
}
if scopeId == "" {
return "", errors.New(ctx, errors.RecordNotFound, op, fmt.Sprintf("app token %s not found", id))
}
return scopeId, nil
}