// Copyright IBM Corp. 2020, 2026
// SPDX-License-Identifier: BUSL-1.1

package scopes_test

import (
	"context"
	"fmt"
	"maps"
	"slices"
	"testing"

	"github.com/hashicorp/boundary/globals"
	"github.com/hashicorp/boundary/internal/auth"
	"github.com/hashicorp/boundary/internal/auth/password"
	"github.com/hashicorp/boundary/internal/authtoken"
	controllerauth "github.com/hashicorp/boundary/internal/daemon/controller/auth"
	"github.com/hashicorp/boundary/internal/daemon/controller/handlers"
	"github.com/hashicorp/boundary/internal/daemon/controller/handlers/scopes"
	"github.com/hashicorp/boundary/internal/db"
	pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services"
	"github.com/hashicorp/boundary/internal/iam"
	"github.com/hashicorp/boundary/internal/kms"
	"github.com/hashicorp/boundary/internal/types/resource"
	"github.com/hashicorp/boundary/internal/types/scope"
	pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/scopes"
	"github.com/stretchr/testify/require"
)

func TestGrants_ListScopes(t *testing.T) {
	ctx := context.Background()
	conn, _ := db.TestSetup(t, "postgres")
	wrap := db.TestWrapper(t)
	iamRepo := iam.TestRepo(t, conn, wrap)
	kmsCache := kms.TestKms(t, conn, wrap)
	repoFn := func() (*iam.Repository, error) {
		return iamRepo, nil
	}
	s, err := scopes.NewServiceFn(ctx, repoFn, kmsCache, 1000)
	require.NoError(t, err)
	rw := db.New(conn)
	atRepo, err := authtoken.NewRepository(ctx, rw, rw, kmsCache)
	require.NoError(t, err)

	org1 := iam.TestOrg(t, iamRepo, iam.WithName("org1"), iam.WithDescription("test org 1"), iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true))
	org2 := iam.TestOrg(t, iamRepo, iam.WithName("org2"), iam.WithDescription("test org 2"), iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true))
	proj1 := iam.TestProject(t, iamRepo, org1.GetPublicId(), iam.WithName("proj1"), iam.WithDescription("test project 1"), iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true))
	proj2a := iam.TestProject(t, iamRepo, org2.GetPublicId(), iam.WithName("proj2a"), iam.WithDescription("test project 2a"), iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true))
	proj2b := iam.TestProject(t, iamRepo, org2.GetPublicId(), iam.WithName("proj2b"), iam.WithDescription("test project 2b"), iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true))

	testcases := []struct {
		name          string
		input         *pbs.ListScopesRequest
		userFunc      func() (*iam.User, auth.Account)
		wantOutfields map[string][]string
		wantErr       error
	}{
		{
			name:     "no grants returns no scopes",
			input:    &pbs.ListScopesRequest{ScopeId: globals.GlobalPrefix},
			userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{}),
			wantErr:  handlers.ForbiddenError(),
		},
		{
			name:  "global role grant this returns all org scopes",
			input: &pbs.ListScopesRequest{ScopeId: globals.GlobalPrefix},
			userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
				{
					RoleScopeId: globals.GlobalPrefix,
					Grants:      []string{"ids=*;type=scope;actions=list,no-op;output_fields=id"},
					GrantScopes: []string{globals.GrantScopeThis},
				},
			}),
			wantOutfields: map[string][]string{
				org1.PublicId: {globals.IdField},
				org2.PublicId: {globals.IdField},
			},
		},
		{
			name: "global role grant this & children returns org scopes",
			input: &pbs.ListScopesRequest{
				ScopeId: globals.GlobalPrefix,
			},
			userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
				{
					RoleScopeId: globals.GlobalPrefix,
					Grants:      []string{"ids=*;type=scope;actions=list,read;output_fields=id"},
					GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren},
				},
			}),
			wantOutfields: map[string][]string{
				org1.PublicId: {globals.IdField},
				org2.PublicId: {globals.IdField},
			},
		},
		{
			name: "global role grant this & children recursive returns org and project scopes",
			input: &pbs.ListScopesRequest{
				ScopeId:   globals.GlobalPrefix,
				Recursive: true,
			},
			userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
				{
					RoleScopeId: globals.GlobalPrefix,
					Grants:      []string{"ids=*;type=scope;actions=list,read;output_fields=id"},
					GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren},
				},
			}),
			wantOutfields: map[string][]string{
				org1.PublicId:   {globals.IdField},
				org2.PublicId:   {globals.IdField},
				proj1.PublicId:  {globals.IdField},
				proj2a.PublicId: {globals.IdField},
				proj2b.PublicId: {globals.IdField},
			},
		},
		{
			name: "global role grant this & descendants recursive returns org and project scopes",
			input: &pbs.ListScopesRequest{
				ScopeId:   globals.GlobalPrefix,
				Recursive: true,
			},
			userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
				{
					RoleScopeId: globals.GlobalPrefix,
					Grants:      []string{"ids=*;type=scope;actions=list,read;output_fields=id,name"},
					GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeDescendants},
				},
			}),
			wantOutfields: map[string][]string{
				org1.PublicId:   {globals.IdField, globals.NameField},
				org2.PublicId:   {globals.IdField, globals.NameField},
				proj1.PublicId:  {globals.IdField, globals.NameField},
				proj2a.PublicId: {globals.IdField, globals.NameField},
				proj2b.PublicId: {globals.IdField, globals.NameField},
			},
		},
	}

	for _, tc := range testcases {
		t.Run(tc.name, func(t *testing.T) {
			// Call function to create direct/indirect relationship
			user, account := tc.userFunc()

			// Create auth token for the user
			tok, err := atRepo.CreateAuthToken(ctx, user, account.GetPublicId())
			require.NoError(t, err)

			// Create auth context from the auth token
			fullGrantAuthCtx := controllerauth.TestAuthContextFromToken(t, conn, wrap, tok, iamRepo)

			got, finalErr := s.ListScopes(fullGrantAuthCtx, tc.input)
			if tc.wantErr != nil {
				require.ErrorIs(t, finalErr, tc.wantErr)
				return
			}
			require.NoError(t, finalErr)
			var gotIds []string

			for _, g := range got.Items {
				gotIds = append(gotIds, g.GetId())

				// check if the output fields are as expected
				if tc.wantOutfields[g.Id] != nil {
					handlers.TestAssertOutputFields(t, g, tc.wantOutfields[g.Id])
				}
			}
			wantIds := slices.Collect(maps.Keys(tc.wantOutfields))
			require.ElementsMatch(t, wantIds, gotIds)
		})
	}
}

// expectedOutput consolidates common output fields for the test cases
type expectedOutput struct {
	err          error
	outputFields []string
}

func TestGrants_GetScope(t *testing.T) {
	ctx := context.Background()
	conn, _ := db.TestSetup(t, "postgres")
	rw := db.New(conn)
	wrap := db.TestWrapper(t)
	iamRepo := iam.TestRepo(t, conn, wrap)
	kmsCache := kms.TestKms(t, conn, wrap)
	atRepo, err := authtoken.NewRepository(ctx, rw, rw, kmsCache)
	require.NoError(t, err)

	repoFn := func() (*iam.Repository, error) {
		return iamRepo, nil
	}
	s, err := scopes.NewServiceFn(ctx, repoFn, kmsCache, 1000)
	require.NoError(t, err)

	org1 := iam.TestOrg(t, iamRepo, iam.WithName("org1"), iam.WithDescription("test org 1"), iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true))
	org2 := iam.TestOrg(t, iamRepo, iam.WithName("org2"), iam.WithDescription("test org 2"), iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true))
	proj1 := iam.TestProject(t, iamRepo, org1.GetPublicId(), iam.WithName("proj1"), iam.WithDescription("test project 1"), iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true))
	proj2 := iam.TestProject(t, iamRepo, org2.GetPublicId(), iam.WithName("proj2"), iam.WithDescription("test project 2"), iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true))

	testcases := []struct {
		name     string
		userFunc func() (*iam.User, auth.Account)
		expected map[*pbs.GetScopeRequest]expectedOutput
	}{
		{
			name:     "no grants returns no scopes",
			userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{}),
			expected: map[*pbs.GetScopeRequest]expectedOutput{
				{Id: scope.Global.String()}: {err: handlers.ForbiddenError()},
				{Id: org1.PublicId}:         {err: handlers.ForbiddenError()},
				{Id: org2.PublicId}:         {err: handlers.ForbiddenError()},
				{Id: proj1.PublicId}:        {err: handlers.ForbiddenError()},
				{Id: proj2.PublicId}:        {err: handlers.ForbiddenError()},
			},
		},
		{
			name: "global role grant this returns scopes under the global scope",
			userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
				{
					RoleScopeId: globals.GlobalPrefix,
					Grants:      []string{"ids=*;type=scope;actions=read;output_fields=id"},
					GrantScopes: []string{globals.GrantScopeThis},
				},
			}),
			expected: map[*pbs.GetScopeRequest]expectedOutput{
				{Id: scope.Global.String()}: {outputFields: []string{globals.IdField}},
				{Id: org1.PublicId}:         {outputFields: []string{globals.IdField}},
				{Id: org2.PublicId}:         {outputFields: []string{globals.IdField}},
				{Id: proj1.PublicId}:        {err: handlers.ForbiddenError()},
				{Id: proj2.PublicId}:        {err: handlers.ForbiddenError()},
			},
		},
		{
			name: "global role grant this and children returns scopes under the global and org scopes",
			userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
				{
					RoleScopeId: globals.GlobalPrefix,
					Grants:      []string{"ids=*;type=scope;actions=read;output_fields=id,name"},
					GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren},
				},
			}),
			expected: map[*pbs.GetScopeRequest]expectedOutput{
				{Id: scope.Global.String()}: {outputFields: []string{globals.IdField, globals.NameField}},
				{Id: org1.PublicId}:         {outputFields: []string{globals.IdField, globals.NameField}},
				{Id: org2.PublicId}:         {outputFields: []string{globals.IdField, globals.NameField}},
				{Id: proj1.PublicId}:        {outputFields: []string{globals.IdField, globals.NameField}},
				{Id: proj2.PublicId}:        {outputFields: []string{globals.IdField, globals.NameField}},
			},
		},
		{
			name: "global role grant children returns scopes under org scopes",
			userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
				{
					RoleScopeId: globals.GlobalPrefix,
					Grants:      []string{"ids=*;type=scope;actions=read;output_fields=id,name,scope_id"},
					GrantScopes: []string{globals.GrantScopeChildren},
				},
			}),
			expected: map[*pbs.GetScopeRequest]expectedOutput{
				{Id: scope.Global.String()}: {err: handlers.ForbiddenError()},
				{Id: org1.PublicId}:         {err: handlers.ForbiddenError()},
				{Id: org2.PublicId}:         {err: handlers.ForbiddenError()},
				{Id: proj1.PublicId}:        {outputFields: []string{globals.IdField, globals.NameField, globals.ScopeIdField}},
				{Id: proj2.PublicId}:        {outputFields: []string{globals.IdField, globals.NameField, globals.ScopeIdField}},
			},
		},
		{
			name: "global role grant descendants returns scopes under org scopes",
			userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
				{
					RoleScopeId: globals.GlobalPrefix,
					Grants:      []string{"ids=*;type=scope;actions=read;output_fields=id,name,scope_id,description"},
					GrantScopes: []string{globals.GrantScopeDescendants},
				},
			}),
			expected: map[*pbs.GetScopeRequest]expectedOutput{
				{Id: scope.Global.String()}: {err: handlers.ForbiddenError()},
				{Id: org1.PublicId}:         {err: handlers.ForbiddenError()},
				{Id: org2.PublicId}:         {err: handlers.ForbiddenError()},
				{Id: proj1.PublicId}:        {outputFields: []string{globals.IdField, globals.NameField, globals.ScopeIdField, globals.DescriptionField}},
				{Id: proj2.PublicId}:        {outputFields: []string{globals.IdField, globals.NameField, globals.ScopeIdField, globals.DescriptionField}},
			},
		},
		{
			name: "org1 role grant this returns scopes under the org1 scope",
			userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
				{
					RoleScopeId: org1.PublicId,
					Grants:      []string{"ids=*;type=scope;actions=read;output_fields=id,name,scope_id,description,version"},
					GrantScopes: []string{globals.GrantScopeThis},
				},
			}),
			expected: map[*pbs.GetScopeRequest]expectedOutput{
				{Id: scope.Global.String()}: {err: handlers.ForbiddenError()},
				{Id: org1.PublicId}:         {err: handlers.ForbiddenError()},
				{Id: org2.PublicId}:         {err: handlers.ForbiddenError()},
				{Id: proj1.PublicId}:        {outputFields: []string{globals.IdField, globals.NameField, globals.ScopeIdField, globals.DescriptionField, globals.VersionField}},
				{Id: proj2.PublicId}:        {err: handlers.ForbiddenError()},
			},
		},
		{
			name: "org1 role grant children returns scopes under project scopes (nothing)",
			userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
				{
					RoleScopeId: org1.PublicId,
					Grants:      []string{"ids=*;type=scope;actions=read"},
					GrantScopes: []string{globals.GrantScopeChildren},
				},
			}),
			expected: map[*pbs.GetScopeRequest]expectedOutput{
				{Id: scope.Global.String()}: {err: handlers.ForbiddenError()},
				{Id: org1.PublicId}:         {err: handlers.ForbiddenError()},
				{Id: org2.PublicId}:         {err: handlers.ForbiddenError()},
				{Id: proj1.PublicId}:        {err: handlers.ForbiddenError()},
				{Id: proj2.PublicId}:        {err: handlers.ForbiddenError()},
			},
		},
		{
			name: "org1 role grant this and children returns scopes under the org1 scope",
			userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
				{
					RoleScopeId: org1.PublicId,
					Grants:      []string{"ids=*;type=scope;actions=read;output_fields=id,name,scope_id,description,version,type"},
					GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeChildren},
				},
			}),
			expected: map[*pbs.GetScopeRequest]expectedOutput{
				{Id: scope.Global.String()}: {err: handlers.ForbiddenError()},
				{Id: org1.PublicId}:         {err: handlers.ForbiddenError()},
				{Id: org2.PublicId}:         {err: handlers.ForbiddenError()},
				{Id: proj1.PublicId}:        {outputFields: []string{globals.IdField, globals.NameField, globals.ScopeIdField, globals.DescriptionField, globals.VersionField, globals.TypeField}},
				{Id: proj2.PublicId}:        {err: handlers.ForbiddenError()},
			},
		},
		{
			name: "proj1 role grant this returns scopes under proj1 scope (nothing)",
			userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
				{
					RoleScopeId: proj1.PublicId,
					Grants:      []string{"ids=*;type=scope;actions=read"},
					GrantScopes: []string{globals.GrantScopeThis},
				},
			}),
			expected: map[*pbs.GetScopeRequest]expectedOutput{
				{Id: scope.Global.String()}: {err: handlers.ForbiddenError()},
				{Id: org1.PublicId}:         {err: handlers.ForbiddenError()},
				{Id: org2.PublicId}:         {err: handlers.ForbiddenError()},
				{Id: proj1.PublicId}:        {err: handlers.ForbiddenError()},
				{Id: proj2.PublicId}:        {err: handlers.ForbiddenError()},
			},
		},
		{
			name: "global role grant this scope pinned to org1 scope returns org1 scope",
			userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
				{
					RoleScopeId: globals.GlobalPrefix,
					Grants:      []string{fmt.Sprintf("ids=%s;types=scope;actions=read;output_fields=id,name,scope_id,description,version,type,authorized_actions", org1.PublicId)},
					GrantScopes: []string{globals.GrantScopeThis},
				},
			}),
			expected: map[*pbs.GetScopeRequest]expectedOutput{
				{Id: scope.Global.String()}: {err: handlers.ForbiddenError()},
				{Id: org1.PublicId}:         {outputFields: []string{globals.IdField, globals.NameField, globals.ScopeIdField, globals.DescriptionField, globals.VersionField, globals.TypeField, globals.AuthorizedActionsField}},
				{Id: org2.PublicId}:         {err: handlers.ForbiddenError()},
				{Id: proj1.PublicId}:        {err: handlers.ForbiddenError()},
				{Id: proj2.PublicId}:        {err: handlers.ForbiddenError()},
			},
		},
		{
			name: "global role grant individual scopes (org1) returns org1 scopes",
			userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
				{
					RoleScopeId: globals.GlobalPrefix,
					Grants:      []string{"ids=*;type=scope;actions=read;output_fields=id,name,scope_id,description,version,type,authorized_actions,created_time,updated_time"},
					GrantScopes: []string{org1.PublicId},
				},
			}),
			expected: map[*pbs.GetScopeRequest]expectedOutput{
				{Id: scope.Global.String()}: {err: handlers.ForbiddenError()},
				{Id: org1.PublicId}:         {err: handlers.ForbiddenError()},
				{Id: org2.PublicId}:         {err: handlers.ForbiddenError()},
				{Id: proj1.PublicId}:        {outputFields: []string{globals.IdField, globals.NameField, globals.ScopeIdField, globals.DescriptionField, globals.VersionField, globals.TypeField, globals.AuthorizedActionsField, globals.CreatedTimeField, globals.UpdatedTimeField}},
				{Id: proj2.PublicId}:        {err: handlers.ForbiddenError()},
			},
		},
	}

	for _, tc := range testcases {
		t.Run(tc.name, func(t *testing.T) {
			user, account := tc.userFunc()
			tok, err := atRepo.CreateAuthToken(ctx, user, account.GetPublicId())
			require.NoError(t, err)
			fullGrantAuthCtx := controllerauth.TestAuthContextFromToken(t, conn, wrap, tok, iamRepo)
			for input, expectedOutput := range tc.expected {
				got, err := s.GetScope(fullGrantAuthCtx, input)
				// not found means expect error
				if expectedOutput.err != nil {
					require.ErrorIs(t, err, expectedOutput.err)
					continue
				}
				// check if the output fields are as expected
				if expectedOutput.outputFields != nil {
					handlers.TestAssertOutputFields(t, got.Item, expectedOutput.outputFields)
				}
				require.NoError(t, err)
			}
		})
	}
}

func TestGrants_CreateScopes(t *testing.T) {
	ctx := context.Background()
	conn, _ := db.TestSetup(t, "postgres")
	rw := db.New(conn)
	wrap := db.TestWrapper(t)
	iamRepo := iam.TestRepo(t, conn, wrap)
	kmsCache := kms.TestKms(t, conn, wrap)
	atRepo, err := authtoken.NewRepository(ctx, rw, rw, kmsCache)
	require.NoError(t, err)
	repoFn := func() (*iam.Repository, error) {
		return iamRepo, nil
	}
	s, err := scopes.NewServiceFn(ctx, repoFn, kmsCache, 1000)
	require.NoError(t, err)

	org1 := iam.TestOrg(t, iamRepo, iam.WithName("org1"), iam.WithDescription("test org 1"), iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true))
	org2 := iam.TestOrg(t, iamRepo, iam.WithName("org2"), iam.WithDescription("test org 2"), iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true))

	testcases := []struct {
		name              string
		userFunc          func() (*iam.User, auth.Account)
		canCreateInScopes map[*pbs.CreateScopeRequest]error
	}{
		{
			name:     "no grants returns no creatable scopes",
			userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{}),
			canCreateInScopes: map[*pbs.CreateScopeRequest]error{
				{Item: &pb.Scope{ScopeId: scope.Global.String()}}: handlers.ForbiddenError(),
				{Item: &pb.Scope{ScopeId: org1.PublicId}}:         handlers.ForbiddenError(),
				{Item: &pb.Scope{ScopeId: org2.PublicId}}:         handlers.ForbiddenError(),
			},
		},
		{
			name: "global role grant this can create scopes in the global scope",
			userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
				{
					RoleScopeId: globals.GlobalPrefix,
					Grants:      []string{"ids=*;type=scope;actions=create"},
					GrantScopes: []string{globals.GrantScopeThis},
				},
			}),
			canCreateInScopes: map[*pbs.CreateScopeRequest]error{
				{Item: &pb.Scope{ScopeId: scope.Global.String()}}: nil,
				{Item: &pb.Scope{ScopeId: org1.PublicId}}:         handlers.ForbiddenError(),
				{Item: &pb.Scope{ScopeId: org2.PublicId}}:         handlers.ForbiddenError(),
			},
		},
		{
			name: "global role grant children can create scopes in org scopes",
			userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
				{
					RoleScopeId: globals.GlobalPrefix,
					Grants:      []string{"ids=*;type=scope;actions=create"},
					GrantScopes: []string{globals.GrantScopeChildren},
				},
			}),
			canCreateInScopes: map[*pbs.CreateScopeRequest]error{
				{Item: &pb.Scope{ScopeId: scope.Global.String()}}: handlers.ForbiddenError(),
				{Item: &pb.Scope{ScopeId: org1.PublicId}}:         nil,
				{Item: &pb.Scope{ScopeId: org2.PublicId}}:         nil,
			},
		},
		{
			name: "global role grant descendants can create scopes in org scopes",
			userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
				{
					RoleScopeId: globals.GlobalPrefix,
					Grants:      []string{"ids=*;type=scope;actions=create"},
					GrantScopes: []string{globals.GrantScopeDescendants},
				},
			}),
			canCreateInScopes: map[*pbs.CreateScopeRequest]error{
				{Item: &pb.Scope{ScopeId: scope.Global.String()}}: handlers.ForbiddenError(),
				{Item: &pb.Scope{ScopeId: org1.PublicId}}:         nil,
				{Item: &pb.Scope{ScopeId: org2.PublicId}}:         nil,
			},
		},
		{
			name: "global role individual grant to org1 can create scopes in org1 scope",
			userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
				{
					RoleScopeId: globals.GlobalPrefix,
					Grants:      []string{"ids=*;type=scope;actions=create"},
					GrantScopes: []string{org1.PublicId},
				},
			}),
			canCreateInScopes: map[*pbs.CreateScopeRequest]error{
				{Item: &pb.Scope{ScopeId: scope.Global.String()}}: handlers.ForbiddenError(),
				{Item: &pb.Scope{ScopeId: org1.PublicId}}:         nil,
				{Item: &pb.Scope{ScopeId: org2.PublicId}}:         handlers.ForbiddenError(),
			},
		},
	}

	for _, tc := range testcases {
		t.Run(tc.name, func(t *testing.T) {
			user, account := tc.userFunc()
			tok, err := atRepo.CreateAuthToken(ctx, user, account.GetPublicId())
			require.NoError(t, err)
			fullGrantAuthCtx := controllerauth.TestAuthContextFromToken(t, conn, wrap, tok, iamRepo)

			for req, wantErr := range tc.canCreateInScopes {
				_, err := s.CreateScope(fullGrantAuthCtx, req)
				if wantErr != nil {
					require.ErrorIs(t, err, wantErr)
					continue
				}
				require.NoError(t, err)
			}
		})
	}
}

func TestGrants_ListKeyVersionDestructionJobs(t *testing.T) {
	ctx := context.Background()
	conn, _ := db.TestSetup(t, "postgres")
	rw := db.New(conn)
	wrap := db.TestWrapper(t)
	iamRepo := iam.TestRepo(t, conn, wrap)
	kmsCache := kms.TestKms(t, conn, wrap)
	atRepo, err := authtoken.NewRepository(ctx, rw, rw, kmsCache)
	require.NoError(t, err)

	repoFn := func() (*iam.Repository, error) {
		return iamRepo, nil
	}
	s, err := scopes.NewServiceFn(ctx, repoFn, kmsCache, 1000)
	require.NoError(t, err)

	org1 := iam.TestOrg(t, iamRepo, iam.WithName("org1"), iam.WithDescription("test org 1"), iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true))
	org2 := iam.TestOrg(t, iamRepo, iam.WithName("org2"), iam.WithDescription("test org 2"), iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true))
	proj1 := iam.TestProject(t, iamRepo, org1.GetPublicId(), iam.WithName("proj1"), iam.WithDescription("test project 1"), iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true))
	proj2 := iam.TestProject(t, iamRepo, org2.GetPublicId(), iam.WithName("proj2"), iam.WithDescription("test project 2"), iam.WithSkipAdminRoleCreation(true), iam.WithSkipDefaultRoleCreation(true))

	testcases := []struct {
		name            string
		userFunc        func() (*iam.User, auth.Account)
		canListInScopes map[string]error
	}{
		{
			name:     "no grants cannot list key version destruction jobs",
			userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{}),
			canListInScopes: map[string]error{
				scope.Global.String(): handlers.ForbiddenError(),
				org1.PublicId:         handlers.ForbiddenError(),
				org2.PublicId:         handlers.ForbiddenError(),
				proj1.PublicId:        handlers.ForbiddenError(),
				proj2.PublicId:        handlers.ForbiddenError(),
			},
		},
		{
			name: "global role grant this can list key version destruction jobs in the global scope",
			userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix,
				password.TestAuthMethodWithAccount,
				[]iam.TestRoleGrantsRequest{
					{
						RoleScopeId: globals.GlobalPrefix,
						Grants:      []string{"ids=*;type=scope;actions=list-key-version-destruction-jobs"},
						GrantScopes: []string{globals.GrantScopeThis},
					},
				}),
			canListInScopes: map[string]error{
				scope.Global.String(): nil,
				org1.PublicId:         handlers.ForbiddenError(),
				org2.PublicId:         handlers.ForbiddenError(),
				proj1.PublicId:        handlers.ForbiddenError(),
				proj2.PublicId:        handlers.ForbiddenError(),
			},
		},
		{
			name: "global role grant children can list key version destruction jobs in org scopes",
			userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix,
				password.TestAuthMethodWithAccount,
				[]iam.TestRoleGrantsRequest{
					{
						RoleScopeId: globals.GlobalPrefix,
						Grants:      []string{"ids=*;type=scope;actions=list-key-version-destruction-jobs"},
						GrantScopes: []string{globals.GrantScopeChildren},
					},
				}),
			canListInScopes: map[string]error{
				scope.Global.String(): handlers.ForbiddenError(),
				org1.PublicId:         nil,
				org2.PublicId:         nil,
				proj1.PublicId:        handlers.ForbiddenError(),
				proj2.PublicId:        handlers.ForbiddenError(),
			},
		},
		{
			name: "org1 role grant this can list key version destruction jobs in the org1 scope",
			userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix,
				password.TestAuthMethodWithAccount,
				[]iam.TestRoleGrantsRequest{
					{
						RoleScopeId: org1.PublicId,
						Grants:      []string{"ids=*;type=scope;actions=list-key-version-destruction-jobs"},
						GrantScopes: []string{globals.GrantScopeThis},
					},
				}),
			canListInScopes: map[string]error{
				scope.Global.String(): handlers.ForbiddenError(),
				org1.PublicId:         nil,
				org2.PublicId:         handlers.ForbiddenError(),
				proj1.PublicId:        handlers.ForbiddenError(),
				proj2.PublicId:        handlers.ForbiddenError(),
			},
		},
		{
			name: "org1 role grant children can list key version destruction jobs in its project scopes",
			userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix,
				password.TestAuthMethodWithAccount,
				[]iam.TestRoleGrantsRequest{
					{
						RoleScopeId: org1.PublicId,
						Grants:      []string{"ids=*;type=scope;actions=list-key-version-destruction-jobs"},
						GrantScopes: []string{globals.GrantScopeChildren},
					},
				}),
			canListInScopes: map[string]error{
				scope.Global.String(): handlers.ForbiddenError(),
				org1.PublicId:         handlers.ForbiddenError(),
				org2.PublicId:         handlers.ForbiddenError(),
				proj1.PublicId:        nil,
				proj2.PublicId:        handlers.ForbiddenError(),
			},
		},
		{
			name: "global role grant descendants can list key version destruction jobs in its project scopes",
			userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix,
				password.TestAuthMethodWithAccount,
				[]iam.TestRoleGrantsRequest{
					{
						RoleScopeId: globals.GlobalPrefix,
						Grants:      []string{"ids=*;type=scope;actions=list-key-version-destruction-jobs"},
						GrantScopes: []string{globals.GrantScopeDescendants},
					},
				}),
			canListInScopes: map[string]error{
				scope.Global.String(): handlers.ForbiddenError(),
				org1.PublicId:         nil,
				org2.PublicId:         nil,
				proj1.PublicId:        nil,
				proj2.PublicId:        nil,
			},
		},
		{
			name: "global role grant org1 scope can list key version destruction jobs in the org1 scope",
			userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix,
				password.TestAuthMethodWithAccount,
				[]iam.TestRoleGrantsRequest{
					{
						RoleScopeId: globals.GlobalPrefix,
						Grants:      []string{"ids=*;type=scope;actions=list-key-version-destruction-jobs"},
						GrantScopes: []string{org1.PublicId},
					},
				}),
			canListInScopes: map[string]error{
				scope.Global.String(): handlers.ForbiddenError(),
				org1.PublicId:         nil,
				org2.PublicId:         handlers.ForbiddenError(),
				proj1.PublicId:        handlers.ForbiddenError(),
				proj2.PublicId:        handlers.ForbiddenError(),
			},
		},
		{
			name: "global role grant to proj1 and proj2 role grant this can list key version destruction in granted projects",
			userFunc: iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix,
				password.TestAuthMethodWithAccount,
				[]iam.TestRoleGrantsRequest{
					{
						RoleScopeId: globals.GlobalPrefix,
						Grants:      []string{"ids=*;type=scope;actions=list-key-version-destruction-jobs"},
						GrantScopes: []string{proj1.PublicId},
					},
					{
						RoleScopeId: proj2.PublicId,
						Grants:      []string{"ids=*;type=scope;actions=list-key-version-destruction-jobs"},
						GrantScopes: []string{globals.GrantScopeThis},
					},
				}),
			canListInScopes: map[string]error{
				scope.Global.String(): handlers.ForbiddenError(),
				org1.PublicId:         handlers.ForbiddenError(),
				org2.PublicId:         handlers.ForbiddenError(),
				proj1.PublicId:        nil,
				proj2.PublicId:        nil,
			},
		},
	}

	for _, tc := range testcases {
		t.Run(tc.name, func(t *testing.T) {
			user, account := tc.userFunc()
			tok, err := atRepo.CreateAuthToken(ctx, user, account.GetPublicId())
			require.NoError(t, err)
			fullGrantAuthCtx := controllerauth.TestAuthContextFromToken(t, conn, wrap, tok, iamRepo)

			for scopeId, wantErr := range tc.canListInScopes {
				_, err = s.ListKeyVersionDestructionJobs(fullGrantAuthCtx, &pbs.ListKeyVersionDestructionJobsRequest{
					ScopeId: scopeId,
				})
				if wantErr != nil {
					require.ErrorIs(t, err, wantErr)
					continue
				}
				require.NoError(t, err)
			}
		})
	}
}

func TestGrants_List_AuthorizedAction(t *testing.T) {
	ctx := context.Background()
	conn, _ := db.TestSetup(t, "postgres")
	wrap := db.TestWrapper(t)
	rw := db.New(conn)
	iamRepo := iam.TestRepo(t, conn, wrap)
	kmsCache := kms.TestKms(t, conn, wrap)
	_, _ = iam.TestScopes(t, iamRepo)
	_, _ = iam.TestScopes(t, iamRepo)
	repoFn := func() (*iam.Repository, error) {
		return iamRepo, nil
	}
	s, err := scopes.NewServiceFn(ctx, repoFn, kmsCache, 1000)
	require.NoError(t, err)
	atRepo, err := authtoken.NewRepository(ctx, rw, rw, kmsCache)
	require.NoError(t, err)
	user, account := iam.TestUserDirectGrantsFunc(t, conn, kmsCache, globals.GlobalPrefix, password.TestAuthMethodWithAccount, []iam.TestRoleGrantsRequest{
		{
			RoleScopeId: globals.GlobalPrefix,
			Grants:      []string{"ids=*;type=*;actions=*"},
			GrantScopes: []string{globals.GrantScopeThis, globals.GrantScopeDescendants},
		},
	})()
	tok, err := atRepo.CreateAuthToken(ctx, user, account.GetPublicId())
	require.NoError(t, err)
	fullGrantAuthCtx := controllerauth.TestAuthContextFromToken(t, conn, wrap, tok, iamRepo)
	got, err := s.ListScopes(fullGrantAuthCtx, &pbs.ListScopesRequest{ScopeId: globals.GlobalPrefix, Recursive: true})
	require.NoError(t, err)
	for _, item := range got.Items {
		switch item.GetType() {
		case "global":
			require.ElementsMatch(t, item.AuthorizedActions, []string{"no-op", "read", "update", "delete", "attach-storage-policy", "detach-storage-policy"})
			require.Len(t, item.AuthorizedCollectionActions, 11)
			require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.Alias.PluralString()].AsSlice(), []string{"create", "list"})
			require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.AuthMethod.PluralString()].AsSlice(), []string{"create", "list"})
			require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.StorageBucket.PluralString()].AsSlice(), []string{"create", "list"})
			require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.AuthToken.PluralString()].AsSlice(), []string{"list"})
			require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.Group.PluralString()].AsSlice(), []string{"create", "list"})
			require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.Role.PluralString()].AsSlice(), []string{"create", "list"})
			require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.Scope.PluralString()].AsSlice(), []string{"create", "list", "list-keys", "rotate-keys", "list-key-version-destruction-jobs", "destroy-key-version"})
			require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.User.PluralString()].AsSlice(), []string{"create", "list"})
			require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.Worker.PluralString()].AsSlice(), []string{"read-certificate-authority", "create-controller-led", "create-worker-led", "reinitialize-certificate-authority", "list"})
			require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.SessionRecording.PluralString()].AsSlice(), []string{"list"})
			require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.Policy.PluralString()].AsSlice(), []string{"create", "list"})
		case "org":
			require.ElementsMatch(t, item.AuthorizedActions, []string{"no-op", "read", "update", "delete", "attach-storage-policy", "detach-storage-policy"})
			require.Len(t, item.AuthorizedCollectionActions, 9)
			require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.AuthMethod.PluralString()].AsSlice(), []string{"create", "list"})
			require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.StorageBucket.PluralString()].AsSlice(), []string{"create", "list"})
			require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.AuthToken.PluralString()].AsSlice(), []string{"list"})
			require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.Group.PluralString()].AsSlice(), []string{"create", "list"})
			require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.Role.PluralString()].AsSlice(), []string{"create", "list"})
			require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.Scope.PluralString()].AsSlice(), []string{"create", "list", "list-keys", "rotate-keys", "list-key-version-destruction-jobs", "destroy-key-version"})
			require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.User.PluralString()].AsSlice(), []string{"create", "list"})
			require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.SessionRecording.PluralString()].AsSlice(), []string{"list"})
			require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.Policy.PluralString()].AsSlice(), []string{"create", "list"})
		case "project":
			require.ElementsMatch(t, item.AuthorizedActions, []string{"no-op", "read", "update", "delete"})
			require.Len(t, item.AuthorizedCollectionActions, 7)
			require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.CredentialStore.PluralString()].AsSlice(), []string{"create", "list"})
			require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.Group.PluralString()].AsSlice(), []string{"create", "list"})
			require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.HostCatalog.PluralString()].AsSlice(), []string{"create", "list"})
			require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.Role.PluralString()].AsSlice(), []string{"create", "list"})
			require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.Scope.PluralString()].AsSlice(), []string{"list-keys", "rotate-keys", "list-key-version-destruction-jobs", "destroy-key-version"})
			require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.Session.PluralString()].AsSlice(), []string{"list"})
			require.ElementsMatch(t, item.AuthorizedCollectionActions[resource.Target.PluralString()].AsSlice(), []string{"create", "list"})
		default:
			t.Fatalf("unknown item type: %s", item.GetType())
		}
	}
}
