package dbauthz_test

import (
	"testing"

	"github.com/google/uuid"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/stretchr/testify/require"

	"cdr.dev/slog"
	"github.com/coder/coder/v2/coderd/coderdtest"
	"github.com/coder/coder/v2/coderd/database"
	"github.com/coder/coder/v2/coderd/database/db2sdk"
	"github.com/coder/coder/v2/coderd/database/dbauthz"
	"github.com/coder/coder/v2/coderd/database/dbmem"
	"github.com/coder/coder/v2/coderd/rbac"
	"github.com/coder/coder/v2/coderd/rbac/policy"
	"github.com/coder/coder/v2/codersdk"
	"github.com/coder/coder/v2/testutil"
)

// TestInsertCustomRoles verifies creating custom roles cannot escalate permissions.
func TestInsertCustomRoles(t *testing.T) {
	t.Parallel()

	userID := uuid.New()
	subjectFromRoles := func(roles rbac.ExpandableRoles) rbac.Subject {
		return rbac.Subject{
			FriendlyName: "Test user",
			ID:           userID.String(),
			Roles:        roles,
			Groups:       nil,
			Scope:        rbac.ScopeAll,
		}
	}

	canAssignRole := rbac.Role{
		Identifier:  rbac.RoleIdentifier{Name: "can-assign"},
		DisplayName: "",
		Site: rbac.Permissions(map[string][]policy.Action{
			rbac.ResourceAssignRole.Type: {policy.ActionRead, policy.ActionCreate},
		}),
	}

	merge := func(u ...interface{}) rbac.Roles {
		all := make([]rbac.Role, 0)
		for _, v := range u {
			v := v
			switch t := v.(type) {
			case rbac.Role:
				all = append(all, t)
			case rbac.ExpandableRoles:
				all = append(all, must(t.Expand())...)
			case rbac.RoleIdentifier:
				all = append(all, must(rbac.RoleByName(t)))
			default:
				panic("unknown type")
			}
		}

		return all
	}

	orgID := uuid.NullUUID{
		UUID:  uuid.New(),
		Valid: true,
	}
	testCases := []struct {
		name string

		subject rbac.ExpandableRoles

		// Perms to create on new custom role
		organizationID uuid.NullUUID
		site           []codersdk.Permission
		org            []codersdk.Permission
		user           []codersdk.Permission
		errorContains  string
	}{
		{
			// No roles, so no assign role
			name:          "no-roles",
			subject:       rbac.RoleIdentifiers{},
			errorContains: "forbidden",
		},
		{
			// This works because the new role has 0 perms
			name:    "empty",
			subject: merge(canAssignRole),
		},
		{
			name:           "mixed-scopes",
			subject:        merge(canAssignRole, rbac.RoleOwner()),
			organizationID: orgID,
			site: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
				codersdk.ResourceWorkspace: {codersdk.ActionRead},
			}),
			org: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
				codersdk.ResourceWorkspace: {codersdk.ActionRead},
			}),
			errorContains: "organization roles specify site or user permissions",
		},
		{
			name:    "invalid-action",
			subject: merge(canAssignRole, rbac.RoleOwner()),
			site: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
				// Action does not go with resource
				codersdk.ResourceWorkspace: {codersdk.ActionViewInsights},
			}),
			errorContains: "invalid action",
		},
		{
			name:    "invalid-resource",
			subject: merge(canAssignRole, rbac.RoleOwner()),
			site: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
				"foobar": {codersdk.ActionViewInsights},
			}),
			errorContains: "invalid resource",
		},
		{
			// Not allowing these at this time.
			name:    "negative-permission",
			subject: merge(canAssignRole, rbac.RoleOwner()),
			site: []codersdk.Permission{
				{
					Negate:       true,
					ResourceType: codersdk.ResourceWorkspace,
					Action:       codersdk.ActionRead,
				},
			},
			errorContains: "no negative permissions",
		},
		{
			name:    "wildcard", // not allowed
			subject: merge(canAssignRole, rbac.RoleOwner()),
			site: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
				codersdk.ResourceWorkspace: {"*"},
			}),
			errorContains: "no wildcard symbols",
		},
		// escalation checks
		{
			name:    "read-workspace-escalation",
			subject: merge(canAssignRole),
			site: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
				codersdk.ResourceWorkspace: {codersdk.ActionRead},
			}),
			errorContains: "not allowed to grant this permission",
		},
		{
			name: "read-workspace-outside-org",
			organizationID: uuid.NullUUID{
				UUID:  uuid.New(),
				Valid: true,
			},
			subject: merge(canAssignRole, rbac.ScopedRoleOrgAdmin(orgID.UUID)),
			org: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
				codersdk.ResourceWorkspace: {codersdk.ActionRead},
			}),
			errorContains: "forbidden",
		},
		{
			name: "user-escalation",
			// These roles do not grant user perms
			subject: merge(canAssignRole, rbac.ScopedRoleOrgAdmin(orgID.UUID)),
			user: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
				codersdk.ResourceWorkspace: {codersdk.ActionRead},
			}),
			errorContains: "not allowed to grant this permission",
		},
		{
			name:    "template-admin-escalation",
			subject: merge(canAssignRole, rbac.RoleTemplateAdmin()),
			site: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
				codersdk.ResourceWorkspace:        {codersdk.ActionRead},   // ok!
				codersdk.ResourceDeploymentConfig: {codersdk.ActionUpdate}, // not ok!
			}),
			user: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
				codersdk.ResourceWorkspace: {codersdk.ActionRead}, // ok!
			}),
			errorContains: "deployment_config",
		},
		// ok!
		{
			name:    "read-workspace-template-admin",
			subject: merge(canAssignRole, rbac.RoleTemplateAdmin()),
			site: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
				codersdk.ResourceWorkspace: {codersdk.ActionRead},
			}),
		},
		{
			name:           "read-workspace-in-org",
			subject:        merge(canAssignRole, rbac.ScopedRoleOrgAdmin(orgID.UUID)),
			organizationID: orgID,
			org: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
				codersdk.ResourceWorkspace: {codersdk.ActionRead},
			}),
		},
		{
			name: "user-perms",
			// This is weird, but is ok
			subject: merge(canAssignRole, rbac.RoleMember()),
			user: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
				codersdk.ResourceWorkspace: {codersdk.ActionRead},
			}),
		},
		{
			name:    "site+user-perms",
			subject: merge(canAssignRole, rbac.RoleMember(), rbac.RoleTemplateAdmin()),
			site: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
				codersdk.ResourceWorkspace: {codersdk.ActionRead},
			}),
			user: codersdk.CreatePermissions(map[codersdk.RBACResource][]codersdk.RBACAction{
				codersdk.ResourceWorkspace: {codersdk.ActionRead},
			}),
		},
	}

	for _, tc := range testCases {
		tc := tc

		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			db := dbmem.New()
			rec := &coderdtest.RecordingAuthorizer{
				Wrapped: rbac.NewAuthorizer(prometheus.NewRegistry()),
			}
			az := dbauthz.New(db, rec, slog.Make(), coderdtest.AccessControlStorePointer())

			subject := subjectFromRoles(tc.subject)
			ctx := testutil.Context(t, testutil.WaitMedium)
			ctx = dbauthz.As(ctx, subject)

			_, err := az.InsertCustomRole(ctx, database.InsertCustomRoleParams{
				Name:            "test-role",
				DisplayName:     "",
				OrganizationID:  tc.organizationID,
				SitePermissions: db2sdk.List(tc.site, convertSDKPerm),
				OrgPermissions:  db2sdk.List(tc.org, convertSDKPerm),
				UserPermissions: db2sdk.List(tc.user, convertSDKPerm),
			})
			if tc.errorContains != "" {
				require.ErrorContains(t, err, tc.errorContains)
			} else {
				require.NoError(t, err)

				// Verify the role is fetched with the lookup filter.
				roles, err := az.CustomRoles(ctx, database.CustomRolesParams{
					LookupRoles: []database.NameOrganizationPair{
						{
							Name:           "test-role",
							OrganizationID: tc.organizationID.UUID,
						},
					},
					ExcludeOrgRoles: false,
					OrganizationID:  uuid.UUID{},
				})
				require.NoError(t, err)
				require.Len(t, roles, 1)
			}
		})
	}
}

func convertSDKPerm(perm codersdk.Permission) database.CustomRolePermission {
	return database.CustomRolePermission{
		Negate:       perm.Negate,
		ResourceType: string(perm.ResourceType),
		Action:       policy.Action(perm.Action),
	}
}
