feat(repository): Implement Create project app token functionality (#6343)

* Squash: merge app-token-prototype into mikemountain-sql-schema-and-pgtap-tests

* add pgtap tests and fkeys

* wip

* [ICU-18111] implement repository create functionality for org, refactor create code to handle multiple scope types

* wip

* update to include description

* make gen proto

* add nil check

* update tests

* pr comments

* address pr comments and tweak some logic

* merge issues

* fix logic in permissions
pull/6420/merge
Michael Milton 4 months ago committed by GitHub
parent 6ea32ecbc9
commit 5f3d4a449d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -216,6 +216,8 @@ protobuild:
@protoc-go-inject-tag -input=./internal/apptoken/store/apptoken_global_permission.pb.go
@protoc-go-inject-tag -input=./internal/apptoken/store/apptoken_org.pb.go
@protoc-go-inject-tag -input=./internal/apptoken/store/apptoken_org_permission.pb.go
@protoc-go-inject-tag -input=./internal/apptoken/store/apptoken_project.pb.go
@protoc-go-inject-tag -input=./internal/apptoken/store/apptoken_project_permission.pb.go
@protoc-go-inject-tag -input=./internal/apptoken/store/apptoken_global_permission_individual_org_grant_scope.pb.go
@protoc-go-inject-tag -input=./internal/apptoken/store/apptoken_global_permission_individual_project_grant_scope.pb.go
@protoc-go-inject-tag -input=./internal/apptoken/store/apptoken_org_permission_individual_grant_scope.pb.go

@ -30,6 +30,9 @@ const (
appTokenPermissionOrgTableName = "app_token_permission_org"
appTokenPermissionOrgIndividualGrantScopeTableName = "app_token_permission_org_individual_grant_scope"
appTokenProjectTableName = "app_token_project"
appTokenPermissionProjectTableName = "app_token_permission_project"
// The version prefix is used to differentiate token versions just for future proofing.
tokenValueVersionPrefix = "0"
tokenLength = 24
@ -206,6 +209,25 @@ func (ato *appTokenOrg) SetTableName(n string) {
ato.tableName = n
}
// for app_token_project (triggers an insert to app_token)
type appTokenProject struct {
*store.AppTokenProject
tableName string
}
// TableName returns the table name.
func (atp *appTokenProject) TableName() string {
if atp.tableName != "" {
return atp.tableName
}
return appTokenProjectTableName
}
// SetTableName sets the table name.
func (atp *appTokenProject) SetTableName(n string) {
atp.tableName = n
}
// for app_token_cipher
type appTokenCipher struct {
*store.AppTokenCipher
@ -317,6 +339,25 @@ func (atop *appTokenPermissionOrgIndividualGrantScope) SetTableName(n string) {
atop.tableName = n
}
// for app_token_permission_project (triggers an insert to app_token_permission)
type appTokenPermissionProject struct {
*store.AppTokenPermissionProject
tableName string
}
// TableName returns the table name.
func (atpp *appTokenPermissionProject) TableName() string {
if atpp.tableName != "" {
return atpp.tableName
}
return appTokenPermissionProjectTableName
}
// SetTableName sets the table name.
func (atpp *appTokenPermissionProject) SetTableName(n string) {
atpp.tableName = n
}
// for app_token_permission_grant (triggers an insert to iam_grant and iam_grant_resource_enm)
type appTokenPermissionGrant struct {
*store.AppTokenPermissionGrant
@ -361,6 +402,71 @@ func (atc *appTokenCipher) decrypt(ctx context.Context, cipher wrapping.Wrapper)
return nil
}
// the appTokenSubtype interface allows us to implement the
// toAppToken method for each app token type, which allows us to
// access the db created fields (CreateTime, etc.) when converting
// to the common AppToken type.
type appTokenSubtype interface {
toAppToken() *AppToken
}
func (atg *appTokenGlobal) toAppToken() *AppToken {
if atg == nil {
return nil
}
return &AppToken{
PublicId: atg.PublicId,
ScopeId: atg.ScopeId,
Name: atg.Name,
Description: atg.Description,
CreateTime: atg.CreateTime,
// add update time eventually
ApproximateLastAccessTime: atg.ApproximateLastAccessTime,
ExpirationTime: atg.ExpirationTime,
TimeToStaleSeconds: atg.TimeToStaleSeconds,
CreatedByUserId: atg.CreatedByUserId,
Revoked: atg.Revoked,
}
}
func (ato *appTokenOrg) toAppToken() *AppToken {
if ato == nil {
return nil
}
return &AppToken{
PublicId: ato.PublicId,
ScopeId: ato.ScopeId,
Name: ato.Name,
Description: ato.Description,
CreateTime: ato.CreateTime,
// add update time eventually
ApproximateLastAccessTime: ato.ApproximateLastAccessTime,
ExpirationTime: ato.ExpirationTime,
TimeToStaleSeconds: ato.TimeToStaleSeconds,
CreatedByUserId: ato.CreatedByUserId,
Revoked: ato.Revoked,
}
}
func (atp *appTokenProject) toAppToken() *AppToken {
if atp == nil {
return nil
}
return &AppToken{
PublicId: atp.PublicId,
ScopeId: atp.ScopeId,
Name: atp.Name,
Description: atp.Description,
CreateTime: atp.CreateTime,
// add update time eventually
ApproximateLastAccessTime: atp.ApproximateLastAccessTime,
ExpirationTime: atp.ExpirationTime,
TimeToStaleSeconds: atp.TimeToStaleSeconds,
CreatedByUserId: atp.CreatedByUserId,
Revoked: atp.Revoked,
}
}
// deletedAppToken represents a deleted app token record in the app_token_deleted table.
// These records are trimmed after a 30 day retention period.
type deletedAppToken struct {

@ -61,24 +61,27 @@ func (r *Repository) CreateAppToken(ctx context.Context, token *AppToken) (*AppT
}
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{}
var createdToken interface{} // store reference to the token for reading db generated values
var createdToken appTokenSubtype
switch {
case strings.HasPrefix(token.GetScopeId(), globals.GlobalPrefix):
token, dbInserts, createdToken, err = r.createAppTokenGlobal(ctx, token)
createdToken, dbInserts, err = createAppTokenGlobal(ctx, token)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
case strings.HasPrefix(token.GetScopeId(), globals.OrgPrefix):
token, dbInserts, createdToken, err = r.createAppTokenOrg(ctx, token)
createdToken, dbInserts, err = createAppTokenOrg(ctx, token)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
case strings.HasPrefix(token.GetScopeId(), globals.ProjectPrefix):
createdToken, dbInserts, err = createAppTokenProject(ctx, token)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
// case strings.HasPrefix(token.GetScopeId(), globals.ProjectPrefix):
// token, dbInserts, err = r.createAppTokenProj(ctx, token, id)
// if err != nil {
// return nil, errors.Wrap(ctx, err, op)
// }
default:
return nil, errors.New(ctx, errors.InvalidParameter, op, "invalid scope type")
}
@ -120,38 +123,17 @@ func (r *Repository) CreateAppToken(ctx context.Context, token *AppToken) (*AppT
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("creating app token in database"))
}
var createTime, expirationTime, approximateLastAccessTime *timestamp.Timestamp
// extract db generated fields from the created token reference
switch ct := createdToken.(type) {
case *appTokenGlobal:
createTime = ct.CreateTime
approximateLastAccessTime = ct.ApproximateLastAccessTime
expirationTime = ct.ExpirationTime
case *appTokenOrg:
createTime = ct.CreateTime
approximateLastAccessTime = ct.ApproximateLastAccessTime
expirationTime = ct.ExpirationTime
default:
return nil, errors.New(ctx, errors.InvalidParameter, op, "unable to read created token reference")
}
newAppToken := &AppToken{
PublicId: token.PublicId,
Name: token.Name,
Description: token.Description,
CreatedByUserId: token.CreatedByUserId,
ScopeId: token.ScopeId,
ApproximateLastAccessTime: approximateLastAccessTime,
CreateTime: createTime,
ExpirationTime: expirationTime,
Revoked: token.Revoked,
TimeToStaleSeconds: token.TimeToStaleSeconds,
Permissions: token.Permissions,
Token: cipherToken,
newAppToken := createdToken.toAppToken()
if newAppToken == nil {
return nil, errors.New(ctx, errors.Internal, op, "failed to convert created app token to domain object")
}
newAppToken.Token = cipherToken
newAppToken.Permissions = token.Permissions
return newAppToken, nil
}
func (r *Repository) createAppTokenGlobal(ctx context.Context, token *AppToken) (*AppToken, []interface{}, *appTokenGlobal, error) {
func createAppTokenGlobal(ctx context.Context, token *AppToken) (*appTokenGlobal, []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
@ -179,28 +161,28 @@ func (r *Repository) createAppTokenGlobal(ctx context.Context, token *AppToken)
// 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, nil, nil, errors.New(ctx, errors.InvalidParameter, op, "only one of descendants or children grant scope can be specified")
return nil, 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, nil, nil, errors.New(ctx, errors.InvalidParameter, op, "descendants grant scope cannot be combined with individual project grant scopes")
return nil, 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, nil, nil, errors.New(ctx, errors.InvalidParameter, op, "children grant scope cannot be combined with individual org grant scopes")
return nil, 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, nil, nil, errors.Wrap(ctx, err, op)
return nil, nil, errors.Wrap(ctx, err, op)
}
grantThisScope := slices.Contains(perm.GrantedScopes, globals.GrantScopeThis)
grantThisScope := slices.Contains(perm.GrantedScopes, globals.GrantScopeThis) || slices.Contains(perm.GrantedScopes, globals.GlobalPrefix)
globalPermGrantScope := determineGrantScope(perm.GrantedScopes)
globalPermToCreate := &appTokenPermissionGlobal{
AppTokenPermissionGlobal: &store.AppTokenPermissionGlobal{
@ -212,21 +194,21 @@ func (r *Repository) createAppTokenGlobal(ctx context.Context, token *AppToken)
},
}
permissionInserts = append(permissionInserts, globalPermToCreate)
// globalInserts = append(globalInserts, globalPermToCreate)
grantInserts, err := processPermissionGrants(ctx, permId, perm.Grants)
if err != nil {
return nil, nil, nil, errors.Wrap(ctx, err, op)
return nil, nil, errors.Wrap(ctx, err, op)
}
permissionGrantInserts = append(permissionGrantInserts, grantInserts...)
// globalInserts = append(globalInserts, grantInserts...)
for _, gs := range perm.GrantedScopes {
if gs == globals.GrantScopeThis ||
gs == globals.GrantScopeChildren ||
gs == globals.GrantScopeDescendants {
continue
}
trimmedScopes := slices.DeleteFunc(perm.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{
@ -247,7 +229,7 @@ func (r *Repository) createAppTokenGlobal(ctx context.Context, token *AppToken)
}
individualProjInserts = append(individualProjInserts, individualProjGlobalPermToCreate)
default:
return nil, nil, nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("invalid grant scope %s", gs))
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("invalid grant scope %s", gs))
}
}
}
@ -265,10 +247,10 @@ func (r *Repository) createAppTokenGlobal(ctx context.Context, token *AppToken)
globalInserts = append(globalInserts, individualProjInserts)
}
return token, globalInserts, tokenToCreate, nil
return tokenToCreate, globalInserts, nil
}
func (r *Repository) createAppTokenOrg(ctx context.Context, token *AppToken) (*AppToken, []interface{}, *appTokenOrg, error) {
func createAppTokenOrg(ctx context.Context, token *AppToken) (*appTokenOrg, []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
@ -293,21 +275,24 @@ func (r *Repository) createAppTokenOrg(ctx context.Context, token *AppToken) (*A
orgInserts = append(orgInserts, []*appTokenOrg{tokenToCreate})
for _, perm := range token.Permissions {
if slices.Contains(perm.GrantedScopes, globals.GlobalPrefix) {
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "org cannot have global grant scope")
}
if slices.Contains(perm.GrantedScopes, globals.GrantScopeDescendants) {
return nil, nil, nil, errors.New(ctx, errors.InvalidParameter, op, "org cannot have descendants grant scope")
return nil, 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, nil, nil, errors.New(ctx, errors.InvalidParameter, op, "children grant scope cannot be combined with individual project grant scopes")
return nil, 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, nil, nil, errors.Wrap(ctx, err, op)
return nil, nil, errors.Wrap(ctx, err, op)
}
grantThisScope := slices.Contains(perm.GrantedScopes, globals.GrantScopeThis)
grantThisScope := slices.Contains(perm.GrantedScopes, globals.GrantScopeThis) || slices.Contains(perm.GrantedScopes, token.GetScopeId())
orgPermGrantScope := determineGrantScope(perm.GrantedScopes)
orgPermToCreate := &appTokenPermissionOrg{
@ -324,15 +309,16 @@ func (r *Repository) createAppTokenOrg(ctx context.Context, token *AppToken) (*A
grantInserts, err := processPermissionGrants(ctx, permId, perm.Grants)
if err != nil {
return nil, nil, nil, errors.Wrap(ctx, err, op)
return nil, nil, errors.Wrap(ctx, err, op)
}
permissionGrantInserts = append(permissionGrantInserts, grantInserts...)
for _, gs := range perm.GrantedScopes {
if gs == globals.GrantScopeThis || gs == globals.GrantScopeChildren {
continue
}
// remove GrantScopeThis and GrantScopeChildren from perm.GrantedScopes as they've already been processed
trimmedScopes := slices.DeleteFunc(perm.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{
@ -343,7 +329,7 @@ func (r *Repository) createAppTokenOrg(ctx context.Context, token *AppToken) (*A
}
individualProjInserts = append(individualProjInserts, individualProjOrgPermToCreate)
} else {
return nil, nil, nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("invalid grant scope %s", gs))
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("invalid grant scope %s", gs))
}
}
}
@ -358,7 +344,78 @@ func (r *Repository) createAppTokenOrg(ctx context.Context, token *AppToken) (*A
orgInserts = append(orgInserts, individualProjInserts)
}
return token, orgInserts, tokenToCreate, nil
return tokenToCreate, orgInserts, nil
}
func createAppTokenProject(ctx context.Context, token *AppToken) (*appTokenProject, []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, 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, 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, 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, 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 tokenToCreate, projectInserts, nil
}
// TODO: Implement additional fields in AppToken and complete this method

@ -339,6 +339,30 @@ func TestRepository_CreateAppToken(t *testing.T) {
},
wantErr: false,
},
{
name: "valid-global-one-perm-multi-individuals",
at: &AppToken{
ScopeId: globals.GlobalPrefix,
CreatedByUserId: u.PublicId,
Permissions: []AppTokenPermission{
{
Label: "test",
Grants: []string{"type=host-catalog;actions=list", "type=session;actions=list"},
GrantedScopes: []string{"this", org.GetPublicId(), proj.GetPublicId()},
},
},
},
wantPerms: []testPermission{
{
GrantThis: true,
GrantScope: "individual",
Description: "test",
Grants: []string{"type=host-catalog;actions=list", "type=session;actions=list"},
Scopes: []string{org.GetPublicId(), proj.GetPublicId()},
},
},
wantErr: false,
},
// org
{
name: "valid-org-basic-no-perms",
@ -504,6 +528,69 @@ func TestRepository_CreateAppToken(t *testing.T) {
},
wantErr: false,
},
// project
{
name: "valid-project-basic-no-perms",
at: &AppToken{
ScopeId: proj.GetPublicId(),
CreatedByUserId: u.PublicId,
},
wantErr: false,
},
{
name: "valid-project-one-perm",
at: &AppToken{
ScopeId: proj.GetPublicId(),
CreatedByUserId: u.PublicId,
Permissions: []AppTokenPermission{
{
Label: "test",
Grants: []string{"type=host-catalog;actions=list", "type=session;actions=list"},
GrantedScopes: []string{"this", proj.GetPublicId()},
},
},
},
wantPerms: []testPermission{
{
GrantThis: true,
Description: "test",
Grants: []string{"type=host-catalog;actions=list", "type=session;actions=list"},
},
},
wantErr: false,
},
{
name: "valid-project-two-perm",
at: &AppToken{
ScopeId: proj.PublicId,
CreatedByUserId: u.PublicId,
Permissions: []AppTokenPermission{
{
Label: "test",
Grants: []string{"type=host-catalog;actions=list", "type=session;actions=list"},
GrantedScopes: []string{"this", proj.GetPublicId()},
},
{
Label: "test-2",
Grants: []string{"type=target;actions=list"},
GrantedScopes: []string{proj.GetPublicId()},
},
},
},
wantErr: false,
wantPerms: []testPermission{
{
GrantThis: true,
Description: "test",
Grants: []string{"type=host-catalog;actions=list", "type=session;actions=list"},
},
{
GrantThis: true,
Description: "test-2",
Grants: []string{"type=target;actions=list"},
},
},
},
// invalid
{
name: "invalid-global-bad-grant",
@ -576,6 +663,23 @@ func TestRepository_CreateAppToken(t *testing.T) {
wantIsError: errors.Exception,
wantErrMsg: "is not a child of org",
},
{
name: "invalid-org-granted-global",
at: &AppToken{
ScopeId: org.PublicId,
CreatedByUserId: u.PublicId,
Permissions: []AppTokenPermission{
{
Label: "test",
Grants: []string{"type=host-catalog;actions=list"},
GrantedScopes: []string{globals.GlobalPrefix},
},
},
},
wantErr: true,
wantIsError: errors.InvalidParameter,
wantErrMsg: "org cannot have global grant scope",
},
{
name: "invalid-org-project-and-children",
at: &AppToken{
@ -610,6 +714,108 @@ func TestRepository_CreateAppToken(t *testing.T) {
wantIsError: errors.InvalidParameter,
wantErrMsg: "org cannot have descendants grant scope",
},
{
name: "invalid-org-project-and-children",
at: &AppToken{
ScopeId: org.PublicId,
CreatedByUserId: u.PublicId,
Permissions: []AppTokenPermission{
{
Label: "test",
Grants: []string{"type=host-catalog;actions=list"},
GrantedScopes: []string{proj.GetPublicId(), "children"},
},
},
},
wantErr: true,
wantIsError: errors.InvalidParameter,
wantErrMsg: "children grant scope cannot be combined with individual project grant scopes",
},
{
name: "invalid-org-descendants",
at: &AppToken{
ScopeId: org.PublicId,
CreatedByUserId: u.PublicId,
Permissions: []AppTokenPermission{
{
Label: "test",
Grants: []string{"type=host-catalog;actions=list"},
GrantedScopes: []string{"descendants"},
},
},
},
wantErr: true,
wantIsError: errors.InvalidParameter,
wantErrMsg: "org cannot have descendants grant scope",
},
{
name: "invalid-proj-children",
at: &AppToken{
ScopeId: proj.PublicId,
CreatedByUserId: u.PublicId,
Permissions: []AppTokenPermission{
{
Label: "test",
Grants: []string{"type=host-catalog;actions=list"},
GrantedScopes: []string{"children"},
},
},
},
wantErr: true,
wantIsError: errors.InvalidParameter,
wantErrMsg: "project can only contain individual project grant scopes",
},
{
name: "invalid-proj-individual-org",
at: &AppToken{
ScopeId: proj.PublicId,
CreatedByUserId: u.PublicId,
Permissions: []AppTokenPermission{
{
Label: "test",
Grants: []string{"type=host-catalog;actions=list"},
GrantedScopes: []string{org.GetPublicId()},
},
},
},
wantErr: true,
wantIsError: errors.InvalidParameter,
wantErrMsg: "project can only contain individual project grant scopes",
},
{
name: "invalid-proj-different-proj",
at: &AppToken{
ScopeId: proj.PublicId,
CreatedByUserId: u.PublicId,
Permissions: []AppTokenPermission{
{
Label: "test",
Grants: []string{"type=host-catalog;actions=list"},
GrantedScopes: []string{proj2.GetPublicId()},
},
},
},
wantErr: true,
wantIsError: errors.InvalidParameter,
wantErrMsg: "project cannot contain individual grant scopes for other projects",
},
{
name: "invalid-duplicate-grants",
at: &AppToken{
ScopeId: globals.GlobalPrefix,
CreatedByUserId: u.PublicId,
Permissions: []AppTokenPermission{
{
Label: "test",
Grants: []string{"type=host-catalog;actions=list", "type=host-catalog;actions=list"},
GrantedScopes: []string{"descendants"},
},
},
},
wantErr: true,
wantIsError: errors.NotUnique,
wantErrMsg: "unique constraint violation",
},
{
name: "nil-token",
at: nil,
@ -650,14 +856,16 @@ func TestRepository_CreateAppToken(t *testing.T) {
return
}
assert.NoError(err)
assert.NotNil(at.PublicId)
assert.NotNil(at.CreateTime)
assert.NotNil(at.ApproximateLastAccessTime)
assert.NotNil(at.Token)
assert.Equal(at.CreateTime, at.ApproximateLastAccessTime)
assert.GreaterOrEqual(at.ExpirationTime.AsTime().Unix(), at.CreateTime.AsTime().Unix())
// validate app token permission global using db queries
if tt.at.Permissions != nil {
// validate app token permissions using db queries
if tt.wantPerms != nil {
assert.NotNil(at.Permissions)
err = testCheckPermission(t, repo, at.PublicId, tt.at.ScopeId, tt.wantPerms)
assert.NoError(err)
}

@ -0,0 +1,251 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.10
// protoc (unknown)
// source: controller/storage/apptoken/store/v1/apptoken_project.proto
package store
import (
timestamp "github.com/hashicorp/boundary/internal/db/timestamp"
_ "github.com/hashicorp/boundary/sdk/pbs/controller/protooptions"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
// AppTokenProject represents app tokens created in project scope
type AppTokenProject struct {
state protoimpl.MessageState `protogen:"open.v1"`
// public_id is used to access the App Token via an API
// @inject_tag: gorm:"primary_key"
PublicId string `protobuf:"bytes,1,opt,name=public_id,json=publicId,proto3" json:"public_id,omitempty" gorm:"primary_key"`
// scope id for the app token
// @inject_tag: `gorm:"default:null"`
ScopeId string `protobuf:"bytes,2,opt,name=scope_id,json=scopeId,proto3" json:"scope_id,omitempty" gorm:"default:null"`
// name is the optional friendly name used to
// access the App Token via an API
// @inject_tag: `gorm:"default:null"`
Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty" gorm:"default:null"`
// description of the app token
// @inject_tag: `gorm:"default:null"`
Description string `protobuf:"bytes,4,opt,name=description,proto3" json:"description,omitempty" gorm:"default:null"`
// control if this app token has been revoked
// @inject_tag: `gorm:"default:false"`
Revoked bool `protobuf:"varint,5,opt,name=revoked,proto3" json:"revoked,omitempty" gorm:"default:false"`
// create_time from the RDBMS
// @inject_tag: `gorm:"default:current_timestamp"`
CreateTime *timestamp.Timestamp `protobuf:"bytes,6,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"`
// update_time from the RDBMS
// @inject_tag: `gorm:"default:current_timestamp"`
UpdateTime *timestamp.Timestamp `protobuf:"bytes,7,opt,name=update_time,json=updateTime,proto3" json:"update_time,omitempty" gorm:"default:current_timestamp"`
// created_by_user_id is the ID of the user that created this App Token
// @inject_tag: `gorm:"default:not_null"`
CreatedByUserId string `protobuf:"bytes,8,opt,name=created_by_user_id,json=createdByUserId,proto3" json:"created_by_user_id,omitempty" gorm:"default:not_null"`
// approximate_last_access_time from the RDBMS
// @inject_tag: `gorm:"default:current_timestamp"`
ApproximateLastAccessTime *timestamp.Timestamp `protobuf:"bytes,9,opt,name=approximate_last_access_time,json=approximateLastAccessTime,proto3" json:"approximate_last_access_time,omitempty" gorm:"default:current_timestamp"`
// time_to_stale_seconds is the number of seconds of inactivity after which
// the token is considered stale
TimeToStaleSeconds uint32 `protobuf:"varint,10,opt,name=time_to_stale_seconds,json=timeToStaleSeconds,proto3" json:"time_to_stale_seconds,omitempty"`
// expiration_time from the RDBMS
// @inject_tag: `gorm:"default:current_timestamp + interval '3 years'"`
ExpirationTime *timestamp.Timestamp `protobuf:"bytes,11,opt,name=expiration_time,json=expirationTime,proto3" json:"expiration_time,omitempty" gorm:"default:current_timestamp + interval '3 years'"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *AppTokenProject) Reset() {
*x = AppTokenProject{}
mi := &file_controller_storage_apptoken_store_v1_apptoken_project_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *AppTokenProject) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AppTokenProject) ProtoMessage() {}
func (x *AppTokenProject) ProtoReflect() protoreflect.Message {
mi := &file_controller_storage_apptoken_store_v1_apptoken_project_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use AppTokenProject.ProtoReflect.Descriptor instead.
func (*AppTokenProject) Descriptor() ([]byte, []int) {
return file_controller_storage_apptoken_store_v1_apptoken_project_proto_rawDescGZIP(), []int{0}
}
func (x *AppTokenProject) GetPublicId() string {
if x != nil {
return x.PublicId
}
return ""
}
func (x *AppTokenProject) GetScopeId() string {
if x != nil {
return x.ScopeId
}
return ""
}
func (x *AppTokenProject) GetName() string {
if x != nil {
return x.Name
}
return ""
}
func (x *AppTokenProject) GetDescription() string {
if x != nil {
return x.Description
}
return ""
}
func (x *AppTokenProject) GetRevoked() bool {
if x != nil {
return x.Revoked
}
return false
}
func (x *AppTokenProject) GetCreateTime() *timestamp.Timestamp {
if x != nil {
return x.CreateTime
}
return nil
}
func (x *AppTokenProject) GetUpdateTime() *timestamp.Timestamp {
if x != nil {
return x.UpdateTime
}
return nil
}
func (x *AppTokenProject) GetCreatedByUserId() string {
if x != nil {
return x.CreatedByUserId
}
return ""
}
func (x *AppTokenProject) GetApproximateLastAccessTime() *timestamp.Timestamp {
if x != nil {
return x.ApproximateLastAccessTime
}
return nil
}
func (x *AppTokenProject) GetTimeToStaleSeconds() uint32 {
if x != nil {
return x.TimeToStaleSeconds
}
return 0
}
func (x *AppTokenProject) GetExpirationTime() *timestamp.Timestamp {
if x != nil {
return x.ExpirationTime
}
return nil
}
var File_controller_storage_apptoken_store_v1_apptoken_project_proto protoreflect.FileDescriptor
const file_controller_storage_apptoken_store_v1_apptoken_project_proto_rawDesc = "" +
"\n" +
";controller/storage/apptoken/store/v1/apptoken_project.proto\x12$controller.storage.apptoken.store.v1\x1a*controller/custom_options/v1/options.proto\x1a/controller/storage/timestamp/v1/timestamp.proto\"\x87\x05\n" +
"\x0fAppTokenProject\x12\x1b\n" +
"\tpublic_id\x18\x01 \x01(\tR\bpublicId\x12\x19\n" +
"\bscope_id\x18\x02 \x01(\tR\ascopeId\x12$\n" +
"\x04name\x18\x03 \x01(\tB\x10\xc2\xdd)\f\n" +
"\x04name\x12\x04nameR\x04name\x12@\n" +
"\vdescription\x18\x04 \x01(\tB\x1e\xc2\xdd)\x1a\n" +
"\vdescription\x12\vdescriptionR\vdescription\x12\x18\n" +
"\arevoked\x18\x05 \x01(\bR\arevoked\x12K\n" +
"\vcreate_time\x18\x06 \x01(\v2*.controller.storage.timestamp.v1.TimestampR\n" +
"createTime\x12K\n" +
"\vupdate_time\x18\a \x01(\v2*.controller.storage.timestamp.v1.TimestampR\n" +
"updateTime\x12+\n" +
"\x12created_by_user_id\x18\b \x01(\tR\x0fcreatedByUserId\x12k\n" +
"\x1capproximate_last_access_time\x18\t \x01(\v2*.controller.storage.timestamp.v1.TimestampR\x19approximateLastAccessTime\x121\n" +
"\x15time_to_stale_seconds\x18\n" +
" \x01(\rR\x12timeToStaleSeconds\x12S\n" +
"\x0fexpiration_time\x18\v \x01(\v2*.controller.storage.timestamp.v1.TimestampR\x0eexpirationTimeB=Z;github.com/hashicorp/boundary/internal/apptoken/store;storeb\x06proto3"
var (
file_controller_storage_apptoken_store_v1_apptoken_project_proto_rawDescOnce sync.Once
file_controller_storage_apptoken_store_v1_apptoken_project_proto_rawDescData []byte
)
func file_controller_storage_apptoken_store_v1_apptoken_project_proto_rawDescGZIP() []byte {
file_controller_storage_apptoken_store_v1_apptoken_project_proto_rawDescOnce.Do(func() {
file_controller_storage_apptoken_store_v1_apptoken_project_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_controller_storage_apptoken_store_v1_apptoken_project_proto_rawDesc), len(file_controller_storage_apptoken_store_v1_apptoken_project_proto_rawDesc)))
})
return file_controller_storage_apptoken_store_v1_apptoken_project_proto_rawDescData
}
var file_controller_storage_apptoken_store_v1_apptoken_project_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_controller_storage_apptoken_store_v1_apptoken_project_proto_goTypes = []any{
(*AppTokenProject)(nil), // 0: controller.storage.apptoken.store.v1.AppTokenProject
(*timestamp.Timestamp)(nil), // 1: controller.storage.timestamp.v1.Timestamp
}
var file_controller_storage_apptoken_store_v1_apptoken_project_proto_depIdxs = []int32{
1, // 0: controller.storage.apptoken.store.v1.AppTokenProject.create_time:type_name -> controller.storage.timestamp.v1.Timestamp
1, // 1: controller.storage.apptoken.store.v1.AppTokenProject.update_time:type_name -> controller.storage.timestamp.v1.Timestamp
1, // 2: controller.storage.apptoken.store.v1.AppTokenProject.approximate_last_access_time:type_name -> controller.storage.timestamp.v1.Timestamp
1, // 3: controller.storage.apptoken.store.v1.AppTokenProject.expiration_time:type_name -> controller.storage.timestamp.v1.Timestamp
4, // [4:4] is the sub-list for method output_type
4, // [4:4] is the sub-list for method input_type
4, // [4:4] is the sub-list for extension type_name
4, // [4:4] is the sub-list for extension extendee
0, // [0:4] is the sub-list for field type_name
}
func init() { file_controller_storage_apptoken_store_v1_apptoken_project_proto_init() }
func file_controller_storage_apptoken_store_v1_apptoken_project_proto_init() {
if File_controller_storage_apptoken_store_v1_apptoken_project_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_controller_storage_apptoken_store_v1_apptoken_project_proto_rawDesc), len(file_controller_storage_apptoken_store_v1_apptoken_project_proto_rawDesc)),
NumEnums: 0,
NumMessages: 1,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_controller_storage_apptoken_store_v1_apptoken_project_proto_goTypes,
DependencyIndexes: file_controller_storage_apptoken_store_v1_apptoken_project_proto_depIdxs,
MessageInfos: file_controller_storage_apptoken_store_v1_apptoken_project_proto_msgTypes,
}.Build()
File_controller_storage_apptoken_store_v1_apptoken_project_proto = out.File
file_controller_storage_apptoken_store_v1_apptoken_project_proto_goTypes = nil
file_controller_storage_apptoken_store_v1_apptoken_project_proto_depIdxs = nil
}

@ -0,0 +1,164 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.10
// protoc (unknown)
// source: controller/storage/apptoken/store/v1/apptoken_project_permission.proto
package store
import (
_ "github.com/hashicorp/boundary/sdk/pbs/controller/protooptions"
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
// AppTokenPermissionProject represents app token permissions created in project scope
type AppTokenPermissionProject struct {
state protoimpl.MessageState `protogen:"open.v1"`
// individual id for the app token's permission
// @inject_tag: gorm:"primary_key"
PrivateId string `protobuf:"bytes,1,opt,name=private_id,json=privateId,proto3" json:"private_id,omitempty" gorm:"primary_key"`
// associated app token id
AppTokenId string `protobuf:"bytes,2,opt,name=app_token_id,json=appTokenId,proto3" json:"app_token_id,omitempty"`
// description of the app token permission
// @inject_tag: `gorm:"default:null"`
Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty" gorm:"default:null"`
// control if this app token permission has been granted "this" scope
// @inject_tag: `gorm:"default:false"`
GrantThisScope bool `protobuf:"varint,5,opt,name=grant_this_scope,json=grantThisScope,proto3" json:"grant_this_scope,omitempty" gorm:"default:false"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *AppTokenPermissionProject) Reset() {
*x = AppTokenPermissionProject{}
mi := &file_controller_storage_apptoken_store_v1_apptoken_project_permission_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *AppTokenPermissionProject) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AppTokenPermissionProject) ProtoMessage() {}
func (x *AppTokenPermissionProject) ProtoReflect() protoreflect.Message {
mi := &file_controller_storage_apptoken_store_v1_apptoken_project_permission_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use AppTokenPermissionProject.ProtoReflect.Descriptor instead.
func (*AppTokenPermissionProject) Descriptor() ([]byte, []int) {
return file_controller_storage_apptoken_store_v1_apptoken_project_permission_proto_rawDescGZIP(), []int{0}
}
func (x *AppTokenPermissionProject) GetPrivateId() string {
if x != nil {
return x.PrivateId
}
return ""
}
func (x *AppTokenPermissionProject) GetAppTokenId() string {
if x != nil {
return x.AppTokenId
}
return ""
}
func (x *AppTokenPermissionProject) GetDescription() string {
if x != nil {
return x.Description
}
return ""
}
func (x *AppTokenPermissionProject) GetGrantThisScope() bool {
if x != nil {
return x.GrantThisScope
}
return false
}
var File_controller_storage_apptoken_store_v1_apptoken_project_permission_proto protoreflect.FileDescriptor
const file_controller_storage_apptoken_store_v1_apptoken_project_permission_proto_rawDesc = "" +
"\n" +
"Fcontroller/storage/apptoken/store/v1/apptoken_project_permission.proto\x12$controller.storage.apptoken.store.v1\x1a*controller/custom_options/v1/options.proto\"\xc8\x01\n" +
"\x19AppTokenPermissionProject\x12\x1d\n" +
"\n" +
"private_id\x18\x01 \x01(\tR\tprivateId\x12 \n" +
"\fapp_token_id\x18\x02 \x01(\tR\n" +
"appTokenId\x12@\n" +
"\vdescription\x18\x03 \x01(\tB\x1e\xc2\xdd)\x1a\n" +
"\vdescription\x12\vdescriptionR\vdescription\x12(\n" +
"\x10grant_this_scope\x18\x05 \x01(\bR\x0egrantThisScopeB=Z;github.com/hashicorp/boundary/internal/apptoken/store;storeb\x06proto3"
var (
file_controller_storage_apptoken_store_v1_apptoken_project_permission_proto_rawDescOnce sync.Once
file_controller_storage_apptoken_store_v1_apptoken_project_permission_proto_rawDescData []byte
)
func file_controller_storage_apptoken_store_v1_apptoken_project_permission_proto_rawDescGZIP() []byte {
file_controller_storage_apptoken_store_v1_apptoken_project_permission_proto_rawDescOnce.Do(func() {
file_controller_storage_apptoken_store_v1_apptoken_project_permission_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_controller_storage_apptoken_store_v1_apptoken_project_permission_proto_rawDesc), len(file_controller_storage_apptoken_store_v1_apptoken_project_permission_proto_rawDesc)))
})
return file_controller_storage_apptoken_store_v1_apptoken_project_permission_proto_rawDescData
}
var file_controller_storage_apptoken_store_v1_apptoken_project_permission_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
var file_controller_storage_apptoken_store_v1_apptoken_project_permission_proto_goTypes = []any{
(*AppTokenPermissionProject)(nil), // 0: controller.storage.apptoken.store.v1.AppTokenPermissionProject
}
var file_controller_storage_apptoken_store_v1_apptoken_project_permission_proto_depIdxs = []int32{
0, // [0:0] is the sub-list for method output_type
0, // [0:0] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_controller_storage_apptoken_store_v1_apptoken_project_permission_proto_init() }
func file_controller_storage_apptoken_store_v1_apptoken_project_permission_proto_init() {
if File_controller_storage_apptoken_store_v1_apptoken_project_permission_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_controller_storage_apptoken_store_v1_apptoken_project_permission_proto_rawDesc), len(file_controller_storage_apptoken_store_v1_apptoken_project_permission_proto_rawDesc)),
NumEnums: 0,
NumMessages: 1,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_controller_storage_apptoken_store_v1_apptoken_project_permission_proto_goTypes,
DependencyIndexes: file_controller_storage_apptoken_store_v1_apptoken_project_permission_proto_depIdxs,
MessageInfos: file_controller_storage_apptoken_store_v1_apptoken_project_permission_proto_msgTypes,
}.Build()
File_controller_storage_apptoken_store_v1_apptoken_project_permission_proto = out.File
file_controller_storage_apptoken_store_v1_apptoken_project_permission_proto_goTypes = nil
file_controller_storage_apptoken_store_v1_apptoken_project_permission_proto_depIdxs = nil
}

@ -4,7 +4,6 @@
package apptoken
import (
"context"
"crypto/rand"
"fmt"
"slices"
@ -204,7 +203,7 @@ func testCheckPermission(t *testing.T, repo *Repository, appTokenId string, scop
where atp.app_token_id = $1
order by atp.private_id, atpg.canonical_grant, atpgios.scope_id, atpgips.scope_id
`
rows, err := repo.reader.Query(context.Background(), permQuery, []any{appTokenId})
rows, err := repo.reader.Query(t.Context(), permQuery, []any{appTokenId})
if err != nil {
return err
}
@ -271,7 +270,7 @@ func testCheckPermission(t *testing.T, repo *Repository, appTokenId string, scop
where atp.app_token_id = $1
order by atp.private_id, atpg.canonical_grant, atpis.scope_id
`
rows, err := repo.reader.Query(context.Background(), permQuery, []any{appTokenId})
rows, err := repo.reader.Query(t.Context(), permQuery, []any{appTokenId})
if err != nil {
return err
}
@ -317,6 +316,56 @@ func testCheckPermission(t *testing.T, repo *Repository, appTokenId string, scop
perm.Scopes = append(perm.Scopes, *individualScopeId)
}
}
case strings.HasPrefix(scopeId, globals.ProjectPrefix):
permQuery = `
select atp.private_id as permission_id,
atpg.canonical_grant,
atpp.description,
atpp.grant_this_scope
from app_token_permission atp
left join app_token_permission_grant atpg on atp.private_id = atpg.permission_id
left join app_token_permission_project atpp on atp.private_id = atpp.private_id
where atp.app_token_id = $1
order by atp.private_id, atpg.canonical_grant
`
rows, err := repo.reader.Query(t.Context(), permQuery, []any{appTokenId})
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var permissionId, canonicalGrant string
// *string for columns that can be null from left joins
var description *string
var grantThisScope bool
if err := rows.Scan(
&permissionId,
&canonicalGrant,
&description,
&grantThisScope,
); err != nil {
return err
}
// Get or create the testPermission for this permission_id
perm, exists := permMap[permissionId]
if !exists {
perm = &testPermission{
Description: *description,
GrantThis: grantThisScope,
Grants: []string{},
}
permMap[permissionId] = perm
}
// Add grant if present and not already added
if !slices.Contains(perm.Grants, canonicalGrant) {
perm.Grants = append(perm.Grants, canonicalGrant)
}
}
default:
return fmt.Errorf("unsupported scope id prefix for permission check: %s", scopeId)
}
@ -349,7 +398,7 @@ func testCheckAppTokenCipher(t *testing.T, repo *Repository, appTokenId string)
from app_token_cipher
where app_token_id = $1
`
rows, err := repo.reader.Query(context.Background(), cipherQuery, []any{appTokenId})
rows, err := repo.reader.Query(t.Context(), cipherQuery, []any{appTokenId})
if err != nil {
return err
}

@ -0,0 +1,65 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
syntax = "proto3";
package controller.storage.apptoken.store.v1;
import "controller/custom_options/v1/options.proto";
import "controller/storage/timestamp/v1/timestamp.proto";
option go_package = "github.com/hashicorp/boundary/internal/apptoken/store;store";
// AppTokenProject represents app tokens created in project scope
message AppTokenProject {
// public_id is used to access the App Token via an API
// @inject_tag: gorm:"primary_key"
string public_id = 1;
// scope id for the app token
// @inject_tag: `gorm:"default:null"`
string scope_id = 2;
// name is the optional friendly name used to
// access the App Token via an API
// @inject_tag: `gorm:"default:null"`
string name = 3 [(custom_options.v1.mask_mapping) = {
this: "name"
that: "name"
}];
// description of the app token
// @inject_tag: `gorm:"default:null"`
string description = 4 [(custom_options.v1.mask_mapping) = {
this: "description"
that: "description"
}];
// control if this app token has been revoked
// @inject_tag: `gorm:"default:false"`
bool revoked = 5;
// create_time from the RDBMS
// @inject_tag: `gorm:"default:current_timestamp"`
timestamp.v1.Timestamp create_time = 6;
// update_time from the RDBMS
// @inject_tag: `gorm:"default:current_timestamp"`
timestamp.v1.Timestamp update_time = 7;
// created_by_user_id is the ID of the user that created this App Token
// @inject_tag: `gorm:"default:not_null"`
string created_by_user_id = 8;
// approximate_last_access_time from the RDBMS
// @inject_tag: `gorm:"default:current_timestamp"`
timestamp.v1.Timestamp approximate_last_access_time = 9;
// time_to_stale_seconds is the number of seconds of inactivity after which
// the token is considered stale
uint32 time_to_stale_seconds = 10;
// expiration_time from the RDBMS
// @inject_tag: `gorm:"default:current_timestamp + interval '3 years'"`
timestamp.v1.Timestamp expiration_time = 11;
}

@ -0,0 +1,31 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
syntax = "proto3";
package controller.storage.apptoken.store.v1;
import "controller/custom_options/v1/options.proto";
option go_package = "github.com/hashicorp/boundary/internal/apptoken/store;store";
// AppTokenPermissionProject represents app token permissions created in project scope
message AppTokenPermissionProject {
// individual id for the app token's permission
// @inject_tag: gorm:"primary_key"
string private_id = 1;
// associated app token id
string app_token_id = 2;
// description of the app token permission
// @inject_tag: `gorm:"default:null"`
string description = 3 [(custom_options.v1.mask_mapping) = {
this: "description"
that: "description"
}];
// control if this app token permission has been granted "this" scope
// @inject_tag: `gorm:"default:false"`
bool grant_this_scope = 5;
}
Loading…
Cancel
Save