Files
grafana/pkg/tests/apis/iam/iam_test.go
Misi e0dbb966fc IAM: Add users filtering and improved RBAC mapper for users API (#119100)
* IAM: Add hidden users filtering and improved RBAC mapper for users API

- Add StoreWrapper for user resource that filters hidden users on Get/List
- Wire up StoreWrapper in the users API group registration
- Expand RBAC verb mapping for users to use explicit action translations
- Add integration tests for hidden users filtering behavior

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* IAM: Fix duplicate user validation and storewrapper context propagation

The storewrapper replaced the request context with a service identity
(OrgID=0) before invoking createValidation/updateValidation callbacks.
Since these callbacks wrap k8s admission webhooks (including the
duplicate email/login checks), the validation ran with OrgID=0 causing
SearchOrgUsers to return no results, silently passing duplicates through
to the DB which then returned a 500 instead of 409.

Fix 1 (storewrapper): Add validationWithUserContext and
updateValidationWithUserContext helpers that rebind validation callbacks
to the original user context before passing them to the inner store.

Fix 2 (legacy store): Add toUserConflictError as defense-in-depth that
converts SQLite UNIQUE constraint failures on user.email/user.login into
proper 409 Conflict API errors in CreateUser and UpdateUser.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Regen

* Use configprovider.ConfigProvider instead of setting.Cfg

* Enforce hidden-users restrictions on write operations

BeforeCreate, BeforeUpdate, and BeforeDelete in the user StoreWrapper
now return HTTP 403 when the target user's login is in the hidden-users
list, returning a generic "operation not permitted" message to callers
and logging the hidden-user detail server-side via a structured logger.

Integration tests are updated to create the user before marking it
hidden (so BeforeCreate does not block setup), then verify all four
guarded paths (get→404, list filtered, update→403, delete→403) and
add a dedicated sub-test that confirms create is blocked once a login
is in the hidden list.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* IAM: Add WithPreserveIdentity option to storewrapper

Introduces a WithPreserveIdentity() functional option on storewrapper.New()
so the users storage path passes the original caller identity through to the
inner store instead of replacing it with a service identity. This ensures
admission validation (e.g. duplicate email/login checks) runs with the correct
OrgID. Adds unit tests for the new option.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Address feedback

* Fix some minor issues

* Update pkg/registry/apis/iam/register.go

Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>

* Address feedback

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>
2026-03-12 13:42:22 +01:00

215 lines
5.3 KiB
Go

package identity
import (
"context"
"testing"
"github.com/stretchr/testify/require"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/tests/apis"
"github.com/grafana/grafana/pkg/tests/testinfra"
"github.com/grafana/grafana/pkg/tests/testsuite"
"github.com/grafana/grafana/pkg/util/testutil"
)
var gvrTeams = schema.GroupVersionResource{
Group: "iam.grafana.app",
Version: "v0alpha1",
Resource: "teams",
}
var gvrUsers = schema.GroupVersionResource{
Group: "iam.grafana.app",
Version: "v0alpha1",
Resource: "users",
}
var gvrTeamBindings = schema.GroupVersionResource{
Group: "iam.grafana.app",
Version: "v0alpha1",
Resource: "teambindings",
}
func TestMain(m *testing.M) {
testsuite.Run(m)
}
func TestIntegrationIdentity(t *testing.T) {
testutil.SkipIntegrationTestInShortMode(t)
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
AppModeProduction: false, // required for experimental APIs
DisableAnonymous: true,
RBACSingleOrganization: true,
EnableFeatureToggles: []string{
featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, // Required to start the example service
featuremgmt.FlagKubernetesUsersApi,
featuremgmt.FlagKubernetesServiceAccountsApi,
featuremgmt.FlagKubernetesServiceAccountTokensApi,
},
})
_, err := helper.NewDiscoveryClient().ServerResourcesForGroupVersion("iam.grafana.app/v0alpha1")
require.NoError(t, err)
t.Run("read only views", func(t *testing.T) {
ctx := context.Background()
teamClient := helper.GetResourceClient(apis.ResourceClientArgs{
User: helper.Org1.Admin,
GVR: gvrTeams,
})
rsp, err := teamClient.Resource.List(ctx, metav1.ListOptions{})
require.NoError(t, err)
found := teamClient.SanitizeJSONList(rsp, "name", "labels")
require.JSONEq(t, `{
"items": [
{
"apiVersion": "iam.grafana.app/v0alpha1",
"kind": "Team",
"metadata": {
"creationTimestamp": "${creationTimestamp}",
"name": "${name}",
"namespace": "default",
"resourceVersion": "${resourceVersion}"
},
"spec": {
"email": "staff@Org1",
"title": "staff",
"provisioned": false,
"externalUID": ""
}
}
]
}`, found)
// Org1 users
userClient := helper.GetResourceClient(apis.ResourceClientArgs{
User: helper.Org1.Admin,
GVR: gvrUsers,
})
rsp, err = userClient.Resource.List(ctx, metav1.ListOptions{})
require.NoError(t, err)
// Get just the specs (avoids values that change with each deployment)
found = teamClient.SpecJSON(rsp)
require.JSONEq(t, `[
{
"disabled": false,
"email": "admin@localhost",
"emailVerified": false,
"grafanaAdmin": true,
"login": "admin",
"title": "",
"provisioned": false,
"role": "Admin"
},
{
"disabled": false,
"email": "grafana-admin",
"emailVerified": false,
"grafanaAdmin": true,
"login": "grafana-admin",
"title": "admin2",
"provisioned": false,
"role": "Admin"
},
{
"disabled": false,
"email": "editor",
"emailVerified": false,
"grafanaAdmin": false,
"login": "editor",
"title": "editor",
"provisioned": false,
"role": "Editor"
},
{
"disabled": false,
"email": "viewer",
"emailVerified": false,
"grafanaAdmin": false,
"login": "viewer",
"title": "viewer",
"provisioned": false,
"role": "Viewer"
},
{
"disabled": false,
"email": "none",
"emailVerified": false,
"grafanaAdmin": false,
"login": "none",
"title": "none",
"provisioned": false,
"role": "None"
}
]`, found)
// OrgB users
userClient = helper.GetResourceClient(apis.ResourceClientArgs{
User: helper.Org1.Admin, // super admin
Namespace: helper.Namespacer(helper.OrgB.Admin.Identity.GetOrgID()), // list values for orgB with super admin user
GVR: gvrUsers,
})
rsp, err = userClient.Resource.List(ctx, metav1.ListOptions{})
require.NoError(t, err)
// Get just the specs (avoids values that change with each deployment)
found = teamClient.SpecJSON(rsp)
require.JSONEq(t, `[
{
"disabled": false,
"email": "grafana-admin",
"emailVerified": false,
"grafanaAdmin": true,
"login": "grafana-admin",
"title": "admin2",
"provisioned": false,
"role": "Admin"
},
{
"disabled": false,
"email": "admin2-org-2",
"emailVerified": false,
"grafanaAdmin": false,
"login": "admin2-org-2",
"title": "admin2",
"provisioned": false,
"role": "Admin"
},
{
"disabled": false,
"email": "editor-org-2",
"emailVerified": false,
"grafanaAdmin": false,
"login": "editor-org-2",
"title": "editor",
"provisioned": false,
"role": "Editor"
},
{
"disabled": false,
"email": "viewer-org-2",
"emailVerified": false,
"grafanaAdmin": false,
"login": "viewer-org-2",
"title": "viewer",
"provisioned": false,
"role": "Viewer"
},
{
"disabled": false,
"email": "none-org-2",
"emailVerified": false,
"grafanaAdmin": false,
"login": "none-org-2",
"title": "none",
"provisioned": false,
"role": "None"
}
] `, found)
})
}