Files
Misi 0c264b7a5f IAM: Add user search endpoint (#114542)
* wip: initial changes, api registration

* wip

* LegacySearch working with sorting

* Revert mapper change for now

* Clean up

* Cleanup, add integration tests

* Improve tests

* OpenAPI def regen

* Use wildcard search, fix lastSeenAt handling, add lastSeenAtAge

* Add missing files

* Fix merge

* Fixes

* Add tests, regen openapi def

* Address feedback

* Address feedback batch 2

* Chores

* regen openapidef

* Address feedback

* Add tests for paging

* gen apis

* Revert go.mod, go.sum. go.work.sum

* Fix + remove extra tracer parameter
2025-12-11 19:54:48 +01:00

109 lines
3.0 KiB
Go

package builders
import (
"bytes"
"context"
"encoding/json"
iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
)
const (
USER_EMAIL = "email"
USER_LOGIN = "login"
USER_LAST_SEEN_AT = "lastSeenAt"
USER_ROLE = "role"
)
// UserSortableExtraFields are the additional fields that can be used for sorting user search results.
// Should not include standard fields like title.
var UserSortableExtraFields = []string{
USER_EMAIL,
USER_LOGIN,
USER_LAST_SEEN_AT,
}
var UserTableColumnDefinitions = map[string]*resourcepb.ResourceTableColumnDefinition{
USER_EMAIL: {
Name: USER_EMAIL,
Type: resourcepb.ResourceTableColumnDefinition_STRING,
Description: "The email address of the user",
Properties: &resourcepb.ResourceTableColumnDefinition_Properties{
UniqueValues: true,
Filterable: true,
},
},
USER_LOGIN: {
Name: USER_LOGIN,
Type: resourcepb.ResourceTableColumnDefinition_STRING,
Description: "The login of the user",
Properties: &resourcepb.ResourceTableColumnDefinition_Properties{
UniqueValues: true,
Filterable: true,
},
},
USER_LAST_SEEN_AT: {
Name: USER_LAST_SEEN_AT,
Type: resourcepb.ResourceTableColumnDefinition_INT64,
Description: "The last seen timestamp of the user",
Properties: &resourcepb.ResourceTableColumnDefinition_Properties{
Filterable: true,
},
},
USER_ROLE: {
Name: USER_ROLE,
Type: resourcepb.ResourceTableColumnDefinition_STRING,
Description: "The role of the user",
Properties: &resourcepb.ResourceTableColumnDefinition_Properties{
Filterable: true,
},
},
}
func GetUserBuilder() (resource.DocumentBuilderInfo, error) {
values := make([]*resourcepb.ResourceTableColumnDefinition, 0, len(UserTableColumnDefinitions))
for _, v := range UserTableColumnDefinitions {
values = append(values, v)
}
fields, err := resource.NewSearchableDocumentFields(values)
return resource.DocumentBuilderInfo{
GroupResource: iamv0.UserResourceInfo.GroupResource(),
Fields: fields,
Builder: new(userDocumentBuilder),
}, err
}
var _ resource.DocumentBuilder = new(userDocumentBuilder)
type userDocumentBuilder struct{}
func (u *userDocumentBuilder) BuildDocument(ctx context.Context, key *resourcepb.ResourceKey, rv int64, value []byte) (*resource.IndexableDocument, error) {
user := &iamv0.User{}
err := json.NewDecoder(bytes.NewReader(value)).Decode(user)
if err != nil {
return nil, err
}
obj, err := utils.MetaAccessor(user)
if err != nil {
return nil, err
}
doc := resource.NewIndexableDocument(key, rv, obj)
doc.Fields = make(map[string]any)
if user.Spec.Email != "" {
doc.Fields[USER_EMAIL] = user.Spec.Email
}
if user.Spec.Login != "" {
doc.Fields[USER_LOGIN] = user.Spec.Login
}
doc.Fields[USER_LAST_SEEN_AT] = user.Status.LastSeenAt
doc.Fields[USER_ROLE] = user.Spec.Role
return doc, nil
}