diff --git a/Makefile b/Makefile index 49d4f666e0..f6467619ae 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/internal/apptoken/apptoken.go b/internal/apptoken/apptoken.go index a1cbfd018c..87ad450e15 100644 --- a/internal/apptoken/apptoken.go +++ b/internal/apptoken/apptoken.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 { diff --git a/internal/apptoken/repository.go b/internal/apptoken/repository.go index 41e2e00305..7c80e8f934 100644 --- a/internal/apptoken/repository.go +++ b/internal/apptoken/repository.go @@ -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 diff --git a/internal/apptoken/repository_test.go b/internal/apptoken/repository_test.go index f2cec6ec30..0a6673144f 100644 --- a/internal/apptoken/repository_test.go +++ b/internal/apptoken/repository_test.go @@ -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) } diff --git a/internal/apptoken/store/apptoken_project.pb.go b/internal/apptoken/store/apptoken_project.pb.go new file mode 100644 index 0000000000..c34bd9eee5 --- /dev/null +++ b/internal/apptoken/store/apptoken_project.pb.go @@ -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 +} diff --git a/internal/apptoken/store/apptoken_project_permission.pb.go b/internal/apptoken/store/apptoken_project_permission.pb.go new file mode 100644 index 0000000000..0af4c5e3df --- /dev/null +++ b/internal/apptoken/store/apptoken_project_permission.pb.go @@ -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 +} diff --git a/internal/apptoken/testing.go b/internal/apptoken/testing.go index 2d141c9bdf..1b6e21bd31 100644 --- a/internal/apptoken/testing.go +++ b/internal/apptoken/testing.go @@ -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 } diff --git a/internal/proto/controller/storage/apptoken/store/v1/apptoken_project.proto b/internal/proto/controller/storage/apptoken/store/v1/apptoken_project.proto new file mode 100644 index 0000000000..79d5b0330e --- /dev/null +++ b/internal/proto/controller/storage/apptoken/store/v1/apptoken_project.proto @@ -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; +} diff --git a/internal/proto/controller/storage/apptoken/store/v1/apptoken_project_permission.proto b/internal/proto/controller/storage/apptoken/store/v1/apptoken_project_permission.proto new file mode 100644 index 0000000000..82b2d3bea3 --- /dev/null +++ b/internal/proto/controller/storage/apptoken/store/v1/apptoken_project_permission.proto @@ -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; +}