diff --git a/pkg/api/admin_users.go b/pkg/api/admin_users.go index f937096daa9..5efd9a9446d 100644 --- a/pkg/api/admin_users.go +++ b/pkg/api/admin_users.go @@ -10,10 +10,10 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/auth" - "github.com/grafana/grafana/pkg/services/authn" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" @@ -366,7 +366,7 @@ func (hs *HTTPServer) AdminLogoutUser(c *contextmodel.ReqContext) response.Respo return response.Error(http.StatusBadRequest, "id is invalid", err) } - if c.SignedInUser.GetID() == authn.NewNamespaceID(authn.NamespaceUser, userID) { + if c.SignedInUser.GetID() == identity.NewTypedID(identity.TypeUser, userID) { return response.Error(http.StatusBadRequest, "You cannot logout yourself", nil) } diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index c78f704d7b4..bc1b44a7a87 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -16,6 +16,7 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/fs" "github.com/grafana/grafana/pkg/infra/tracing" @@ -189,7 +190,7 @@ func getContextHandler(t *testing.T, cfg *setting.Cfg) *contexthandler.ContextHa return contexthandler.ProvideService( cfg, tracing.InitializeTracerForTest(), - &authntest.FakeService{ExpectedIdentity: &authn.Identity{ID: authn.AnonymousNamespaceID, SessionToken: &usertoken.UserToken{}}}, + &authntest.FakeService{ExpectedIdentity: &authn.Identity{ID: identity.AnonymousTypedID, SessionToken: &usertoken.UserToken{}}}, ) } diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index f9f231db381..cb650138f1a 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -43,9 +43,9 @@ func (hs *HTTPServer) isDashboardStarredByUser(c *contextmodel.ReqContext, dashI return false, nil } - namespaceID, userIDstr := c.SignedInUser.GetNamespacedID() + namespaceID, userIDstr := c.SignedInUser.GetTypedID() - if namespaceID != identity.NamespaceUser { + if namespaceID != identity.TypeUser { return false, nil } @@ -436,7 +436,7 @@ func (hs *HTTPServer) deleteDashboard(c *contextmodel.ReqContext) response.Respo return response.Error(http.StatusBadRequest, "Use folders endpoint for deleting folders.", nil) } - namespaceID, userIDStr := c.SignedInUser.GetNamespacedID() + namespaceID, userIDStr := c.SignedInUser.GetTypedID() // disconnect all library elements for this dashboard err = hs.LibraryElementService.DisconnectElementsFromDashboard(c.Req.Context(), dash.ID) @@ -513,8 +513,8 @@ func (hs *HTTPServer) postDashboard(c *contextmodel.ReqContext, cmd dashboards.S var err error userID := int64(0) - namespaceID, userIDstr := c.SignedInUser.GetNamespacedID() - if namespaceID != identity.NamespaceUser && namespaceID != identity.NamespaceServiceAccount { + namespaceID, userIDstr := c.SignedInUser.GetTypedID() + if namespaceID != identity.TypeUser && namespaceID != identity.TypeServiceAccount { hs.log.Debug("User does not belong to a user or service account namespace", "namespaceID", namespaceID, "userID", userIDstr) } else { userID, err = identity.IntIdentifier(namespaceID, userIDstr) @@ -631,8 +631,8 @@ func (hs *HTTPServer) postDashboard(c *contextmodel.ReqContext, cmd dashboards.S func (hs *HTTPServer) GetHomeDashboard(c *contextmodel.ReqContext) response.Response { userID := int64(0) var err error - namespaceID, userIDstr := c.SignedInUser.GetNamespacedID() - if namespaceID != identity.NamespaceUser && namespaceID != identity.NamespaceServiceAccount { + namespaceID, userIDstr := c.SignedInUser.GetTypedID() + if namespaceID != identity.TypeUser && namespaceID != identity.TypeServiceAccount { hs.log.Debug("User does not belong to a user or service account namespace", "namespaceID", namespaceID, "userID", userIDstr) } else { userID, err = identity.IntIdentifier(namespaceID, userIDstr) @@ -1083,8 +1083,8 @@ func (hs *HTTPServer) RestoreDashboardVersion(c *contextmodel.ReqContext) respon } userID := int64(0) - namespaceID, userIDstr := c.SignedInUser.GetNamespacedID() - if namespaceID != identity.NamespaceUser && namespaceID != identity.NamespaceServiceAccount { + namespaceID, userIDstr := c.SignedInUser.GetTypedID() + if namespaceID != identity.TypeUser && namespaceID != identity.TypeServiceAccount { hs.log.Warn("User does not belong to a user or service account namespace", "namespaceID", namespaceID, "userID", userIDstr) } else { userID, err = identity.IntIdentifier(namespaceID, userIDstr) diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index 4c207742c74..bc58368ab76 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -442,7 +442,7 @@ func (hs *HTTPServer) AddDataSource(c *contextmodel.ReqContext) response.Respons return response.Error(http.StatusBadRequest, "bad request data", err) } - userID, err := identity.UserIdentifier(c.SignedInUser.GetNamespacedID()) + userID, err := identity.UserIdentifier(c.SignedInUser.GetTypedID()) if err != nil { return response.Error(http.StatusInternalServerError, "Failed to add datasource", err) diff --git a/pkg/api/folder.go b/pkg/api/folder.go index abe935889ce..13882e22594 100644 --- a/pkg/api/folder.go +++ b/pkg/api/folder.go @@ -194,8 +194,8 @@ func (hs *HTTPServer) setDefaultFolderPermissions(ctx context.Context, orgID int var permissions []accesscontrol.SetResourcePermissionCommand var userID int64 - namespace, id := user.GetNamespacedID() - if namespace == identity.NamespaceUser { + namespace, id := user.GetTypedID() + if namespace == identity.TypeUser { var errID error userID, errID = identity.IntIdentifier(namespace, id) if errID != nil { diff --git a/pkg/api/index.go b/pkg/api/index.go index 95a8c984ec0..09bef43890d 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -28,7 +28,7 @@ func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexV return nil, err } - userID, _ := identity.UserIdentifier(c.SignedInUser.GetNamespacedID()) + userID, _ := identity.UserIdentifier(c.SignedInUser.GetTypedID()) prefsQuery := pref.GetPreferenceWithDefaultsQuery{UserID: userID, OrgID: c.SignedInUser.GetOrgID(), Teams: c.Teams} prefs, err := hs.preferenceService.GetWithDefaults(c.Req.Context(), &prefsQuery) @@ -166,10 +166,10 @@ func (hs *HTTPServer) setIndexViewData(c *contextmodel.ReqContext) (*dtos.IndexV } func (hs *HTTPServer) buildUserAnalyticsSettings(c *contextmodel.ReqContext) dtos.AnalyticsSettings { - namespace, _ := c.SignedInUser.GetNamespacedID() + namespace, _ := c.SignedInUser.GetTypedID() // Anonymous users do not have an email or auth info - if namespace != identity.NamespaceUser { + if namespace != identity.TypeUser { return dtos.AnalyticsSettings{Identifier: "@" + hs.Cfg.AppURL} } diff --git a/pkg/api/login.go b/pkg/api/login.go index 415abb871aa..f670290f3b3 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -257,7 +257,7 @@ func (hs *HTTPServer) Logout(c *contextmodel.ReqContext) { return } - _, id := c.SignedInUser.GetNamespacedID() + _, id := c.SignedInUser.GetTypedID() hs.log.Info("Successful Logout", "userID", id) c.Redirect(redirect.URL) } @@ -305,7 +305,7 @@ func (hs *HTTPServer) redirectURLWithErrorCookie(c *contextmodel.ReqContext, err var userID int64 if c.SignedInUser != nil && !c.SignedInUser.IsNil() { var errID error - userID, errID = identity.UserIdentifier(c.SignedInUser.GetNamespacedID()) + userID, errID = identity.UserIdentifier(c.SignedInUser.GetTypedID()) if errID != nil { hs.log.Error("failed to retrieve user ID", "error", errID) } diff --git a/pkg/api/login_test.go b/pkg/api/login_test.go index c0aed770605..c442b35cb3e 100644 --- a/pkg/api/login_test.go +++ b/pkg/api/login_test.go @@ -18,6 +18,7 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/login/social" @@ -331,7 +332,7 @@ func TestLoginPostRedirect(t *testing.T) { HooksService: &hooks.HooksService{}, License: &licensing.OSSLicensingService{}, authnService: &authntest.FakeService{ - ExpectedIdentity: &authn.Identity{ID: authn.MustParseNamespaceID("user:42"), SessionToken: &usertoken.UserToken{}}, + ExpectedIdentity: &authn.Identity{ID: identity.MustParseTypedID("user:42"), SessionToken: &usertoken.UserToken{}}, }, AuthTokenService: authtest.NewFakeUserAuthTokenService(), Features: featuremgmt.WithFeatures(), diff --git a/pkg/api/org.go b/pkg/api/org.go index e6b129dd7dc..d7f572d0905 100644 --- a/pkg/api/org.go +++ b/pkg/api/org.go @@ -132,8 +132,8 @@ func (hs *HTTPServer) CreateOrg(c *contextmodel.ReqContext) response.Response { return response.Error(http.StatusBadRequest, "bad request data", err) } - namespace, identifier := c.SignedInUser.GetNamespacedID() - if namespace != identity.NamespaceUser { + namespace, identifier := c.SignedInUser.GetTypedID() + if namespace != identity.TypeUser { return response.Error(http.StatusForbidden, "Only users can create organizations", nil) } diff --git a/pkg/api/org_invite.go b/pkg/api/org_invite.go index 706ef2ca64b..ea6e0ef7722 100644 --- a/pkg/api/org_invite.go +++ b/pkg/api/org_invite.go @@ -101,9 +101,9 @@ func (hs *HTTPServer) AddOrgInvite(c *contextmodel.ReqContext) response.Response cmd.Name = inviteDto.Name cmd.Status = tempuser.TmpUserInvitePending - namespace, identifier := c.SignedInUser.GetNamespacedID() + namespace, identifier := c.SignedInUser.GetTypedID() var userID int64 - if namespace == identity.NamespaceUser || namespace == identity.NamespaceServiceAccount { + if namespace == identity.TypeUser || namespace == identity.TypeServiceAccount { var err error userID, err = strconv.ParseInt(identifier, 10, 64) if err != nil { diff --git a/pkg/api/org_test.go b/pkg/api/org_test.go index faeeb4bdbb7..246b7cd303c 100644 --- a/pkg/api/org_test.go +++ b/pkg/api/org_test.go @@ -6,14 +6,14 @@ import ( "strings" "testing" - "github.com/grafana/grafana/pkg/services/authn" - "github.com/grafana/grafana/pkg/services/authn/authntest" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" + "github.com/grafana/grafana/pkg/services/authn" + "github.com/grafana/grafana/pkg/services/authn/authntest" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgtest" "github.com/grafana/grafana/pkg/services/user" @@ -267,7 +267,7 @@ func TestAPIEndpoint_GetOrg(t *testing.T) { for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { expectedIdentity := &authn.Identity{ - ID: authn.MustParseNamespaceID("user:1"), + ID: identity.MustParseTypedID("user:1"), OrgID: 1, Permissions: map[int64]map[string][]string{ 0: accesscontrol.GroupScopesByActionContext(context.Background(), tt.permissions), diff --git a/pkg/api/pluginproxy/ds_proxy_test.go b/pkg/api/pluginproxy/ds_proxy_test.go index 5ceee760033..63a3c6eb280 100644 --- a/pkg/api/pluginproxy/ds_proxy_test.go +++ b/pkg/api/pluginproxy/ds_proxy_test.go @@ -31,7 +31,6 @@ import ( pluginfakes "github.com/grafana/grafana/pkg/plugins/manager/fakes" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" - "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/authz/zanzana" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/datasources" @@ -533,7 +532,7 @@ func TestDataSourceProxy_routeRule(t *testing.T) { &contextmodel.ReqContext{ SignedInUser: &user.SignedInUser{ Login: "test_user", - NamespacedID: authn.MustParseNamespaceID("user:1"), + NamespacedID: identity.MustParseTypedID("user:1"), }, }, &setting.Cfg{SendUserHeader: true}, diff --git a/pkg/api/pluginproxy/pluginproxy_test.go b/pkg/api/pluginproxy/pluginproxy_test.go index ab8961a42ea..91730768e34 100644 --- a/pkg/api/pluginproxy/pluginproxy_test.go +++ b/pkg/api/pluginproxy/pluginproxy_test.go @@ -12,10 +12,10 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" - "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/authz/zanzana" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -79,7 +79,7 @@ func TestPluginProxy(t *testing.T) { &contextmodel.ReqContext{ SignedInUser: &user.SignedInUser{ Login: "test_user", - NamespacedID: authn.MustParseNamespaceID("user:1"), + NamespacedID: identity.MustParseTypedID("user:1"), }, Context: &web.Context{ Req: httpReq, diff --git a/pkg/api/preferences.go b/pkg/api/preferences.go index d5c2d216204..c64d464ec25 100644 --- a/pkg/api/preferences.go +++ b/pkg/api/preferences.go @@ -22,7 +22,7 @@ func (hs *HTTPServer) SetHomeDashboard(c *contextmodel.ReqContext) response.Resp return response.Error(http.StatusBadRequest, "bad request data", err) } - userID, errID := identity.UserIdentifier(c.SignedInUser.GetNamespacedID()) + userID, errID := identity.UserIdentifier(c.SignedInUser.GetTypedID()) if errID != nil { return response.Error(http.StatusInternalServerError, "Failed to set home dashboard", errID) } @@ -64,7 +64,7 @@ func (hs *HTTPServer) SetHomeDashboard(c *contextmodel.ReqContext) response.Resp // 401: unauthorisedError // 500: internalServerError func (hs *HTTPServer) GetUserPreferences(c *contextmodel.ReqContext) response.Response { - userID, errID := identity.UserIdentifier(c.SignedInUser.GetNamespacedID()) + userID, errID := identity.UserIdentifier(c.SignedInUser.GetTypedID()) if errID != nil { return response.Error(http.StatusInternalServerError, "Failed to get user preferences", errID) } @@ -89,7 +89,7 @@ func (hs *HTTPServer) UpdateUserPreferences(c *contextmodel.ReqContext) response return response.Error(http.StatusBadRequest, "bad request data", err) } - userID, errID := identity.UserIdentifier(c.SignedInUser.GetNamespacedID()) + userID, errID := identity.UserIdentifier(c.SignedInUser.GetTypedID()) if errID != nil { return response.Error(http.StatusInternalServerError, "Failed to update user preferences", errID) } @@ -113,7 +113,7 @@ func (hs *HTTPServer) PatchUserPreferences(c *contextmodel.ReqContext) response. return response.Error(http.StatusBadRequest, "bad request data", err) } - userID, errID := identity.UserIdentifier(c.SignedInUser.GetNamespacedID()) + userID, errID := identity.UserIdentifier(c.SignedInUser.GetTypedID()) if errID != nil { return response.Error(http.StatusInternalServerError, "Failed to update user preferences", errID) } diff --git a/pkg/api/render.go b/pkg/api/render.go index ed2bb57a93b..eb5b4c10207 100644 --- a/pkg/api/render.go +++ b/pkg/api/render.go @@ -65,7 +65,7 @@ func (hs *HTTPServer) RenderHandler(c *contextmodel.ReqContext) { headers["Accept-Language"] = acceptLanguageHeader } - userID, errID := identity.UserIdentifier(c.SignedInUser.GetNamespacedID()) + userID, errID := identity.UserIdentifier(c.SignedInUser.GetTypedID()) if errID != nil { hs.log.Error("Failed to parse user id", "err", errID) } diff --git a/pkg/api/signup.go b/pkg/api/signup.go index 74cb5379c45..6f4c175432c 100644 --- a/pkg/api/signup.go +++ b/pkg/api/signup.go @@ -47,7 +47,7 @@ func (hs *HTTPServer) SignUp(c *contextmodel.ReqContext) response.Response { return response.Error(http.StatusUnprocessableEntity, "User with same email address already exists", nil) } - userID, errID := identity.UserIdentifier(c.SignedInUser.GetNamespacedID()) + userID, errID := identity.UserIdentifier(c.SignedInUser.GetTypedID()) if errID != nil { hs.log.Error("Failed to parse user id", "err", errID) } diff --git a/pkg/api/user.go b/pkg/api/user.go index 32b9d5f5199..dd09ea8f65c 100644 --- a/pkg/api/user.go +++ b/pkg/api/user.go @@ -31,8 +31,8 @@ import ( // 404: notFoundError // 500: internalServerError func (hs *HTTPServer) GetSignedInUser(c *contextmodel.ReqContext) response.Response { - namespace, identifier := c.SignedInUser.GetNamespacedID() - if namespace != identity.NamespaceUser { + namespace, identifier := c.SignedInUser.GetTypedID() + if namespace != identity.TypeUser { return response.JSON(http.StatusOK, user.UserProfileDTO{ IsGrafanaAdmin: c.SignedInUser.GetIsGrafanaAdmin(), OrgID: c.SignedInUser.GetOrgID(), @@ -278,8 +278,8 @@ func (hs *HTTPServer) handleUpdateUser(ctx context.Context, cmd user.UpdateUserC } func (hs *HTTPServer) StartEmailVerificaton(c *contextmodel.ReqContext) response.Response { - namespace, id := c.SignedInUser.GetNamespacedID() - if !identity.IsNamespace(namespace, identity.NamespaceUser) { + namespace, id := c.SignedInUser.GetTypedID() + if !identity.IsIdentityType(namespace, identity.TypeUser) { return response.Error(http.StatusBadRequest, "Only users can verify their email", nil) } @@ -506,8 +506,8 @@ func (hs *HTTPServer) ChangeActiveOrgAndRedirectToHome(c *contextmodel.ReqContex return } - namespace, identifier := c.SignedInUser.GetNamespacedID() - if namespace != identity.NamespaceUser { + namespace, identifier := c.SignedInUser.GetTypedID() + if namespace != identity.TypeUser { c.JsonApiErr(http.StatusForbidden, "Endpoint only available for users", nil) return } @@ -632,8 +632,8 @@ func (hs *HTTPServer) ClearHelpFlags(c *contextmodel.ReqContext) response.Respon } func getUserID(c *contextmodel.ReqContext) (int64, *response.NormalResponse) { - namespace, identifier := c.SignedInUser.GetNamespacedID() - if namespace != identity.NamespaceUser { + namespace, identifier := c.SignedInUser.GetTypedID() + if namespace != identity.TypeUser { return 0, response.Error(http.StatusForbidden, "Endpoint only available for users", nil) } diff --git a/pkg/api/user_token.go b/pkg/api/user_token.go index 50b34ebfd0f..fc59121d03a 100644 --- a/pkg/api/user_token.go +++ b/pkg/api/user_token.go @@ -32,8 +32,8 @@ import ( // 403: forbiddenError // 500: internalServerError func (hs *HTTPServer) GetUserAuthTokens(c *contextmodel.ReqContext) response.Response { - namespace, identifier := c.SignedInUser.GetNamespacedID() - if namespace != identity.NamespaceUser { + namespace, identifier := c.SignedInUser.GetTypedID() + if namespace != identity.TypeUser { return response.Error(http.StatusForbidden, "entity not allowed to revoke tokens", nil) } @@ -63,8 +63,8 @@ func (hs *HTTPServer) RevokeUserAuthToken(c *contextmodel.ReqContext) response.R return response.Error(http.StatusBadRequest, "bad request data", err) } - namespace, identifier := c.SignedInUser.GetNamespacedID() - if namespace != identity.NamespaceUser { + namespace, identifier := c.SignedInUser.GetTypedID() + if namespace != identity.TypeUser { return response.Error(http.StatusForbidden, "entity not allowed to revoke tokens", nil) } diff --git a/pkg/apimachinery/identity/error.go b/pkg/apimachinery/identity/error.go index 91291e7bbb8..bdc5520bd72 100644 --- a/pkg/apimachinery/identity/error.go +++ b/pkg/apimachinery/identity/error.go @@ -7,7 +7,7 @@ import ( ) var ( - ErrInvalidNamespaceID = errutil.BadRequest("auth.identity.invalid-namespace-id") + ErrInvalidTypedID = errutil.BadRequest("auth.identity.invalid-typed-id") ErrNotIntIdentifier = errors.New("identifier is not an int64") ErrIdentifierNotInitialized = errors.New("identifier is not initialized") ) diff --git a/pkg/apimachinery/identity/namespace.go b/pkg/apimachinery/identity/namespace.go index 2b72ae71f58..485d5ea4081 100644 --- a/pkg/apimachinery/identity/namespace.go +++ b/pkg/apimachinery/identity/namespace.go @@ -6,120 +6,131 @@ import ( "strings" ) -type Namespace string +type IdentityType string const ( - NamespaceUser Namespace = "user" - NamespaceAPIKey Namespace = "api-key" - NamespaceServiceAccount Namespace = "service-account" - NamespaceAnonymous Namespace = "anonymous" - NamespaceRenderService Namespace = "render" - NamespaceAccessPolicy Namespace = "access-policy" - NamespaceProvisioning Namespace = "provisioning" - NamespaceEmpty Namespace = "" + TypeUser IdentityType = "user" + TypeAPIKey IdentityType = "api-key" + TypeServiceAccount IdentityType = "service-account" + TypeAnonymous IdentityType = "anonymous" + TypeRenderService IdentityType = "render" + TypeAccessPolicy IdentityType = "access-policy" + TypeProvisioning IdentityType = "provisioning" + TypeEmpty IdentityType = "" ) -func (n Namespace) String() string { +func (n IdentityType) String() string { return string(n) } -func ParseNamespace(str string) (Namespace, error) { +func ParseType(str string) (IdentityType, error) { switch str { - case string(NamespaceUser): - return NamespaceUser, nil - case string(NamespaceAPIKey): - return NamespaceAPIKey, nil - case string(NamespaceServiceAccount): - return NamespaceServiceAccount, nil - case string(NamespaceAnonymous): - return NamespaceAnonymous, nil - case string(NamespaceRenderService): - return NamespaceRenderService, nil - case string(NamespaceAccessPolicy): - return NamespaceAccessPolicy, nil + case string(TypeUser): + return TypeUser, nil + case string(TypeAPIKey): + return TypeAPIKey, nil + case string(TypeServiceAccount): + return TypeServiceAccount, nil + case string(TypeAnonymous): + return TypeAnonymous, nil + case string(TypeRenderService): + return TypeRenderService, nil + case string(TypeAccessPolicy): + return TypeAccessPolicy, nil default: - return "", ErrInvalidNamespaceID.Errorf("got invalid namespace %s", str) + return "", ErrInvalidTypedID.Errorf("got invalid identity type %s", str) } } -var AnonymousNamespaceID = NewNamespaceID(NamespaceAnonymous, 0) +// IsIdentityType returns true if type matches any expected identity type +func IsIdentityType(typ IdentityType, expected ...IdentityType) bool { + for _, e := range expected { + if typ == e { + return true + } + } -func ParseNamespaceID(str string) (NamespaceID, error) { - var namespaceID NamespaceID + return false +} + +var AnonymousTypedID = NewTypedID(TypeAnonymous, 0) + +func ParseTypedID(str string) (TypedID, error) { + var typeID TypedID parts := strings.Split(str, ":") if len(parts) != 2 { - return namespaceID, ErrInvalidNamespaceID.Errorf("expected namespace id to have 2 parts") + return typeID, ErrInvalidTypedID.Errorf("expected typed id to have 2 parts") } - namespace, err := ParseNamespace(parts[0]) + t, err := ParseType(parts[0]) if err != nil { - return namespaceID, err + return typeID, err } - namespaceID.id = parts[1] - namespaceID.namespace = namespace + typeID.id = parts[1] + typeID.t = t - return namespaceID, nil + return typeID, nil } -// MustParseNamespaceID parses namespace id, it will panic if it fails to do so. +// MustParseTypedID parses namespace id, it will panic if it fails to do so. // Suitable to use in tests or when we can guarantee that we pass a correct format. -func MustParseNamespaceID(str string) NamespaceID { - namespaceID, err := ParseNamespaceID(str) +func MustParseTypedID(str string) TypedID { + typeID, err := ParseTypedID(str) if err != nil { panic(err) } - return namespaceID + return typeID } -func NewNamespaceID(namespace Namespace, id int64) NamespaceID { - return NamespaceID{ - id: strconv.FormatInt(id, 10), - namespace: namespace, +func NewTypedID(t IdentityType, id int64) TypedID { + return TypedID{ + id: strconv.FormatInt(id, 10), + t: t, } } -// NewNamespaceIDString creates a new NamespaceID with a string id -func NewNamespaceIDString(namespace Namespace, id string) NamespaceID { - return NamespaceID{ - id: id, - namespace: namespace, +// NewTypedIDString creates a new TypedID with a string id +func NewTypedIDString(t IdentityType, id string) TypedID { + return TypedID{ + id: id, + t: t, } } // FIXME: use this instead of encoded string through the codebase -type NamespaceID struct { - id string - namespace Namespace +type TypedID struct { + id string + t IdentityType } -func (ni NamespaceID) ID() string { +func (ni TypedID) ID() string { return ni.id } // UserID will try to parse and int64 identifier if namespace is either user or service-account. // For all other namespaces '0' will be returned. -func (ni NamespaceID) UserID() (int64, error) { - if ni.IsNamespace(NamespaceUser, NamespaceServiceAccount) { +func (ni TypedID) UserID() (int64, error) { + if ni.IsType(TypeUser, TypeServiceAccount) { return ni.ParseInt() } return 0, nil } // ParseInt will try to parse the id as an int64 identifier. -func (ni NamespaceID) ParseInt() (int64, error) { +func (ni TypedID) ParseInt() (int64, error) { return strconv.ParseInt(ni.id, 10, 64) } -func (ni NamespaceID) Namespace() Namespace { - return ni.namespace +func (ni TypedID) Type() IdentityType { + return ni.t } -func (ni NamespaceID) IsNamespace(expected ...Namespace) bool { - return IsNamespace(ni.namespace, expected...) +func (ni TypedID) IsType(expected ...IdentityType) bool { + return IsIdentityType(ni.t, expected...) } -func (ni NamespaceID) String() string { - return fmt.Sprintf("%s:%s", ni.namespace, ni.id) +func (ni TypedID) String() string { + return fmt.Sprintf("%s:%s", ni.t, ni.id) } diff --git a/pkg/apimachinery/identity/requester.go b/pkg/apimachinery/identity/requester.go index f65ec49e91e..2c372d2182c 100644 --- a/pkg/apimachinery/identity/requester.go +++ b/pkg/apimachinery/identity/requester.go @@ -7,13 +7,13 @@ import ( type Requester interface { // GetID returns namespaced id for the entity - GetID() NamespaceID - // GetNamespacedID returns the namespace and ID of the active entity. + GetID() TypedID + // GetTypedID returns the namespace and ID of the active entity. // The namespace is one of the constants defined in pkg/apimachinery/identity. // Deprecated: use GetID instead - GetNamespacedID() (namespace Namespace, identifier string) + GetTypedID() (kind IdentityType, identifier string) // GetUID returns namespaced uid for the entity - GetUID() NamespaceID + GetUID() TypedID // GetDisplayName returns the display name of the active entity. // The display name is the name if it is set, otherwise the login or email. GetDisplayName() string @@ -68,25 +68,14 @@ type Requester interface { GetIDToken() string } -// IsNamespace returns true if namespace matches any expected namespace -func IsNamespace(namespace Namespace, expected ...Namespace) bool { - for _, e := range expected { - if namespace == e { - return true - } - } - - return false -} - // IntIdentifier converts a string identifier to an int64. // Applicable for users, service accounts, api keys and renderer service. // Errors if the identifier is not initialized or if namespace is not recognized. -func IntIdentifier(namespace Namespace, identifier string) (int64, error) { - if IsNamespace(namespace, NamespaceUser, NamespaceAPIKey, NamespaceServiceAccount, NamespaceRenderService) { +func IntIdentifier(kind IdentityType, identifier string) (int64, error) { + if IsIdentityType(kind, TypeUser, TypeAPIKey, TypeServiceAccount, TypeRenderService) { id, err := strconv.ParseInt(identifier, 10, 64) if err != nil { - return 0, fmt.Errorf("unrecognized format for valid namespace %s: %w", namespace, err) + return 0, fmt.Errorf("unrecognized format for valid type %s: %w", kind, err) } if id < 1 { @@ -102,14 +91,14 @@ func IntIdentifier(namespace Namespace, identifier string) (int64, error) { // UserIdentifier converts a string identifier to an int64. // Errors if the identifier is not initialized or if namespace is not recognized. // Returns 0 if the namespace is not user or service account -func UserIdentifier(namespace Namespace, identifier string) (int64, error) { - userID, err := IntIdentifier(namespace, identifier) +func UserIdentifier(kind IdentityType, identifier string) (int64, error) { + userID, err := IntIdentifier(kind, identifier) if err != nil { // FIXME: return this error once entity namespaces are handled by stores return 0, nil } - if IsNamespace(namespace, NamespaceUser, NamespaceServiceAccount) { + if IsIdentityType(kind, TypeUser, TypeServiceAccount) { return userID, nil } diff --git a/pkg/apimachinery/identity/static.go b/pkg/apimachinery/identity/static.go index a76420aa119..43772845033 100644 --- a/pkg/apimachinery/identity/static.go +++ b/pkg/apimachinery/identity/static.go @@ -9,7 +9,7 @@ var _ Requester = &StaticRequester{} // This is mostly copied from: // https://github.com/grafana/grafana/blob/v11.0.0/pkg/services/user/identity.go#L16 type StaticRequester struct { - Namespace Namespace + Type IdentityType UserID int64 UserUID string OrgID int64 @@ -105,19 +105,19 @@ func (u *StaticRequester) HasUniqueId() bool { } // GetID returns namespaced id for the entity -func (u *StaticRequester) GetID() NamespaceID { - return NewNamespaceIDString(u.Namespace, fmt.Sprintf("%d", u.UserID)) +func (u *StaticRequester) GetID() TypedID { + return NewTypedIDString(u.Type, fmt.Sprintf("%d", u.UserID)) } // GetUID returns namespaced uid for the entity -func (u *StaticRequester) GetUID() NamespaceID { - return NewNamespaceIDString(u.Namespace, u.UserUID) +func (u *StaticRequester) GetUID() TypedID { + return NewTypedIDString(u.Type, u.UserUID) } -// GetNamespacedID returns the namespace and ID of the active entity +// GetTypedID returns the namespace and ID of the active entity // The namespace is one of the constants defined in pkg/apimachinery/identity -func (u *StaticRequester) GetNamespacedID() (Namespace, string) { - return u.Namespace, fmt.Sprintf("%d", u.UserID) +func (u *StaticRequester) GetTypedID() (IdentityType, string) { + return u.Type, fmt.Sprintf("%d", u.UserID) } func (u *StaticRequester) GetAuthID() string { diff --git a/pkg/apiserver/endpoints/filters/requester.go b/pkg/apiserver/endpoints/filters/requester.go index 4885594b0fb..71bb45e19c0 100644 --- a/pkg/apiserver/endpoints/filters/requester.go +++ b/pkg/apiserver/endpoints/filters/requester.go @@ -26,7 +26,7 @@ func WithRequester(handler http.Handler) http.Handler { if ok { if info.GetName() == user.Anonymous { requester = &identity.StaticRequester{ - Namespace: identity.NamespaceAnonymous, + Type: identity.TypeAnonymous, Name: info.GetName(), Login: info.GetName(), Permissions: map[int64]map[string][]string{}, @@ -37,12 +37,12 @@ func WithRequester(handler http.Handler) http.Handler { slices.Contains(info.GetGroups(), user.SystemPrivilegedGroup) { orgId := int64(1) requester = &identity.StaticRequester{ - Namespace: identity.NamespaceServiceAccount, // system:apiserver - UserID: 1, - OrgID: orgId, - Name: info.GetName(), - Login: info.GetName(), - OrgRole: identity.RoleAdmin, + Type: identity.TypeServiceAccount, // system:apiserver + UserID: 1, + OrgID: orgId, + Name: info.GetName(), + Login: info.GetName(), + OrgRole: identity.RoleAdmin, IsGrafanaAdmin: true, AllowedKubernetesNamespace: "default", diff --git a/pkg/middleware/auth_test.go b/pkg/middleware/auth_test.go index d52c95b9905..ee75387d963 100644 --- a/pkg/middleware/auth_test.go +++ b/pkg/middleware/auth_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log/logtest" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/plugins" @@ -62,7 +63,7 @@ func TestAuth_Middleware(t *testing.T) { desc: "ReqSignedIn should return 200 for anonymous user", path: "/api/secure", authMiddleware: ReqSignedIn, - identity: &authn.Identity{ID: authn.AnonymousNamespaceID}, + identity: &authn.Identity{ID: identity.AnonymousTypedID}, expecedReached: true, expectedCode: http.StatusOK, }, @@ -70,7 +71,7 @@ func TestAuth_Middleware(t *testing.T) { desc: "ReqSignedIn should return redirect anonymous user with forceLogin query string", path: "/secure?forceLogin=true", authMiddleware: ReqSignedIn, - identity: &authn.Identity{ID: authn.AnonymousNamespaceID}, + identity: &authn.Identity{ID: identity.AnonymousTypedID}, expecedReached: false, expectedCode: http.StatusFound, }, @@ -78,7 +79,7 @@ func TestAuth_Middleware(t *testing.T) { desc: "ReqSignedIn should return redirect anonymous user when orgId in query string is different from currently used", path: "/secure?orgId=2", authMiddleware: ReqSignedIn, - identity: &authn.Identity{ID: authn.AnonymousNamespaceID, OrgID: 1}, + identity: &authn.Identity{ID: identity.AnonymousTypedID, OrgID: 1}, expecedReached: false, expectedCode: http.StatusFound, }, @@ -86,7 +87,7 @@ func TestAuth_Middleware(t *testing.T) { desc: "ReqSignedInNoAnonymous should return 401 for anonymous user", path: "/api/secure", authMiddleware: ReqSignedInNoAnonymous, - identity: &authn.Identity{ID: authn.AnonymousNamespaceID}, + identity: &authn.Identity{ID: identity.AnonymousTypedID}, expecedReached: false, expectedCode: http.StatusUnauthorized, }, @@ -94,7 +95,7 @@ func TestAuth_Middleware(t *testing.T) { desc: "ReqSignedInNoAnonymous should return 200 for authenticated user", path: "/api/secure", authMiddleware: ReqSignedInNoAnonymous, - identity: &authn.Identity{ID: authn.MustParseNamespaceID("user:1")}, + identity: &authn.Identity{ID: identity.MustParseTypedID("user:1")}, expecedReached: true, expectedCode: http.StatusOK, }, @@ -102,7 +103,7 @@ func TestAuth_Middleware(t *testing.T) { desc: "snapshot public mode disabled should return 200 for authenticated user", path: "/api/secure", authMiddleware: SnapshotPublicModeOrSignedIn(&setting.Cfg{SnapshotPublicMode: false}), - identity: &authn.Identity{ID: authn.MustParseNamespaceID("user:1")}, + identity: &authn.Identity{ID: identity.MustParseTypedID("user:1")}, expecedReached: true, expectedCode: http.StatusOK, }, diff --git a/pkg/middleware/quota_test.go b/pkg/middleware/quota_test.go index 811871de525..94b6ee3e0e3 100644 --- a/pkg/middleware/quota_test.go +++ b/pkg/middleware/quota_test.go @@ -5,6 +5,7 @@ import ( "github.com/stretchr/testify/assert" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/quota/quotatest" @@ -52,7 +53,7 @@ func TestMiddlewareQuota(t *testing.T) { t.Run("with user logged in", func(t *testing.T) { setUp := func(sc *scenarioContext) { - sc.withIdentity(&authn.Identity{ID: authn.MustParseNamespaceID("user:1"), SessionToken: &auth.UserToken{UserId: 12}}) + sc.withIdentity(&authn.Identity{ID: identity.MustParseTypedID("user:1"), SessionToken: &auth.UserToken{UserId: 12}}) } middlewareScenario(t, "global datasource quota reached", func(t *testing.T, sc *scenarioContext) { diff --git a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go index 9347f389609..35032b5d956 100644 --- a/pkg/registry/apis/dashboard/legacy/sql_dashboards.go +++ b/pkg/registry/apis/dashboard/legacy/sql_dashboards.go @@ -333,9 +333,9 @@ func (a *dashboardSqlAccess) scanRow(rows *sql.Rows) (*dashboardRow, error) { func getUserID(v sql.NullString) string { if v.String == "" { - return identity.NewNamespaceIDString(identity.NamespaceProvisioning, "").String() + return identity.NewTypedIDString(identity.TypeProvisioning, "").String() } - return identity.NewNamespaceIDString(identity.NamespaceUser, v.String).String() + return identity.NewTypedIDString(identity.TypeUser, v.String).String() } // DeleteDashboard implements DashboardAccess. diff --git a/pkg/services/accesscontrol/accesscontrol.go b/pkg/services/accesscontrol/accesscontrol.go index 485c98dfe36..2c89d4e3e0f 100644 --- a/pkg/services/accesscontrol/accesscontrol.go +++ b/pkg/services/accesscontrol/accesscontrol.go @@ -2,7 +2,6 @@ package accesscontrol import ( "context" - "errors" "fmt" "strings" @@ -84,8 +83,8 @@ type SearchOptions struct { Action string ActionSets []string Scope string - NamespacedID string // ID of the identity (ex: user:3, service-account:4) - wildcards Wildcards // private field computed based on the Scope + TypedID identity.TypedID // ID of the identity (ex: user:3, service-account:4) + wildcards Wildcards // private field computed based on the Scope RolePrefixes []string } @@ -105,21 +104,17 @@ func (s *SearchOptions) Wildcards() []string { } func (s *SearchOptions) ComputeUserID() (int64, error) { - if s.NamespacedID == "" { - return 0, errors.New("namespacedID must be set") - } - - id, err := identity.ParseNamespaceID(s.NamespacedID) + id, err := s.TypedID.ParseInt() if err != nil { return 0, err } // Validate namespace type is user or service account - if id.Namespace() != identity.NamespaceUser && id.Namespace() != identity.NamespaceServiceAccount { - return 0, fmt.Errorf("invalid namespace: %s", id.Namespace()) + if s.TypedID.Type() != identity.TypeUser && s.TypedID.Type() != identity.TypeServiceAccount { + return 0, fmt.Errorf("invalid type: %s", s.TypedID.Type()) } - return id.ParseInt() + return id, nil } type SyncUserRolesCommand struct { diff --git a/pkg/services/accesscontrol/acimpl/accesscontrol.go b/pkg/services/accesscontrol/acimpl/accesscontrol.go index 338ea891a44..16818dd27ce 100644 --- a/pkg/services/accesscontrol/acimpl/accesscontrol.go +++ b/pkg/services/accesscontrol/acimpl/accesscontrol.go @@ -191,6 +191,6 @@ func (a *AccessControl) RegisterScopeAttributeResolver(prefix string, resolver a } func (a *AccessControl) debug(ctx context.Context, ident identity.Requester, msg string, eval accesscontrol.Evaluator) { - namespace, id := ident.GetNamespacedID() + namespace, id := ident.GetTypedID() a.log.FromContext(ctx).Debug(msg, "namespace", namespace, "id", id, "orgID", ident.GetOrgID(), "permissions", eval.GoString()) } diff --git a/pkg/services/accesscontrol/acimpl/service.go b/pkg/services/accesscontrol/acimpl/service.go index 31512dfbf5f..790fc5773fd 100644 --- a/pkg/services/accesscontrol/acimpl/service.go +++ b/pkg/services/accesscontrol/acimpl/service.go @@ -25,7 +25,6 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol/database" "github.com/grafana/grafana/pkg/services/accesscontrol/migrator" "github.com/grafana/grafana/pkg/services/accesscontrol/pluginutils" - "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/authz/zanzana" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/featuremgmt" @@ -141,7 +140,7 @@ func (s *Service) getUserPermissions(ctx context.Context, user identity.Requeste permissions = append(permissions, SharedWithMeFolderPermission) } - userID, err := identity.UserIdentifier(user.GetNamespacedID()) + userID, err := identity.UserIdentifier(user.GetTypedID()) if err != nil { return nil, err } @@ -209,10 +208,10 @@ func (s *Service) getUserDirectPermissions(ctx context.Context, user identity.Re ctx, span := s.tracer.Start(ctx, "authz.getUserDirectPermissions") defer span.End() - namespace, identifier := user.GetNamespacedID() + namespace, identifier := user.GetTypedID() var userID int64 - if namespace == authn.NamespaceUser || namespace == authn.NamespaceServiceAccount { + if namespace == identity.TypeUser || namespace == identity.TypeServiceAccount { var err error userID, err = strconv.ParseInt(identifier, 10, 64) if err != nil { @@ -483,7 +482,7 @@ func (s *Service) SearchUsersPermissions(ctx context.Context, usr identity.Reque // Limit roles to available in OSS options.RolePrefixes = OSSRolesPrefixes - if options.NamespacedID != "" { + if options.TypedID.Type() != "" { userID, err := options.ComputeUserID() if err != nil { s.log.Error("Failed to resolve user ID", "error", err) @@ -598,7 +597,7 @@ func (s *Service) SearchUserPermissions(ctx context.Context, orgID int64, search timer := prometheus.NewTimer(metrics.MAccessPermissionsSummary) defer timer.ObserveDuration() - if searchOptions.NamespacedID == "" { + if searchOptions.TypedID.Type() == "" { return nil, fmt.Errorf("expected namespaced ID to be specified") } diff --git a/pkg/services/accesscontrol/acimpl/service_bench_test.go b/pkg/services/accesscontrol/acimpl/service_bench_test.go index 78e153936c7..20772c5c8a9 100644 --- a/pkg/services/accesscontrol/acimpl/service_bench_test.go +++ b/pkg/services/accesscontrol/acimpl/service_bench_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/infra/log" @@ -261,7 +262,7 @@ func benchSearchUserWithAction(b *testing.B, usersCount, resourceCount int) { for n := 0; n < b.N; n++ { usersPermissions, err := acService.SearchUsersPermissions(context.Background(), siu, - accesscontrol.SearchOptions{Action: "resources:action2", NamespacedID: "user:14"}) + accesscontrol.SearchOptions{Action: "resources:action2", TypedID: identity.NewTypedID(identity.TypeUser, 14)}) require.NoError(b, err) require.Len(b, usersPermissions, 1) for _, permissions := range usersPermissions { diff --git a/pkg/services/accesscontrol/acimpl/service_test.go b/pkg/services/accesscontrol/acimpl/service_test.go index ae11d5b3b84..19245d113f8 100644 --- a/pkg/services/accesscontrol/acimpl/service_test.go +++ b/pkg/services/accesscontrol/acimpl/service_test.go @@ -2,7 +2,6 @@ package acimpl import ( "context" - "fmt" "strings" "testing" @@ -544,7 +543,7 @@ func TestService_SearchUsersPermissions(t *testing.T) { // only the user's basic roles and the user's stored permissions name: "check namespacedId filter works correctly", siuPermissions: listAllPerms, - searchOption: accesscontrol.SearchOptions{NamespacedID: fmt.Sprintf("%s:1", identity.NamespaceServiceAccount)}, + searchOption: accesscontrol.SearchOptions{TypedID: identity.NewTypedID(identity.TypeServiceAccount, 1)}, ramRoles: map[string]*accesscontrol.RoleDTO{ string(identity.RoleEditor): {Permissions: []accesscontrol.Permission{ {Action: accesscontrol.ActionTeamsRead, Scope: "teams:*"}, @@ -616,7 +615,7 @@ func TestService_SearchUserPermissions(t *testing.T) { name: "ram only", searchOption: accesscontrol.SearchOptions{ ActionPrefix: "teams", - NamespacedID: fmt.Sprintf("%s:2", identity.NamespaceUser), + TypedID: identity.NewTypedID(identity.TypeUser, 2), }, ramRoles: map[string]*accesscontrol.RoleDTO{ string(identity.RoleEditor): {Permissions: []accesscontrol.Permission{ @@ -641,7 +640,7 @@ func TestService_SearchUserPermissions(t *testing.T) { name: "stored only", searchOption: accesscontrol.SearchOptions{ ActionPrefix: "teams", - NamespacedID: fmt.Sprintf("%s:2", identity.NamespaceUser), + TypedID: identity.NewTypedID(identity.TypeUser, 2), }, storedPerms: map[int64][]accesscontrol.Permission{ 1: {{Action: accesscontrol.ActionTeamsRead, Scope: "teams:id:1"}}, @@ -661,7 +660,7 @@ func TestService_SearchUserPermissions(t *testing.T) { name: "ram and stored", searchOption: accesscontrol.SearchOptions{ ActionPrefix: "teams", - NamespacedID: fmt.Sprintf("%s:2", identity.NamespaceUser), + TypedID: identity.NewTypedID(identity.TypeUser, 2), }, ramRoles: map[string]*accesscontrol.RoleDTO{ string(identity.RoleAdmin): {Permissions: []accesscontrol.Permission{ @@ -691,7 +690,7 @@ func TestService_SearchUserPermissions(t *testing.T) { name: "check action prefix filter works correctly", searchOption: accesscontrol.SearchOptions{ ActionPrefix: "teams", - NamespacedID: fmt.Sprintf("%s:1", identity.NamespaceUser), + TypedID: identity.NewTypedID(identity.TypeUser, 1), }, ramRoles: map[string]*accesscontrol.RoleDTO{ string(identity.RoleEditor): {Permissions: []accesscontrol.Permission{ @@ -712,8 +711,8 @@ func TestService_SearchUserPermissions(t *testing.T) { { name: "check action filter works correctly", searchOption: accesscontrol.SearchOptions{ - Action: accesscontrol.ActionTeamsRead, - NamespacedID: fmt.Sprintf("%s:1", identity.NamespaceUser), + Action: accesscontrol.ActionTeamsRead, + TypedID: identity.NewTypedID(identity.TypeUser, 1), }, ramRoles: map[string]*accesscontrol.RoleDTO{ string(identity.RoleEditor): {Permissions: []accesscontrol.Permission{ @@ -734,8 +733,8 @@ func TestService_SearchUserPermissions(t *testing.T) { { name: "check action sets are correctly included if an action is specified", searchOption: accesscontrol.SearchOptions{ - Action: "dashboards:read", - NamespacedID: fmt.Sprintf("%s:1", identity.NamespaceUser), + Action: "dashboards:read", + TypedID: identity.NewTypedID(identity.TypeUser, 1), }, withActionSets: true, actionSets: map[string][]string{ @@ -768,7 +767,7 @@ func TestService_SearchUserPermissions(t *testing.T) { name: "check action sets are correctly included if an action prefix is specified", searchOption: accesscontrol.SearchOptions{ ActionPrefix: "dashboards", - NamespacedID: fmt.Sprintf("%s:1", identity.NamespaceUser), + TypedID: identity.NewTypedID(identity.TypeUser, 1), }, withActionSets: true, actionSets: map[string][]string{ diff --git a/pkg/services/accesscontrol/api/api.go b/pkg/services/accesscontrol/api/api.go index bd7caa6e35c..a56f21b4f61 100644 --- a/pkg/services/accesscontrol/api/api.go +++ b/pkg/services/accesscontrol/api/api.go @@ -5,6 +5,7 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/middleware/requestmeta" ac "github.com/grafana/grafana/pkg/services/accesscontrol" @@ -71,16 +72,23 @@ func (api *AccessControlAPI) searchUsersPermissions(c *contextmodel.ReqContext) ActionPrefix: c.Query("actionPrefix"), Action: c.Query("action"), Scope: c.Query("scope"), - NamespacedID: c.Query("namespacedId"), } + namespacedId := c.Query("namespacedId") // Validate inputs if searchOptions.ActionPrefix != "" && searchOptions.Action != "" { return response.JSON(http.StatusBadRequest, "'action' and 'actionPrefix' are mutually exclusive") } - if searchOptions.NamespacedID == "" && searchOptions.ActionPrefix == "" && searchOptions.Action == "" { + if namespacedId == "" && searchOptions.ActionPrefix == "" && searchOptions.Action == "" { return response.JSON(http.StatusBadRequest, "at least one search option must be provided") } + if namespacedId != "" { + var err error + searchOptions.TypedID, err = identity.ParseTypedID(namespacedId) + if err != nil { + return response.Error(http.StatusBadGateway, "invalid namespacedId", err) + } + } // Compute metadata permissions, err := api.Service.SearchUsersPermissions(c.Req.Context(), c.SignedInUser, searchOptions) diff --git a/pkg/services/accesscontrol/authorize_in_org_test.go b/pkg/services/accesscontrol/authorize_in_org_test.go index 47d117766f2..44f41d37bb0 100644 --- a/pkg/services/accesscontrol/authorize_in_org_test.go +++ b/pkg/services/accesscontrol/authorize_in_org_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/assert" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/accesscontrol/actest" @@ -186,7 +187,7 @@ func TestAuthorizeInOrgMiddleware(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/api/endpoint", nil) expectedIdentity := &authn.Identity{ - ID: authn.NewNamespaceID(authn.NamespaceUser, tc.ctxSignedInUser.UserID), + ID: identity.NewTypedID(identity.TypeUser, tc.ctxSignedInUser.UserID), OrgID: tc.targetOrgId, Permissions: map[int64]map[string][]string{}, } diff --git a/pkg/services/accesscontrol/cacheutils_test.go b/pkg/services/accesscontrol/cacheutils_test.go index 91416b51223..b661f5fd6fc 100644 --- a/pkg/services/accesscontrol/cacheutils_test.go +++ b/pkg/services/accesscontrol/cacheutils_test.go @@ -21,7 +21,7 @@ func TestPermissionCacheKey(t *testing.T) { signedInUser: &user.SignedInUser{ OrgID: 1, UserID: 1, - NamespacedID: identity.MustParseNamespaceID("user:1"), + NamespacedID: identity.MustParseTypedID("user:1"), }, expected: "rbac-permissions-1-user-1", }, @@ -31,7 +31,7 @@ func TestPermissionCacheKey(t *testing.T) { OrgID: 1, ApiKeyID: 1, IsServiceAccount: false, - NamespacedID: identity.MustParseNamespaceID("user:1"), + NamespacedID: identity.MustParseTypedID("user:1"), }, expected: "rbac-permissions-1-api-key-1", }, @@ -41,7 +41,7 @@ func TestPermissionCacheKey(t *testing.T) { OrgID: 1, UserID: 1, IsServiceAccount: true, - NamespacedID: identity.MustParseNamespaceID("service-account:1"), + NamespacedID: identity.MustParseTypedID("service-account:1"), }, expected: "rbac-permissions-1-service-account-1", }, @@ -51,7 +51,7 @@ func TestPermissionCacheKey(t *testing.T) { OrgID: 1, UserID: -1, IsServiceAccount: true, - NamespacedID: identity.MustParseNamespaceID("service-account:-1"), + NamespacedID: identity.MustParseTypedID("service-account:-1"), }, expected: "rbac-permissions-1-service-account--1", }, @@ -60,7 +60,7 @@ func TestPermissionCacheKey(t *testing.T) { signedInUser: &user.SignedInUser{ OrgID: 1, OrgRole: org.RoleNone, - NamespacedID: identity.MustParseNamespaceID("user:1"), + NamespacedID: identity.MustParseTypedID("user:1"), }, expected: "rbac-permissions-1-user-None", }, diff --git a/pkg/services/accesscontrol/database/database.go b/pkg/services/accesscontrol/database/database.go index 4b3c6479de6..43c3fa448ee 100644 --- a/pkg/services/accesscontrol/database/database.go +++ b/pkg/services/accesscontrol/database/database.go @@ -163,8 +163,8 @@ func (s *AccessControlStore) SearchUsersPermissions(ctx context.Context, orgID i } dbPerms := make([]UserRBACPermission, 0) - var userID int64 - if options.NamespacedID != "" { + userID := int64(-1) + if options.TypedID.Type() != "" { var err error userID, err = options.ComputeUserID() if err != nil { @@ -181,26 +181,26 @@ func (s *AccessControlStore) SearchUsersPermissions(ctx context.Context, orgID i params := []any{} direct := userAssignsSQL - if options.NamespacedID != "" { + if userID >= 0 { direct += " WHERE ur.user_id = ?" params = append(params, userID) } team := teamAssignsSQL - if options.NamespacedID != "" { + if userID >= 0 { team += " WHERE tm.user_id = ?" params = append(params, userID) } basic := basicRoleAssignsSQL - if options.NamespacedID != "" { + if userID >= 0 { basic += " WHERE ou.user_id = ?" params = append(params, userID) } grafanaAdmin := fmt.Sprintf(grafanaAdminAssignsSQL, s.sql.ReadReplica().Quote("user")) params = append(params, accesscontrol.RoleGrafanaAdmin) - if options.NamespacedID != "" { + if userID >= 0 { grafanaAdmin += " AND sa.user_id = ?" params = append(params, userID) } diff --git a/pkg/services/accesscontrol/database/database_test.go b/pkg/services/accesscontrol/database/database_test.go index 39d25fa3029..8099a22e910 100644 --- a/pkg/services/accesscontrol/database/database_test.go +++ b/pkg/services/accesscontrol/database/database_test.go @@ -626,7 +626,7 @@ func TestIntegrationAccessControlStore_SearchUsersPermissions(t *testing.T) { }, options: accesscontrol.SearchOptions{ ActionPrefix: "teams:", - NamespacedID: fmt.Sprintf("%s:1", identity.NamespaceUser), + TypedID: identity.NewTypedID(identity.TypeUser, 1), }, wantPerm: map[int64][]accesscontrol.Permission{ 1: {{Action: "teams:read", Scope: "teams:id:1"}, {Action: "teams:read", Scope: "teams:id:10"}, diff --git a/pkg/services/accesscontrol/middleware.go b/pkg/services/accesscontrol/middleware.go index 3fd16fa8807..c4bb73eee0a 100644 --- a/pkg/services/accesscontrol/middleware.go +++ b/pkg/services/accesscontrol/middleware.go @@ -80,7 +80,7 @@ func deny(c *contextmodel.ReqContext, evaluator Evaluator, err error) { if err != nil { c.Logger.Error("Error from access control system", "error", err, "accessErrorID", id) } else { - namespace, identifier := c.SignedInUser.GetNamespacedID() + namespace, identifier := c.SignedInUser.GetTypedID() c.Logger.Info( "Access denied", "namespace", namespace, diff --git a/pkg/services/anonymous/anonimpl/client.go b/pkg/services/anonymous/anonimpl/client.go index b6fd22271a1..587f1c2fd78 100644 --- a/pkg/services/anonymous/anonimpl/client.go +++ b/pkg/services/anonymous/anonimpl/client.go @@ -69,11 +69,11 @@ func (a *Anonymous) Test(ctx context.Context, r *authn.Request) bool { return true } -func (a *Anonymous) Namespace() string { - return authn.NamespaceAnonymous.String() +func (a *Anonymous) IdentityType() identity.IdentityType { + return identity.TypeAnonymous } -func (a *Anonymous) ResolveIdentity(ctx context.Context, orgID int64, namespaceID identity.NamespaceID) (*authn.Identity, error) { +func (a *Anonymous) ResolveIdentity(ctx context.Context, orgID int64, namespaceID identity.TypedID) (*authn.Identity, error) { o, err := a.orgService.GetByName(ctx, &org.GetOrgByNameQuery{Name: a.cfg.AnonymousOrgName}) if err != nil { return nil, err @@ -84,7 +84,7 @@ func (a *Anonymous) ResolveIdentity(ctx context.Context, orgID int64, namespaceI } // Anonymous identities should always have the same namespace id. - if namespaceID != authn.AnonymousNamespaceID { + if namespaceID != identity.AnonymousTypedID { return nil, errInvalidID } @@ -109,7 +109,7 @@ func (a *Anonymous) Priority() uint { func (a *Anonymous) newAnonymousIdentity(o *org.Org) *authn.Identity { return &authn.Identity{ - ID: authn.AnonymousNamespaceID, + ID: identity.AnonymousTypedID, OrgID: o.ID, OrgName: o.Name, OrgRoles: map[int64]org.RoleType{o.ID: org.RoleType(a.cfg.AnonymousOrgRole)}, diff --git a/pkg/services/anonymous/anonimpl/client_test.go b/pkg/services/anonymous/anonimpl/client_test.go index e33a36ca9fb..7a57a51cb74 100644 --- a/pkg/services/anonymous/anonimpl/client_test.go +++ b/pkg/services/anonymous/anonimpl/client_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/anonymous/anontest" "github.com/grafana/grafana/pkg/services/authn" @@ -52,17 +53,17 @@ func TestAnonymous_Authenticate(t *testing.T) { anonDeviceService: anontest.NewFakeService(), } - identity, err := c.Authenticate(context.Background(), &authn.Request{}) + user, err := c.Authenticate(context.Background(), &authn.Request{}) if err != nil { require.Error(t, err) - require.Nil(t, identity) + require.Nil(t, user) } else { require.Nil(t, err) - assert.Equal(t, authn.AnonymousNamespaceID, identity.ID) - assert.Equal(t, tt.org.ID, identity.OrgID) - assert.Equal(t, tt.org.Name, identity.OrgName) - assert.Equal(t, tt.cfg.AnonymousOrgRole, string(identity.GetOrgRole())) + assert.Equal(t, identity.AnonymousTypedID, user.ID) + assert.Equal(t, tt.org.ID, user.OrgID) + assert.Equal(t, tt.org.Name, user.OrgName) + assert.Equal(t, tt.cfg.AnonymousOrgRole, string(user.GetOrgRole())) } }) } @@ -73,7 +74,7 @@ func TestAnonymous_ResolveIdentity(t *testing.T) { desc string cfg *setting.Cfg orgID int64 - namespaceID authn.NamespaceID + namespaceID identity.TypedID org *org.Org orgErr error expectedErr error @@ -87,7 +88,7 @@ func TestAnonymous_ResolveIdentity(t *testing.T) { AnonymousOrgName: "some org", }, orgID: 1, - namespaceID: authn.AnonymousNamespaceID, + namespaceID: identity.AnonymousTypedID, expectedErr: errInvalidOrg, }, { @@ -97,7 +98,7 @@ func TestAnonymous_ResolveIdentity(t *testing.T) { AnonymousOrgName: "some org", }, orgID: 1, - namespaceID: authn.MustParseNamespaceID("anonymous:1"), + namespaceID: identity.MustParseTypedID("anonymous:1"), expectedErr: errInvalidID, }, { @@ -107,7 +108,7 @@ func TestAnonymous_ResolveIdentity(t *testing.T) { AnonymousOrgName: "some org", }, orgID: 1, - namespaceID: authn.AnonymousNamespaceID, + namespaceID: identity.AnonymousTypedID, }, } diff --git a/pkg/services/apiserver/storage/entity/test/watch_test.go b/pkg/services/apiserver/storage/entity/test/watch_test.go index d826895a226..e1eabf5cb9e 100644 --- a/pkg/services/apiserver/storage/entity/test/watch_test.go +++ b/pkg/services/apiserver/storage/entity/test/watch_test.go @@ -147,7 +147,7 @@ func testSetup(t *testing.T, opts ...setupOption) (context.Context, storage.Inte // Test with an admin identity ctx := identity.WithRequester(context.Background(), &identity.StaticRequester{ - Namespace: identity.NamespaceUser, + Type: identity.TypeUser, Login: "testuser", UserID: 123, UserUID: "u123", diff --git a/pkg/services/auth/idimpl/service.go b/pkg/services/auth/idimpl/service.go index 3eb000e74b4..8fdde765255 100644 --- a/pkg/services/auth/idimpl/service.go +++ b/pkg/services/auth/idimpl/service.go @@ -66,7 +66,7 @@ func (s *Service) SignIdentity(ctx context.Context, id identity.Requester) (stri cacheKey := prefixCacheKey(id.GetCacheKey()) result, err, _ := s.si.Do(cacheKey, func() (interface{}, error) { - namespace, identifier := id.GetNamespacedID() + namespace, identifier := id.GetTypedID() cachedToken, err := s.cache.Get(ctx, cacheKey) if err == nil { @@ -92,7 +92,7 @@ func (s *Service) SignIdentity(ctx context.Context, id identity.Requester) (stri }, } - if identity.IsNamespace(namespace, identity.NamespaceUser) { + if identity.IsIdentityType(namespace, identity.TypeUser) { claims.Rest.Email = id.GetEmail() claims.Rest.EmailVerified = id.IsEmailVerified() claims.Rest.AuthenticatedBy = id.GetAuthenticatedBy() @@ -144,7 +144,7 @@ func (s *Service) hook(ctx context.Context, identity *authn.Identity, _ *authn.R token, err := s.SignIdentity(ctx, identity) if err != nil { if shouldLogErr(err) { - namespace, id := identity.GetNamespacedID() + namespace, id := identity.GetTypedID() s.logger.FromContext(ctx).Error("Failed to sign id token", "err", err, "namespace", namespace, "id", id) } // for now don't return error so we don't break authentication from this hook diff --git a/pkg/services/auth/idimpl/service_test.go b/pkg/services/auth/idimpl/service_test.go index a4dbdee0407..6b619b5075f 100644 --- a/pkg/services/auth/idimpl/service_test.go +++ b/pkg/services/auth/idimpl/service_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/remotecache" "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/auth/idtest" @@ -69,7 +70,7 @@ func TestService_SignIdentity(t *testing.T) { featuremgmt.WithFeatures(featuremgmt.FlagIdForwarding), &authntest.FakeService{}, nil, ) - token, err := s.SignIdentity(context.Background(), &authn.Identity{ID: authn.MustParseNamespaceID("user:1")}) + token, err := s.SignIdentity(context.Background(), &authn.Identity{ID: identity.MustParseTypedID("user:1")}) require.NoError(t, err) require.NotEmpty(t, token) }) @@ -81,10 +82,10 @@ func TestService_SignIdentity(t *testing.T) { &authntest.FakeService{}, nil, ) token, err := s.SignIdentity(context.Background(), &authn.Identity{ - ID: authn.MustParseNamespaceID("user:1"), + ID: identity.MustParseTypedID("user:1"), AuthenticatedBy: login.AzureADAuthModule, Login: "U1", - UID: authn.NewNamespaceIDString(authn.NamespaceUser, "edpu3nnt61se8e")}) + UID: identity.NewTypedIDString(identity.TypeUser, "edpu3nnt61se8e")}) require.NoError(t, err) parsed, err := jwt.ParseSigned(token) diff --git a/pkg/services/authn/authn.go b/pkg/services/authn/authn.go index 87e3491dd0b..af7e20923db 100644 --- a/pkg/services/authn/authn.go +++ b/pkg/services/authn/authn.go @@ -95,7 +95,7 @@ type Service interface { // RegisterPreLogoutHook registers a hook that is called before a logout request. RegisterPreLogoutHook(hook PreLogoutHookFn, priority uint) // ResolveIdentity resolves an identity from org and namespace id. - ResolveIdentity(ctx context.Context, orgID int64, namespaceID NamespaceID) (*Identity, error) + ResolveIdentity(ctx context.Context, orgID int64, namespaceID identity.TypedID) (*Identity, error) // RegisterClient will register a new authn.Client that can be used for authentication RegisterClient(c Client) @@ -157,7 +157,7 @@ type RedirectClient interface { // that should happen during logout and supports client specific redirect URL. type LogoutClient interface { Client - Logout(ctx context.Context, user Requester) (*Redirect, bool) + Logout(ctx context.Context, user identity.Requester) (*Redirect, bool) } type PasswordClient interface { @@ -179,8 +179,8 @@ type UsageStatClient interface { // Clients that implements this interface can resolve an full identity from an orgID and namespaceID. type IdentityResolverClient interface { Client - Namespace() string - ResolveIdentity(ctx context.Context, orgID int64, namespaceID NamespaceID) (*Identity, error) + IdentityType() identity.IdentityType + ResolveIdentity(ctx context.Context, orgID int64, namespaceID identity.TypedID) (*Identity, error) } type Request struct { diff --git a/pkg/services/authn/authnimpl/service.go b/pkg/services/authn/authnimpl/service.go index 18bfab199a2..865ad9dca19 100644 --- a/pkg/services/authn/authnimpl/service.go +++ b/pkg/services/authn/authnimpl/service.go @@ -13,6 +13,7 @@ import ( "go.opentelemetry.io/otel/trace" "github.com/grafana/grafana/pkg/apimachinery/errutil" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/network" "github.com/grafana/grafana/pkg/infra/tracing" @@ -216,9 +217,9 @@ func (s *Service) Login(ctx context.Context, client string, r *authn.Request) (i } // Login is only supported for users - if !id.ID.IsNamespace(authn.NamespaceUser) { + if !id.ID.IsType(identity.TypeUser) { s.metrics.failedLogin.WithLabelValues(client).Inc() - return nil, authn.ErrUnsupportedIdentity.Errorf("expected identity of type user but got: %s", id.ID.Namespace()) + return nil, authn.ErrUnsupportedIdentity.Errorf("expected identity of type user but got: %s", id.ID.Type()) } userID, err := id.ID.ParseInt() @@ -271,7 +272,7 @@ func (s *Service) RegisterPreLogoutHook(hook authn.PreLogoutHookFn, priority uin s.preLogoutHooks.insert(hook, priority) } -func (s *Service) Logout(ctx context.Context, user authn.Requester, sessionToken *auth.UserToken) (*authn.Redirect, error) { +func (s *Service) Logout(ctx context.Context, user identity.Requester, sessionToken *auth.UserToken) (*authn.Redirect, error) { ctx, span := s.tracer.Start(ctx, "authn.Logout") defer span.End() @@ -280,7 +281,7 @@ func (s *Service) Logout(ctx context.Context, user authn.Requester, sessionToken redirect.URL = s.cfg.SignoutRedirectUrl } - if !user.GetID().IsNamespace(authn.NamespaceUser) { + if !user.GetID().IsType(identity.TypeUser) { return redirect, nil } @@ -327,7 +328,7 @@ Default: return redirect, nil } -func (s *Service) ResolveIdentity(ctx context.Context, orgID int64, namespaceID authn.NamespaceID) (*authn.Identity, error) { +func (s *Service) ResolveIdentity(ctx context.Context, orgID int64, namespaceID identity.TypedID) (*authn.Identity, error) { ctx, span := s.tracer.Start(ctx, "authn.ResolveIdentity") defer span.End() @@ -352,7 +353,7 @@ func (s *Service) RegisterClient(c authn.Client) { } if rc, ok := c.(authn.IdentityResolverClient); ok { - s.idenityResolverClients[rc.Namespace()] = rc + s.idenityResolverClients[rc.IdentityType().String()] = rc } } @@ -375,11 +376,11 @@ func (s *Service) SyncIdentity(ctx context.Context, identity *authn.Identity) er return s.runPostAuthHooks(ctx, identity, r) } -func (s *Service) resolveIdenity(ctx context.Context, orgID int64, namespaceID authn.NamespaceID) (*authn.Identity, error) { +func (s *Service) resolveIdenity(ctx context.Context, orgID int64, namespaceID identity.TypedID) (*authn.Identity, error) { ctx, span := s.tracer.Start(ctx, "authn.resolveIdentity") defer span.End() - if namespaceID.IsNamespace(authn.NamespaceUser) { + if namespaceID.IsType(identity.TypeUser) { return &authn.Identity{ OrgID: orgID, ID: namespaceID, @@ -390,7 +391,7 @@ func (s *Service) resolveIdenity(ctx context.Context, orgID int64, namespaceID a }}, nil } - if namespaceID.IsNamespace(authn.NamespaceServiceAccount) { + if namespaceID.IsType(identity.TypeServiceAccount) { return &authn.Identity{ ID: namespaceID, OrgID: orgID, @@ -401,9 +402,9 @@ func (s *Service) resolveIdenity(ctx context.Context, orgID int64, namespaceID a }}, nil } - resolver, ok := s.idenityResolverClients[namespaceID.Namespace().String()] + resolver, ok := s.idenityResolverClients[string(namespaceID.Type())] if !ok { - return nil, authn.ErrUnsupportedIdentity.Errorf("no resolver for : %s", namespaceID.Namespace()) + return nil, authn.ErrUnsupportedIdentity.Errorf("no resolver for : %s", namespaceID.Type()) } return resolver.ResolveIdentity(ctx, orgID, namespaceID) } diff --git a/pkg/services/authn/authnimpl/service_test.go b/pkg/services/authn/authnimpl/service_test.go index 9dc1ebedd17..ca34c134b4d 100644 --- a/pkg/services/authn/authnimpl/service_test.go +++ b/pkg/services/authn/authnimpl/service_test.go @@ -44,9 +44,9 @@ func TestService_Authenticate(t *testing.T) { { desc: "should succeed with authentication for configured client", clients: []authn.Client{ - &authntest.FakeClient{ExpectedTest: true, ExpectedIdentity: &authn.Identity{ID: authn.MustParseNamespaceID("user:1")}}, + &authntest.FakeClient{ExpectedTest: true, ExpectedIdentity: &authn.Identity{ID: identity.MustParseTypedID("user:1")}}, }, - expectedIdentity: &authn.Identity{ID: authn.MustParseNamespaceID("user:1")}, + expectedIdentity: &authn.Identity{ID: identity.MustParseTypedID("user:1")}, }, { desc: "should succeed with authentication for configured client for identity with fetch permissions params", @@ -54,7 +54,7 @@ func TestService_Authenticate(t *testing.T) { &authntest.FakeClient{ ExpectedTest: true, ExpectedIdentity: &authn.Identity{ - ID: authn.MustParseNamespaceID("user:2"), + ID: identity.MustParseTypedID("user:2"), ClientParams: authn.ClientParams{ FetchPermissionsParams: authn.FetchPermissionsParams{ ActionsLookup: []string{ @@ -70,7 +70,7 @@ func TestService_Authenticate(t *testing.T) { }, }, expectedIdentity: &authn.Identity{ - ID: authn.MustParseNamespaceID("user:2"), + ID: identity.MustParseTypedID("user:2"), ClientParams: authn.ClientParams{ FetchPermissionsParams: authn.FetchPermissionsParams{ ActionsLookup: []string{ @@ -92,19 +92,19 @@ func TestService_Authenticate(t *testing.T) { ExpectedName: "2", ExpectedPriority: 2, ExpectedTest: true, - ExpectedIdentity: &authn.Identity{ID: authn.MustParseNamespaceID("user:2"), AuthID: "service:some-service", AuthenticatedBy: "service_auth"}, + ExpectedIdentity: &authn.Identity{ID: identity.MustParseTypedID("user:2"), AuthID: "service:some-service", AuthenticatedBy: "service_auth"}, }, }, - expectedIdentity: &authn.Identity{ID: authn.MustParseNamespaceID("user:2"), AuthID: "service:some-service", AuthenticatedBy: "service_auth"}, + expectedIdentity: &authn.Identity{ID: identity.MustParseTypedID("user:2"), AuthID: "service:some-service", AuthenticatedBy: "service_auth"}, }, { desc: "should succeed with authentication for third client when error happened in first", clients: []authn.Client{ &authntest.FakeClient{ExpectedName: "1", ExpectedPriority: 2, ExpectedTest: false}, &authntest.FakeClient{ExpectedName: "2", ExpectedPriority: 1, ExpectedTest: true, ExpectedErr: errors.New("some error")}, - &authntest.FakeClient{ExpectedName: "3", ExpectedPriority: 3, ExpectedTest: true, ExpectedIdentity: &authn.Identity{ID: authn.MustParseNamespaceID("user:3")}}, + &authntest.FakeClient{ExpectedName: "3", ExpectedPriority: 3, ExpectedTest: true, ExpectedIdentity: &authn.Identity{ID: identity.MustParseTypedID("user:3")}}, }, - expectedIdentity: &authn.Identity{ID: authn.MustParseNamespaceID("user:3")}, + expectedIdentity: &authn.Identity{ID: identity.MustParseTypedID("user:3")}, }, { desc: "should return error when no client could authenticate the request", @@ -315,10 +315,10 @@ func TestService_Login(t *testing.T) { client: "fake", expectedClientOK: true, expectedClientIdentity: &authn.Identity{ - ID: authn.MustParseNamespaceID("user:1"), + ID: identity.MustParseTypedID("user:1"), }, expectedIdentity: &authn.Identity{ - ID: authn.MustParseNamespaceID("user:1"), + ID: identity.MustParseTypedID("user:1"), SessionToken: &auth.UserToken{UserId: 1}, }, }, @@ -331,7 +331,7 @@ func TestService_Login(t *testing.T) { desc: "should not login non user identity", client: "fake", expectedClientOK: true, - expectedClientIdentity: &authn.Identity{ID: authn.MustParseNamespaceID("api-key:1")}, + expectedClientIdentity: &authn.Identity{ID: identity.MustParseTypedID("api-key:1")}, expectedErr: authn.ErrUnsupportedIdentity, }, } @@ -420,31 +420,31 @@ func TestService_Logout(t *testing.T) { tests := []TestCase{ { desc: "should redirect to default redirect url when identity is not a user", - identity: &authn.Identity{ID: authn.NewNamespaceID(authn.NamespaceServiceAccount, 1)}, + identity: &authn.Identity{ID: identity.NewTypedID(identity.TypeServiceAccount, 1)}, expectedRedirect: &authn.Redirect{URL: "http://localhost:3000/login"}, }, { desc: "should redirect to default redirect url when no external provider was used to authenticate", - identity: &authn.Identity{ID: authn.NewNamespaceID(authn.NamespaceUser, 1)}, + identity: &authn.Identity{ID: identity.NewTypedID(identity.TypeUser, 1)}, expectedRedirect: &authn.Redirect{URL: "http://localhost:3000/login"}, expectedTokenRevoked: true, }, { desc: "should redirect to default redirect url when client is not found", - identity: &authn.Identity{ID: authn.NewNamespaceID(authn.NamespaceUser, 1), AuthenticatedBy: "notfound"}, + identity: &authn.Identity{ID: identity.NewTypedID(identity.TypeUser, 1), AuthenticatedBy: "notfound"}, expectedRedirect: &authn.Redirect{URL: "http://localhost:3000/login"}, expectedTokenRevoked: true, }, { desc: "should redirect to default redirect url when client do not implement logout extension", - identity: &authn.Identity{ID: authn.NewNamespaceID(authn.NamespaceUser, 1), AuthenticatedBy: "azuread"}, + identity: &authn.Identity{ID: identity.NewTypedID(identity.TypeUser, 1), AuthenticatedBy: "azuread"}, expectedRedirect: &authn.Redirect{URL: "http://localhost:3000/login"}, client: &authntest.FakeClient{ExpectedName: "auth.client.azuread"}, expectedTokenRevoked: true, }, { desc: "should use signout redirect url if configured", - identity: &authn.Identity{ID: authn.NewNamespaceID(authn.NamespaceUser, 1), AuthenticatedBy: "azuread"}, + identity: &authn.Identity{ID: identity.NewTypedID(identity.TypeUser, 1), AuthenticatedBy: "azuread"}, expectedRedirect: &authn.Redirect{URL: "some-url"}, client: &authntest.FakeClient{ExpectedName: "auth.client.azuread"}, signoutRedirectURL: "some-url", @@ -452,7 +452,7 @@ func TestService_Logout(t *testing.T) { }, { desc: "should redirect to client specific url", - identity: &authn.Identity{ID: authn.NewNamespaceID(authn.NamespaceUser, 1), AuthenticatedBy: "azuread"}, + identity: &authn.Identity{ID: identity.NewTypedID(identity.TypeUser, 1), AuthenticatedBy: "azuread"}, expectedRedirect: &authn.Redirect{URL: "http://idp.com/logout"}, client: &authntest.MockClient{ NameFunc: func() string { return "auth.client.azuread" }, @@ -500,26 +500,26 @@ func TestService_Logout(t *testing.T) { func TestService_ResolveIdentity(t *testing.T) { t.Run("should return error for for unknown namespace", func(t *testing.T) { svc := setupTests(t) - _, err := svc.ResolveIdentity(context.Background(), 1, authn.NewNamespaceID("some", 1)) + _, err := svc.ResolveIdentity(context.Background(), 1, identity.NewTypedID("some", 1)) assert.ErrorIs(t, err, authn.ErrUnsupportedIdentity) }) t.Run("should return error for for namespace that don't have a resolver", func(t *testing.T) { svc := setupTests(t) - _, err := svc.ResolveIdentity(context.Background(), 1, authn.MustParseNamespaceID("api-key:1")) + _, err := svc.ResolveIdentity(context.Background(), 1, identity.MustParseTypedID("api-key:1")) assert.ErrorIs(t, err, authn.ErrUnsupportedIdentity) }) t.Run("should resolve for user", func(t *testing.T) { svc := setupTests(t) - identity, err := svc.ResolveIdentity(context.Background(), 1, authn.MustParseNamespaceID("user:1")) + identity, err := svc.ResolveIdentity(context.Background(), 1, identity.MustParseTypedID("user:1")) assert.NoError(t, err) assert.NotNil(t, identity) }) t.Run("should resolve for service account", func(t *testing.T) { svc := setupTests(t) - identity, err := svc.ResolveIdentity(context.Background(), 1, authn.MustParseNamespaceID("service-account:1")) + identity, err := svc.ResolveIdentity(context.Background(), 1, identity.MustParseTypedID("service-account:1")) assert.NoError(t, err) assert.NotNil(t, identity) }) @@ -527,14 +527,14 @@ func TestService_ResolveIdentity(t *testing.T) { t.Run("should resolve for valid namespace if client is registered", func(t *testing.T) { svc := setupTests(t, func(svc *Service) { svc.RegisterClient(&authntest.MockClient{ - NamespaceFunc: func() string { return authn.NamespaceAPIKey.String() }, - ResolveIdentityFunc: func(ctx context.Context, orgID int64, namespaceID authn.NamespaceID) (*authn.Identity, error) { + IdentityTypeFunc: func() identity.IdentityType { return identity.TypeAPIKey }, + ResolveIdentityFunc: func(ctx context.Context, orgID int64, namespaceID identity.TypedID) (*authn.Identity, error) { return &authn.Identity{}, nil }, }) }) - identity, err := svc.ResolveIdentity(context.Background(), 1, authn.MustParseNamespaceID("api-key:1")) + identity, err := svc.ResolveIdentity(context.Background(), 1, identity.MustParseTypedID("api-key:1")) assert.NoError(t, err) assert.NotNil(t, identity) }) diff --git a/pkg/services/authn/authnimpl/sync/oauth_token_sync.go b/pkg/services/authn/authnimpl/sync/oauth_token_sync.go index 1a3891570ab..8262d290d93 100644 --- a/pkg/services/authn/authnimpl/sync/oauth_token_sync.go +++ b/pkg/services/authn/authnimpl/sync/oauth_token_sync.go @@ -8,6 +8,7 @@ import ( "golang.org/x/sync/singleflight" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/login/social" @@ -36,42 +37,42 @@ type OAuthTokenSync struct { tracer tracing.Tracer } -func (s *OAuthTokenSync) SyncOauthTokenHook(ctx context.Context, identity *authn.Identity, _ *authn.Request) error { +func (s *OAuthTokenSync) SyncOauthTokenHook(ctx context.Context, id *authn.Identity, _ *authn.Request) error { ctx, span := s.tracer.Start(ctx, "oauth.sync.SyncOauthTokenHook") defer span.End() // only perform oauth token check if identity is a user - if !identity.ID.IsNamespace(authn.NamespaceUser) { + if !id.ID.IsType(identity.TypeUser) { return nil } // Not authenticated through session tokens, so we can skip this hook. - if identity.SessionToken == nil { + if id.SessionToken == nil { return nil } // Not authenticated with a oauth provider, so we can skip this hook. - if !strings.HasPrefix(identity.GetAuthenticatedBy(), "oauth") { + if !strings.HasPrefix(id.GetAuthenticatedBy(), "oauth") { return nil } - ctxLogger := s.log.FromContext(ctx).New("userID", identity.ID.ID()) + ctxLogger := s.log.FromContext(ctx).New("userID", id.ID.ID()) - _, err, _ := s.singleflightGroup.Do(identity.ID.String(), func() (interface{}, error) { + _, err, _ := s.singleflightGroup.Do(id.ID.String(), func() (interface{}, error) { ctxLogger.Debug("Singleflight request for OAuth token sync") // FIXME: Consider using context.WithoutCancel instead of context.Background after Go 1.21 update updateCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() - if refreshErr := s.service.TryTokenRefresh(updateCtx, identity); refreshErr != nil { + if refreshErr := s.service.TryTokenRefresh(updateCtx, id); refreshErr != nil { if errors.Is(refreshErr, context.Canceled) { return nil, nil } - token, _, err := s.service.HasOAuthEntry(ctx, identity) + token, _, err := s.service.HasOAuthEntry(ctx, id) if err != nil { - ctxLogger.Error("Failed to get OAuth entry for verifying if token has already been refreshed", "id", identity.ID, "error", err) + ctxLogger.Error("Failed to get OAuth entry for verifying if token has already been refreshed", "id", id.ID, "error", err) return nil, err } @@ -81,14 +82,14 @@ func (s *OAuthTokenSync) SyncOauthTokenHook(ctx context.Context, identity *authn return nil, nil } - ctxLogger.Error("Failed to refresh OAuth access token", "id", identity.ID, "error", refreshErr) + ctxLogger.Error("Failed to refresh OAuth access token", "id", id.ID, "error", refreshErr) if err := s.service.InvalidateOAuthTokens(ctx, token); err != nil { - ctxLogger.Warn("Failed to invalidate OAuth tokens", "id", identity.ID, "error", err) + ctxLogger.Warn("Failed to invalidate OAuth tokens", "id", id.ID, "error", err) } - if err := s.sessionService.RevokeToken(ctx, identity.SessionToken, false); err != nil { - ctxLogger.Warn("Failed to revoke session token", "id", identity.ID, "tokenId", identity.SessionToken.Id, "error", err) + if err := s.sessionService.RevokeToken(ctx, id.SessionToken, false); err != nil { + ctxLogger.Warn("Failed to revoke session token", "id", id.ID, "tokenId", id.SessionToken.Id, "error", err) } return nil, refreshErr diff --git a/pkg/services/authn/authnimpl/sync/oauth_token_sync_test.go b/pkg/services/authn/authnimpl/sync/oauth_token_sync_test.go index 41722d4344e..326a8d1eea7 100644 --- a/pkg/services/authn/authnimpl/sync/oauth_token_sync_test.go +++ b/pkg/services/authn/authnimpl/sync/oauth_token_sync_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/assert" "golang.org/x/sync/singleflight" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/login/social" @@ -41,17 +42,17 @@ func TestOAuthTokenSync_SyncOAuthTokenHook(t *testing.T) { tests := []testCase{ { desc: "should skip sync when identity is not a user", - identity: &authn.Identity{ID: authn.MustParseNamespaceID("service-account:1")}, + identity: &authn.Identity{ID: identity.MustParseTypedID("service-account:1")}, expectTryRefreshTokenCalled: false, }, { desc: "should skip sync when identity is a user but is not authenticated with session token", - identity: &authn.Identity{ID: authn.MustParseNamespaceID("user:1")}, + identity: &authn.Identity{ID: identity.MustParseTypedID("user:1")}, expectTryRefreshTokenCalled: false, }, { desc: "should invalidate access token and session token if token refresh fails", - identity: &authn.Identity{ID: authn.MustParseNamespaceID("user:1"), SessionToken: &auth.UserToken{}, AuthenticatedBy: login.AzureADAuthModule}, + identity: &authn.Identity{ID: identity.MustParseTypedID("user:1"), SessionToken: &auth.UserToken{}, AuthenticatedBy: login.AzureADAuthModule}, expectHasEntryCalled: true, expectedTryRefreshErr: errors.New("some err"), expectTryRefreshTokenCalled: true, @@ -62,7 +63,7 @@ func TestOAuthTokenSync_SyncOAuthTokenHook(t *testing.T) { }, { desc: "should refresh the token successfully", - identity: &authn.Identity{ID: authn.MustParseNamespaceID("user:1"), SessionToken: &auth.UserToken{}, AuthenticatedBy: login.AzureADAuthModule}, + identity: &authn.Identity{ID: identity.MustParseTypedID("user:1"), SessionToken: &auth.UserToken{}, AuthenticatedBy: login.AzureADAuthModule}, expectHasEntryCalled: false, expectTryRefreshTokenCalled: true, expectInvalidateOauthTokensCalled: false, @@ -70,7 +71,7 @@ func TestOAuthTokenSync_SyncOAuthTokenHook(t *testing.T) { }, { desc: "should not invalidate the token if the token has already been refreshed by another request (singleflight)", - identity: &authn.Identity{ID: authn.MustParseNamespaceID("user:1"), SessionToken: &auth.UserToken{}, AuthenticatedBy: login.AzureADAuthModule}, + identity: &authn.Identity{ID: identity.MustParseTypedID("user:1"), SessionToken: &auth.UserToken{}, AuthenticatedBy: login.AzureADAuthModule}, expectHasEntryCalled: true, expectTryRefreshTokenCalled: true, expectInvalidateOauthTokensCalled: false, @@ -92,7 +93,7 @@ func TestOAuthTokenSync_SyncOAuthTokenHook(t *testing.T) { ) service := &oauthtokentest.MockOauthTokenService{ - HasOAuthEntryFunc: func(ctx context.Context, usr authn.Requester) (*login.UserAuth, bool, error) { + HasOAuthEntryFunc: func(ctx context.Context, usr identity.Requester) (*login.UserAuth, bool, error) { hasEntryCalled = true return tt.expectedHasEntryToken, tt.expectedHasEntryToken != nil, nil }, @@ -100,7 +101,7 @@ func TestOAuthTokenSync_SyncOAuthTokenHook(t *testing.T) { invalidateTokensCalled = true return nil }, - TryTokenRefreshFunc: func(ctx context.Context, usr authn.Requester) error { + TryTokenRefreshFunc: func(ctx context.Context, usr identity.Requester) error { tryRefreshCalled = true return tt.expectedTryRefreshErr }, diff --git a/pkg/services/authn/authnimpl/sync/org_sync.go b/pkg/services/authn/authnimpl/sync/org_sync.go index c3ef49343a6..9fc5b1d3e7c 100644 --- a/pkg/services/authn/authnimpl/sync/org_sync.go +++ b/pkg/services/authn/authnimpl/sync/org_sync.go @@ -6,6 +6,7 @@ import ( "fmt" "sort" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" @@ -38,14 +39,14 @@ func (s *OrgSync) SyncOrgRolesHook(ctx context.Context, id *authn.Identity, _ *a ctxLogger := s.log.FromContext(ctx).New("id", id.ID, "login", id.Login) - if !id.ID.IsNamespace(authn.NamespaceUser) { - ctxLogger.Warn("Failed to sync org role, invalid namespace for identity", "namespace", id.ID.Namespace()) + if !id.ID.IsType(identity.TypeUser) { + ctxLogger.Warn("Failed to sync org role, invalid namespace for identity", "type", id.ID.Type()) return nil } userID, err := id.ID.ParseInt() if err != nil { - ctxLogger.Warn("Failed to sync org role, invalid ID for identity", "namespace", id.ID.Namespace(), "err", err) + ctxLogger.Warn("Failed to sync org role, invalid ID for identity", "type", id.ID.Type(), "err", err) return nil } @@ -144,14 +145,14 @@ func (s *OrgSync) SetDefaultOrgHook(ctx context.Context, currentIdentity *authn. ctxLogger := s.log.FromContext(ctx) - if !currentIdentity.ID.IsNamespace(authn.NamespaceUser) { - ctxLogger.Debug("Skipping default org sync, not a user", "namespace", currentIdentity.ID.Namespace()) + if !currentIdentity.ID.IsType(identity.TypeUser) { + ctxLogger.Debug("Skipping default org sync, not a user", "type", currentIdentity.ID.Type()) return } userID, err := currentIdentity.ID.ParseInt() if err != nil { - ctxLogger.Debug("Skipping default org sync, invalid ID for identity", "id", currentIdentity.ID, "namespace", currentIdentity.ID.Namespace(), "err", err) + ctxLogger.Debug("Skipping default org sync, invalid ID for identity", "id", currentIdentity.ID, "type", currentIdentity.ID.Type(), "err", err) return } diff --git a/pkg/services/authn/authnimpl/sync/org_sync_test.go b/pkg/services/authn/authnimpl/sync/org_sync_test.go index e03d4b8b0e2..b6d14096783 100644 --- a/pkg/services/authn/authnimpl/sync/org_sync_test.go +++ b/pkg/services/authn/authnimpl/sync/org_sync_test.go @@ -76,7 +76,7 @@ func TestOrgSync_SyncOrgRolesHook(t *testing.T) { args: args{ ctx: context.Background(), id: &authn.Identity{ - ID: authn.MustParseNamespaceID("user:1"), + ID: identity.MustParseTypedID("user:1"), Login: "test", Name: "test", Email: "test", @@ -92,7 +92,7 @@ func TestOrgSync_SyncOrgRolesHook(t *testing.T) { }, }, wantID: &authn.Identity{ - ID: authn.MustParseNamespaceID("user:1"), + ID: identity.MustParseTypedID("user:1"), Login: "test", Name: "test", Email: "test", @@ -139,7 +139,7 @@ func TestOrgSync_SetDefaultOrgHook(t *testing.T) { { name: "should set default org", defaultOrgSetting: 2, - identity: &authn.Identity{ID: authn.MustParseNamespaceID("user:1")}, + identity: &authn.Identity{ID: identity.MustParseTypedID("user:1")}, setupMock: func(userService *usertest.MockService, orgService *orgtest.FakeOrgService) { userService.On("Update", mock.Anything, mock.MatchedBy(func(cmd *user.UpdateUserCommand) bool { return cmd.UserID == 1 && *cmd.OrgID == 2 @@ -149,7 +149,7 @@ func TestOrgSync_SetDefaultOrgHook(t *testing.T) { { name: "should skip setting the default org when default org is not set", defaultOrgSetting: -1, - identity: &authn.Identity{ID: authn.MustParseNamespaceID("user:1")}, + identity: &authn.Identity{ID: identity.MustParseTypedID("user:1")}, }, { name: "should skip setting the default org when identity is nil", @@ -159,28 +159,28 @@ func TestOrgSync_SetDefaultOrgHook(t *testing.T) { { name: "should skip setting the default org when input err is not nil", defaultOrgSetting: 2, - identity: &authn.Identity{ID: authn.MustParseNamespaceID("user:1")}, + identity: &authn.Identity{ID: identity.MustParseTypedID("user:1")}, inputErr: fmt.Errorf("error"), }, { name: "should skip setting the default org when identity is not a user", defaultOrgSetting: 2, - identity: &authn.Identity{ID: authn.MustParseNamespaceID("service-account:1")}, + identity: &authn.Identity{ID: identity.MustParseTypedID("service-account:1")}, }, { name: "should skip setting the default org when user id is not valid", defaultOrgSetting: 2, - identity: &authn.Identity{ID: authn.MustParseNamespaceID("user:invalid")}, + identity: &authn.Identity{ID: identity.MustParseTypedID("user:invalid")}, }, { name: "should skip setting the default org when user is not allowed to use the configured default org", defaultOrgSetting: 3, - identity: &authn.Identity{ID: authn.MustParseNamespaceID("user:1")}, + identity: &authn.Identity{ID: identity.MustParseTypedID("user:1")}, }, { name: "should skip setting the default org when validateUsingOrg returns error", defaultOrgSetting: 2, - identity: &authn.Identity{ID: authn.MustParseNamespaceID("user:1")}, + identity: &authn.Identity{ID: identity.MustParseTypedID("user:1")}, setupMock: func(userService *usertest.MockService, orgService *orgtest.FakeOrgService) { orgService.ExpectedError = fmt.Errorf("error") }, @@ -188,7 +188,7 @@ func TestOrgSync_SetDefaultOrgHook(t *testing.T) { { name: "should skip the hook when the user org update was unsuccessful", defaultOrgSetting: 2, - identity: &authn.Identity{ID: authn.MustParseNamespaceID("user:1")}, + identity: &authn.Identity{ID: identity.MustParseTypedID("user:1")}, setupMock: func(userService *usertest.MockService, orgService *orgtest.FakeOrgService) { userService.On("Update", mock.Anything, mock.Anything).Return(fmt.Errorf("error")) }, diff --git a/pkg/services/authn/authnimpl/sync/rbac_sync.go b/pkg/services/authn/authnimpl/sync/rbac_sync.go index 198dc020a7d..b1a3542af6d 100644 --- a/pkg/services/authn/authnimpl/sync/rbac_sync.go +++ b/pkg/services/authn/authnimpl/sync/rbac_sync.go @@ -5,6 +5,7 @@ import ( "errors" "github.com/grafana/grafana/pkg/apimachinery/errutil" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" @@ -146,7 +147,7 @@ func (s *RBACSync) SyncCloudRoles(ctx context.Context, ident *authn.Identity, r return nil } - if !ident.ID.IsNamespace(authn.NamespaceUser) { + if !ident.ID.IsType(identity.TypeUser) { s.log.FromContext(ctx).Debug("Skip syncing cloud role", "id", ident.ID) return nil } diff --git a/pkg/services/authn/authnimpl/sync/rbac_sync_test.go b/pkg/services/authn/authnimpl/sync/rbac_sync_test.go index f69d6e52a32..4035565d8f9 100644 --- a/pkg/services/authn/authnimpl/sync/rbac_sync_test.go +++ b/pkg/services/authn/authnimpl/sync/rbac_sync_test.go @@ -4,6 +4,10 @@ import ( "context" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" @@ -11,8 +15,6 @@ import ( "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func TestRBACSync_SyncPermission(t *testing.T) { @@ -24,14 +26,14 @@ func TestRBACSync_SyncPermission(t *testing.T) { testCases := []testCase{ { name: "enriches the identity successfully when SyncPermissions is true", - identity: &authn.Identity{ID: authn.MustParseNamespaceID("user:2"), OrgID: 1, ClientParams: authn.ClientParams{SyncPermissions: true}}, + identity: &authn.Identity{ID: identity.MustParseTypedID("user:2"), OrgID: 1, ClientParams: authn.ClientParams{SyncPermissions: true}}, expectedPermissions: []accesscontrol.Permission{ {Action: accesscontrol.ActionUsersRead}, }, }, { name: "does not load the permissions when SyncPermissions is false", - identity: &authn.Identity{ID: authn.MustParseNamespaceID("user:2"), OrgID: 1, ClientParams: authn.ClientParams{SyncPermissions: true}}, + identity: &authn.Identity{ID: identity.MustParseTypedID("user:2"), OrgID: 1, ClientParams: authn.ClientParams{SyncPermissions: true}}, expectedPermissions: []accesscontrol.Permission{ {Action: accesscontrol.ActionUsersRead}, }, @@ -65,7 +67,7 @@ func TestRBACSync_SyncCloudRoles(t *testing.T) { desc: "should call sync when authenticated with grafana com and has viewer role", module: login.GrafanaComAuthModule, identity: &authn.Identity{ - ID: authn.NewNamespaceID(authn.NamespaceUser, 1), + ID: identity.NewTypedID(identity.TypeUser, 1), OrgID: 1, OrgRoles: map[int64]org.RoleType{1: org.RoleViewer}, }, @@ -76,7 +78,7 @@ func TestRBACSync_SyncCloudRoles(t *testing.T) { desc: "should call sync when authenticated with grafana com and has editor role", module: login.GrafanaComAuthModule, identity: &authn.Identity{ - ID: authn.NewNamespaceID(authn.NamespaceUser, 1), + ID: identity.NewTypedID(identity.TypeUser, 1), OrgID: 1, OrgRoles: map[int64]org.RoleType{1: org.RoleEditor}, }, @@ -87,7 +89,7 @@ func TestRBACSync_SyncCloudRoles(t *testing.T) { desc: "should call sync when authenticated with grafana com and has admin role", module: login.GrafanaComAuthModule, identity: &authn.Identity{ - ID: authn.NewNamespaceID(authn.NamespaceUser, 1), + ID: identity.NewTypedID(identity.TypeUser, 1), OrgID: 1, OrgRoles: map[int64]org.RoleType{1: org.RoleAdmin}, }, @@ -98,7 +100,7 @@ func TestRBACSync_SyncCloudRoles(t *testing.T) { desc: "should not call sync when authenticated with grafana com and has invalid role", module: login.GrafanaComAuthModule, identity: &authn.Identity{ - ID: authn.NewNamespaceID(authn.NamespaceUser, 1), + ID: identity.NewTypedID(identity.TypeUser, 1), OrgID: 1, OrgRoles: map[int64]org.RoleType{1: org.RoleType("something else")}, }, @@ -109,7 +111,7 @@ func TestRBACSync_SyncCloudRoles(t *testing.T) { desc: "should not call sync when not authenticated with grafana com", module: login.LDAPAuthModule, identity: &authn.Identity{ - ID: authn.NewNamespaceID(authn.NamespaceUser, 1), + ID: identity.NewTypedID(identity.TypeUser, 1), OrgID: 1, OrgRoles: map[int64]org.RoleType{1: org.RoleAdmin}, }, @@ -155,7 +157,7 @@ func TestRBACSync_cloudRolesToAddAndRemove(t *testing.T) { { desc: "should map Cloud Viewer to Grafana Cloud Viewer and Support ticket reader", identity: &authn.Identity{ - ID: authn.NewNamespaceID(authn.NamespaceUser, 1), + ID: identity.NewTypedID(identity.TypeUser, 1), OrgID: 1, OrgRoles: map[int64]org.RoleType{1: org.RoleViewer}, }, @@ -174,7 +176,7 @@ func TestRBACSync_cloudRolesToAddAndRemove(t *testing.T) { { desc: "should map Cloud Editor to Grafana Cloud Editor and Support ticket admin", identity: &authn.Identity{ - ID: authn.NewNamespaceID(authn.NamespaceUser, 1), + ID: identity.NewTypedID(identity.TypeUser, 1), OrgID: 1, OrgRoles: map[int64]org.RoleType{1: org.RoleEditor}, }, @@ -193,7 +195,7 @@ func TestRBACSync_cloudRolesToAddAndRemove(t *testing.T) { { desc: "should map Cloud Admin to Grafana Cloud Admin and Support ticket admin", identity: &authn.Identity{ - ID: authn.NewNamespaceID(authn.NamespaceUser, 1), + ID: identity.NewTypedID(identity.TypeUser, 1), OrgID: 1, OrgRoles: map[int64]org.RoleType{1: org.RoleAdmin}, }, @@ -212,7 +214,7 @@ func TestRBACSync_cloudRolesToAddAndRemove(t *testing.T) { { desc: "should return an error for not supported role", identity: &authn.Identity{ - ID: authn.NewNamespaceID(authn.NamespaceUser, 1), + ID: identity.NewTypedID(identity.TypeUser, 1), OrgID: 1, OrgRoles: map[int64]org.RoleType{1: org.RoleNone}, }, @@ -234,7 +236,7 @@ func TestRBACSync_cloudRolesToAddAndRemove(t *testing.T) { func setupTestEnv() *RBACSync { acMock := &acmock.Mock{ - GetUserPermissionsFunc: func(ctx context.Context, siu authn.Requester, o accesscontrol.Options) ([]accesscontrol.Permission, error) { + GetUserPermissionsFunc: func(ctx context.Context, siu identity.Requester, o accesscontrol.Options) ([]accesscontrol.Permission, error) { return []accesscontrol.Permission{ {Action: accesscontrol.ActionUsersRead}, }, nil diff --git a/pkg/services/authn/authnimpl/sync/user_sync.go b/pkg/services/authn/authnimpl/sync/user_sync.go index 524ece3a450..622846e5ac6 100644 --- a/pkg/services/authn/authnimpl/sync/user_sync.go +++ b/pkg/services/authn/authnimpl/sync/user_sync.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/grafana/grafana/pkg/apimachinery/errutil" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/authn" @@ -109,21 +110,21 @@ func (s *UserSync) SyncUserHook(ctx context.Context, id *authn.Identity, _ *auth return nil } -func (s *UserSync) FetchSyncedUserHook(ctx context.Context, identity *authn.Identity, r *authn.Request) error { +func (s *UserSync) FetchSyncedUserHook(ctx context.Context, id *authn.Identity, r *authn.Request) error { ctx, span := s.tracer.Start(ctx, "user.sync.FetchSyncedUserHook") defer span.End() - if !identity.ClientParams.FetchSyncedUser { + if !id.ClientParams.FetchSyncedUser { return nil } - if !identity.ID.IsNamespace(authn.NamespaceUser, authn.NamespaceServiceAccount) { + if !id.ID.IsType(identity.TypeUser, identity.TypeServiceAccount) { return nil } - userID, err := identity.ID.ParseInt() + userID, err := id.ID.ParseInt() if err != nil { - s.log.FromContext(ctx).Warn("got invalid identity ID", "id", identity.ID, "err", err) + s.log.FromContext(ctx).Warn("got invalid identity ID", "id", id.ID, "err", err) return nil } @@ -138,18 +139,18 @@ func (s *UserSync) FetchSyncedUserHook(ctx context.Context, identity *authn.Iden return errFetchingSignedInUser.Errorf("failed to resolve user: %w", err) } - if identity.ClientParams.AllowGlobalOrg && identity.OrgID == authn.GlobalOrgID { + if id.ClientParams.AllowGlobalOrg && id.OrgID == authn.GlobalOrgID { usr.Teams = nil usr.OrgName = "" usr.OrgRole = org.RoleNone usr.OrgID = authn.GlobalOrgID } - syncSignedInUserToIdentity(usr, identity) + syncSignedInUserToIdentity(usr, id) return nil } -func (s *UserSync) SyncLastSeenHook(ctx context.Context, identity *authn.Identity, r *authn.Request) error { +func (s *UserSync) SyncLastSeenHook(ctx context.Context, id *authn.Identity, r *authn.Request) error { ctx, span := s.tracer.Start(ctx, "user.sync.SyncLastSeenHook") defer span.End() @@ -158,13 +159,13 @@ func (s *UserSync) SyncLastSeenHook(ctx context.Context, identity *authn.Identit return nil } - if !identity.ID.IsNamespace(authn.NamespaceUser, authn.NamespaceServiceAccount) { + if !id.ID.IsType(identity.TypeUser, identity.TypeServiceAccount) { return nil } - userID, err := identity.ID.ParseInt() + userID, err := id.ID.ParseInt() if err != nil { - s.log.FromContext(ctx).Warn("got invalid identity ID", "id", identity.ID, "err", err) + s.log.FromContext(ctx).Warn("got invalid identity ID", "id", id.ID, "err", err) return nil } @@ -186,21 +187,21 @@ func (s *UserSync) SyncLastSeenHook(ctx context.Context, identity *authn.Identit return nil } -func (s *UserSync) EnableUserHook(ctx context.Context, identity *authn.Identity, _ *authn.Request) error { +func (s *UserSync) EnableUserHook(ctx context.Context, id *authn.Identity, _ *authn.Request) error { ctx, span := s.tracer.Start(ctx, "user.sync.EnableUserHook") defer span.End() - if !identity.ClientParams.EnableUser { + if !id.ClientParams.EnableUser { return nil } - if !identity.ID.IsNamespace(authn.NamespaceUser) { + if !id.ID.IsType(identity.TypeUser) { return nil } - userID, err := identity.ID.ParseInt() + userID, err := id.ID.ParseInt() if err != nil { - s.log.FromContext(ctx).Warn("got invalid identity ID", "id", identity.ID, "err", err) + s.log.FromContext(ctx).Warn("got invalid identity ID", "id", id.ID, "err", err) return nil } @@ -417,8 +418,8 @@ func (s *UserSync) lookupByOneOf(ctx context.Context, params login.UserLookupPar // syncUserToIdentity syncs a user to an identity. // This is used to update the identity with the latest user information. func syncUserToIdentity(usr *user.User, id *authn.Identity) { - id.ID = authn.NewNamespaceID(authn.NamespaceUser, usr.ID) - id.UID = authn.NewNamespaceIDString(authn.NamespaceUser, usr.UID) + id.ID = identity.NewTypedID(identity.TypeUser, usr.ID) + id.UID = identity.NewTypedIDString(identity.TypeUser, usr.UID) id.Login = usr.Login id.Email = usr.Email id.Name = usr.Name @@ -427,25 +428,25 @@ func syncUserToIdentity(usr *user.User, id *authn.Identity) { } // syncSignedInUserToIdentity syncs a user to an identity. -func syncSignedInUserToIdentity(usr *user.SignedInUser, identity *authn.Identity) { - var ns authn.Namespace - if identity.ID.IsNamespace(authn.NamespaceServiceAccount) { - ns = authn.NamespaceServiceAccount +func syncSignedInUserToIdentity(usr *user.SignedInUser, id *authn.Identity) { + var ns identity.IdentityType + if id.ID.IsType(identity.TypeServiceAccount) { + ns = identity.TypeServiceAccount } else { - ns = authn.NamespaceUser + ns = identity.TypeUser } - identity.UID = authn.NewNamespaceIDString(ns, usr.UserUID) + id.UID = identity.NewTypedIDString(ns, usr.UserUID) - identity.Name = usr.Name - identity.Login = usr.Login - identity.Email = usr.Email - identity.OrgID = usr.OrgID - identity.OrgName = usr.OrgName - identity.OrgRoles = map[int64]org.RoleType{identity.OrgID: usr.OrgRole} - identity.HelpFlags1 = usr.HelpFlags1 - identity.Teams = usr.Teams - identity.LastSeenAt = usr.LastSeenAt - identity.IsDisabled = usr.IsDisabled - identity.IsGrafanaAdmin = &usr.IsGrafanaAdmin - identity.EmailVerified = usr.EmailVerified + id.Name = usr.Name + id.Login = usr.Login + id.Email = usr.Email + id.OrgID = usr.OrgID + id.OrgName = usr.OrgName + id.OrgRoles = map[int64]org.RoleType{id.OrgID: usr.OrgRole} + id.HelpFlags1 = usr.HelpFlags1 + id.Teams = usr.Teams + id.LastSeenAt = usr.LastSeenAt + id.IsDisabled = usr.IsDisabled + id.IsGrafanaAdmin = &usr.IsGrafanaAdmin + id.EmailVerified = usr.EmailVerified } diff --git a/pkg/services/authn/authnimpl/sync/user_sync_test.go b/pkg/services/authn/authnimpl/sync/user_sync_test.go index 1a01b748bd2..3363f421662 100644 --- a/pkg/services/authn/authnimpl/sync/user_sync_test.go +++ b/pkg/services/authn/authnimpl/sync/user_sync_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/login" @@ -163,8 +164,8 @@ func TestUserSync_SyncUserHook(t *testing.T) { }, wantErr: false, wantID: &authn.Identity{ - ID: authn.MustParseNamespaceID("user:1"), - UID: authn.MustParseNamespaceID("user:1"), + ID: identity.MustParseTypedID("user:1"), + UID: identity.MustParseTypedID("user:1"), Login: "test", Name: "test", Email: "test", @@ -202,8 +203,8 @@ func TestUserSync_SyncUserHook(t *testing.T) { }, wantErr: false, wantID: &authn.Identity{ - ID: authn.MustParseNamespaceID("user:1"), - UID: authn.MustParseNamespaceID("user:1"), + ID: identity.MustParseTypedID("user:1"), + UID: identity.MustParseTypedID("user:1"), Login: "test", Name: "test", Email: "test", @@ -243,8 +244,8 @@ func TestUserSync_SyncUserHook(t *testing.T) { }, wantErr: false, wantID: &authn.Identity{ - ID: authn.MustParseNamespaceID("user:1"), - UID: authn.MustParseNamespaceID("user:1"), + ID: identity.MustParseTypedID("user:1"), + UID: identity.MustParseTypedID("user:1"), AuthID: "2032", AuthenticatedBy: "oauth", Login: "test", @@ -315,8 +316,8 @@ func TestUserSync_SyncUserHook(t *testing.T) { }, wantErr: false, wantID: &authn.Identity{ - ID: authn.MustParseNamespaceID("user:2"), - UID: authn.MustParseNamespaceID("user:2"), + ID: identity.MustParseTypedID("user:2"), + UID: identity.MustParseTypedID("user:2"), Login: "test_create", Name: "test_create", Email: "test_create", @@ -361,8 +362,8 @@ func TestUserSync_SyncUserHook(t *testing.T) { }, wantErr: false, wantID: &authn.Identity{ - ID: authn.MustParseNamespaceID("user:3"), - UID: authn.MustParseNamespaceID("user:3"), + ID: identity.MustParseTypedID("user:3"), + UID: identity.MustParseTypedID("user:3"), Login: "test_mod", Name: "test_mod", Email: "test_mod", @@ -406,8 +407,8 @@ func TestUserSync_SyncUserHook(t *testing.T) { }, wantErr: false, wantID: &authn.Identity{ - ID: authn.MustParseNamespaceID("user:3"), - UID: authn.MustParseNamespaceID("user:3"), + ID: identity.MustParseTypedID("user:3"), + UID: identity.MustParseTypedID("user:3"), Name: "test", Login: "test", Email: "test_mod@test.com", @@ -457,7 +458,7 @@ func TestUserSync_FetchSyncedUserHook(t *testing.T) { { desc: "should skip hook when identity is not a user", req: &authn.Request{}, - identity: &authn.Identity{ID: authn.MustParseNamespaceID("api-key:1"), ClientParams: authn.ClientParams{FetchSyncedUser: true}}, + identity: &authn.Identity{ID: identity.MustParseTypedID("api-key:1"), ClientParams: authn.ClientParams{FetchSyncedUser: true}}, }, } @@ -483,7 +484,7 @@ func TestUserSync_EnableDisabledUserHook(t *testing.T) { { desc: "should skip if correct flag is not set", identity: &authn.Identity{ - ID: authn.NewNamespaceID(authn.NamespaceUser, 1), + ID: identity.NewTypedID(identity.TypeUser, 1), IsDisabled: true, ClientParams: authn.ClientParams{EnableUser: false}, }, @@ -492,7 +493,7 @@ func TestUserSync_EnableDisabledUserHook(t *testing.T) { { desc: "should skip if identity is not a user", identity: &authn.Identity{ - ID: authn.NewNamespaceID(authn.NamespaceAPIKey, 1), + ID: identity.NewTypedID(identity.TypeAPIKey, 1), IsDisabled: true, ClientParams: authn.ClientParams{EnableUser: true}, }, @@ -501,7 +502,7 @@ func TestUserSync_EnableDisabledUserHook(t *testing.T) { { desc: "should enabled disabled user", identity: &authn.Identity{ - ID: authn.NewNamespaceID(authn.NamespaceUser, 1), + ID: identity.NewTypedID(identity.TypeUser, 1), IsDisabled: true, ClientParams: authn.ClientParams{EnableUser: true}, }, diff --git a/pkg/services/authn/authntest/fake.go b/pkg/services/authn/authntest/fake.go index 73219cf245c..8589ae9a812 100644 --- a/pkg/services/authn/authntest/fake.go +++ b/pkg/services/authn/authntest/fake.go @@ -78,7 +78,7 @@ func (f *FakeService) Logout(_ context.Context, _ identity.Requester, _ *usertok panic("unimplemented") } -func (f *FakeService) ResolveIdentity(ctx context.Context, orgID int64, namespaceID authn.NamespaceID) (*authn.Identity, error) { +func (f *FakeService) ResolveIdentity(ctx context.Context, orgID int64, namespaceID identity.TypedID) (*authn.Identity, error) { if f.ExpectedIdentities != nil { if f.CurrentIndex >= len(f.ExpectedIdentities) { panic("ExpectedIdentities is empty") diff --git a/pkg/services/authn/authntest/mock.go b/pkg/services/authn/authntest/mock.go index 177fc7c722a..a4934ca640b 100644 --- a/pkg/services/authn/authntest/mock.go +++ b/pkg/services/authn/authntest/mock.go @@ -54,7 +54,7 @@ func (*MockService) Logout(_ context.Context, _ identity.Requester, _ *usertoken panic("unimplemented") } -func (m *MockService) ResolveIdentity(ctx context.Context, orgID int64, namespaceID authn.NamespaceID) (*authn.Identity, error) { +func (m *MockService) ResolveIdentity(ctx context.Context, orgID int64, namespaceID identity.TypedID) (*authn.Identity, error) { panic("unimplemented") } @@ -77,8 +77,8 @@ type MockClient struct { PriorityFunc func() uint HookFunc func(ctx context.Context, identity *authn.Identity, r *authn.Request) error LogoutFunc func(ctx context.Context, user identity.Requester) (*authn.Redirect, bool) - NamespaceFunc func() string - ResolveIdentityFunc func(ctx context.Context, orgID int64, namespaceID authn.NamespaceID) (*authn.Identity, error) + IdentityTypeFunc func() identity.IdentityType + ResolveIdentityFunc func(ctx context.Context, orgID int64, namespaceID identity.TypedID) (*authn.Identity, error) } func (m MockClient) Name() string { @@ -127,15 +127,15 @@ func (m *MockClient) Logout(ctx context.Context, user identity.Requester) (*auth return nil, false } -func (m *MockClient) Namespace() string { - if m.NamespaceFunc != nil { - return m.NamespaceFunc() +func (m *MockClient) IdentityType() identity.IdentityType { + if m.IdentityTypeFunc != nil { + return m.IdentityTypeFunc() } - return "" + return identity.TypeEmpty } // ResolveIdentity implements authn.IdentityResolverClient. -func (m *MockClient) ResolveIdentity(ctx context.Context, orgID int64, namespaceID authn.NamespaceID) (*authn.Identity, error) { +func (m *MockClient) ResolveIdentity(ctx context.Context, orgID int64, namespaceID identity.TypedID) (*authn.Identity, error) { if m.ResolveIdentityFunc != nil { return m.ResolveIdentityFunc(ctx, orgID, namespaceID) } diff --git a/pkg/services/authn/clients/api_key.go b/pkg/services/authn/clients/api_key.go index b8095831c75..bfbe8b59bd1 100644 --- a/pkg/services/authn/clients/api_key.go +++ b/pkg/services/authn/clients/api_key.go @@ -7,6 +7,7 @@ import ( "time" "github.com/grafana/grafana/pkg/apimachinery/errutil" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/components/apikeygen" "github.com/grafana/grafana/pkg/components/satokengen" "github.com/grafana/grafana/pkg/infra/log" @@ -134,13 +135,13 @@ func (s *APIKey) Priority() uint { return 30 } -func (s *APIKey) Namespace() string { - return authn.NamespaceAPIKey.String() +func (s *APIKey) IdentityType() identity.IdentityType { + return identity.TypeAPIKey } -func (s *APIKey) ResolveIdentity(ctx context.Context, orgID int64, namespaceID authn.NamespaceID) (*authn.Identity, error) { - if !namespaceID.IsNamespace(authn.NamespaceAPIKey) { - return nil, authn.ErrInvalidNamespaceID.Errorf("got unspected namespace: %s", namespaceID.Namespace()) +func (s *APIKey) ResolveIdentity(ctx context.Context, orgID int64, namespaceID identity.TypedID) (*authn.Identity, error) { + if !namespaceID.IsType(identity.TypeAPIKey) { + return nil, identity.ErrInvalidTypedID.Errorf("got unspected namespace: %s", namespaceID.Type()) } apiKeyID, err := namespaceID.ParseInt() @@ -160,7 +161,7 @@ func (s *APIKey) ResolveIdentity(ctx context.Context, orgID int64, namespaceID a } if key.ServiceAccountId != nil && *key.ServiceAccountId >= 1 { - return nil, authn.ErrInvalidNamespaceID.Errorf("api key belongs to service account") + return nil, identity.ErrInvalidTypedID.Errorf("api key belongs to service account") } return newAPIKeyIdentity(key), nil @@ -187,18 +188,18 @@ func (s *APIKey) Hook(ctx context.Context, identity *authn.Identity, r *authn.Re return nil } -func (s *APIKey) getAPIKeyID(ctx context.Context, identity *authn.Identity, r *authn.Request) (apiKeyID int64, exists bool) { - id, err := identity.ID.ParseInt() +func (s *APIKey) getAPIKeyID(ctx context.Context, id *authn.Identity, r *authn.Request) (apiKeyID int64, exists bool) { + internalId, err := id.ID.ParseInt() if err != nil { s.log.Warn("Failed to parse ID from identifier", "err", err) return -1, false } - if identity.ID.IsNamespace(authn.NamespaceAPIKey) { - return id, true + if id.ID.IsType(identity.TypeAPIKey) { + return internalId, true } - if identity.ID.IsNamespace(authn.NamespaceServiceAccount) { + if id.ID.IsType(identity.TypeServiceAccount) { // When the identity is service account, the ID in from the namespace is the service account ID. // We need to fetch the API key in this scenario, as we could use it to uniquely identify a service account token. apiKey, err := s.getAPIKey(ctx, getTokenFromRequest(r)) @@ -255,7 +256,7 @@ func validateApiKey(orgID int64, key *apikey.APIKey) error { func newAPIKeyIdentity(key *apikey.APIKey) *authn.Identity { return &authn.Identity{ - ID: authn.NewNamespaceID(authn.NamespaceAPIKey, key.ID), + ID: identity.NewTypedID(identity.TypeAPIKey, key.ID), OrgID: key.OrgID, OrgRoles: map[int64]org.RoleType{key.OrgID: key.Role}, ClientParams: authn.ClientParams{SyncPermissions: true}, @@ -265,7 +266,7 @@ func newAPIKeyIdentity(key *apikey.APIKey) *authn.Identity { func newServiceAccountIdentity(key *apikey.APIKey) *authn.Identity { return &authn.Identity{ - ID: authn.NewNamespaceID(authn.NamespaceServiceAccount, *key.ServiceAccountId), + ID: identity.NewTypedID(identity.TypeServiceAccount, *key.ServiceAccountId), OrgID: key.OrgID, AuthenticatedBy: login.APIKeyAuthModule, ClientParams: authn.ClientParams{FetchSyncedUser: true, SyncPermissions: true}, diff --git a/pkg/services/authn/clients/api_key_test.go b/pkg/services/authn/clients/api_key_test.go index d52289c643f..97e295f72b9 100644 --- a/pkg/services/authn/clients/api_key_test.go +++ b/pkg/services/authn/clients/api_key_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/assert" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/components/apikeygen" "github.com/grafana/grafana/pkg/components/satokengen" "github.com/grafana/grafana/pkg/services/apikey" @@ -47,7 +48,7 @@ func TestAPIKey_Authenticate(t *testing.T) { Role: org.RoleAdmin, }, expectedIdentity: &authn.Identity{ - ID: authn.MustParseNamespaceID("api-key:1"), + ID: identity.MustParseTypedID("api-key:1"), OrgID: 1, OrgRoles: map[int64]org.RoleType{1: org.RoleAdmin}, ClientParams: authn.ClientParams{ @@ -70,7 +71,7 @@ func TestAPIKey_Authenticate(t *testing.T) { ServiceAccountId: intPtr(1), }, expectedIdentity: &authn.Identity{ - ID: authn.MustParseNamespaceID("service-account:1"), + ID: identity.MustParseTypedID("service-account:1"), OrgID: 1, ClientParams: authn.ClientParams{ FetchSyncedUser: true, @@ -205,7 +206,7 @@ func TestAPIKey_GetAPIKeyIDFromIdentity(t *testing.T) { ServiceAccountId: intPtr(1), }, expectedIdentity: &authn.Identity{ - ID: authn.MustParseNamespaceID("service-account:1"), + ID: identity.MustParseTypedID("service-account:1"), OrgID: 1, Name: "test", AuthenticatedBy: login.APIKeyAuthModule, @@ -221,7 +222,7 @@ func TestAPIKey_GetAPIKeyIDFromIdentity(t *testing.T) { Key: hash, }, expectedIdentity: &authn.Identity{ - ID: authn.MustParseNamespaceID("api-key:2"), + ID: identity.MustParseTypedID("api-key:2"), OrgID: 1, Name: "test", AuthenticatedBy: login.APIKeyAuthModule, @@ -237,7 +238,7 @@ func TestAPIKey_GetAPIKeyIDFromIdentity(t *testing.T) { Key: hash, }, expectedIdentity: &authn.Identity{ - ID: authn.MustParseNamespaceID("user:2"), + ID: identity.MustParseTypedID("user:2"), OrgID: 1, Name: "test", AuthenticatedBy: login.APIKeyAuthModule, @@ -253,7 +254,7 @@ func TestAPIKey_GetAPIKeyIDFromIdentity(t *testing.T) { Key: hash, }, expectedIdentity: &authn.Identity{ - ID: authn.MustParseNamespaceID("service-account:2"), + ID: identity.MustParseTypedID("service-account:2"), OrgID: 1, Name: "test", AuthenticatedBy: login.APIKeyAuthModule, @@ -286,7 +287,7 @@ func TestAPIKey_GetAPIKeyIDFromIdentity(t *testing.T) { func TestAPIKey_ResolveIdentity(t *testing.T) { type testCase struct { desc string - namespaceID authn.NamespaceID + namespaceID identity.TypedID exptedApiKey *apikey.APIKey @@ -297,12 +298,12 @@ func TestAPIKey_ResolveIdentity(t *testing.T) { tests := []testCase{ { desc: "should return error for invalid namespace", - namespaceID: authn.MustParseNamespaceID("user:1"), - expectedErr: authn.ErrInvalidNamespaceID, + namespaceID: identity.MustParseTypedID("user:1"), + expectedErr: identity.ErrInvalidTypedID, }, { desc: "should return error when api key has expired", - namespaceID: authn.MustParseNamespaceID("api-key:1"), + namespaceID: identity.MustParseTypedID("api-key:1"), exptedApiKey: &apikey.APIKey{ ID: 1, OrgID: 1, @@ -312,7 +313,7 @@ func TestAPIKey_ResolveIdentity(t *testing.T) { }, { desc: "should return error when api key is revoked", - namespaceID: authn.MustParseNamespaceID("api-key:1"), + namespaceID: identity.MustParseTypedID("api-key:1"), exptedApiKey: &apikey.APIKey{ ID: 1, OrgID: 1, @@ -322,17 +323,17 @@ func TestAPIKey_ResolveIdentity(t *testing.T) { }, { desc: "should return error when api key is connected to service account", - namespaceID: authn.MustParseNamespaceID("api-key:1"), + namespaceID: identity.MustParseTypedID("api-key:1"), exptedApiKey: &apikey.APIKey{ ID: 1, OrgID: 1, ServiceAccountId: intPtr(1), }, - expectedErr: authn.ErrInvalidNamespaceID, + expectedErr: identity.ErrInvalidTypedID, }, { desc: "should return error when api key is belongs to different org", - namespaceID: authn.MustParseNamespaceID("api-key:1"), + namespaceID: identity.MustParseTypedID("api-key:1"), exptedApiKey: &apikey.APIKey{ ID: 1, OrgID: 2, @@ -342,7 +343,7 @@ func TestAPIKey_ResolveIdentity(t *testing.T) { }, { desc: "should return valid idenitty", - namespaceID: authn.MustParseNamespaceID("api-key:1"), + namespaceID: identity.MustParseTypedID("api-key:1"), exptedApiKey: &apikey.APIKey{ ID: 1, OrgID: 1, @@ -351,7 +352,7 @@ func TestAPIKey_ResolveIdentity(t *testing.T) { expectedIdenity: &authn.Identity{ OrgID: 1, OrgRoles: map[int64]org.RoleType{1: org.RoleEditor}, - ID: authn.MustParseNamespaceID("api-key:1"), + ID: identity.MustParseTypedID("api-key:1"), AuthenticatedBy: login.APIKeyAuthModule, ClientParams: authn.ClientParams{SyncPermissions: true}, }, diff --git a/pkg/services/authn/clients/basic_test.go b/pkg/services/authn/clients/basic_test.go index f92f29a3144..7521aa7e966 100644 --- a/pkg/services/authn/clients/basic_test.go +++ b/pkg/services/authn/clients/basic_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/assert" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/authn/authntest" ) @@ -24,8 +25,8 @@ func TestBasic_Authenticate(t *testing.T) { { desc: "should success when password client return identity", req: &authn.Request{HTTPRequest: &http.Request{Header: map[string][]string{authorizationHeaderName: {encodeBasicAuth("user", "password")}}}}, - client: authntest.FakePasswordClient{ExpectedIdentity: &authn.Identity{ID: authn.MustParseNamespaceID("user:1")}}, - expectedIdentity: &authn.Identity{ID: authn.MustParseNamespaceID("user:1")}, + client: authntest.FakePasswordClient{ExpectedIdentity: &authn.Identity{ID: identity.MustParseTypedID("user:1")}}, + expectedIdentity: &authn.Identity{ID: identity.MustParseTypedID("user:1")}, }, { desc: "should fail when basic auth header could not be decoded", diff --git a/pkg/services/authn/clients/ext_jwt.go b/pkg/services/authn/clients/ext_jwt.go index 69e29db1d97..af0682b8bd9 100644 --- a/pkg/services/authn/clients/ext_jwt.go +++ b/pkg/services/authn/clients/ext_jwt.go @@ -10,6 +10,7 @@ import ( authlib "github.com/grafana/authlib/authn" "github.com/grafana/grafana/pkg/apimachinery/errutil" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/apiserver/endpoints/request" "github.com/grafana/grafana/pkg/services/authn" @@ -108,21 +109,21 @@ func (s *ExtendedJWT) authenticateAsUser( return nil, errExtJWTMisMatchedNamespaceClaims.Errorf("unexpected access token namespace: %s", accessTokenClaims.Rest.Namespace) } - accessID, err := authn.ParseNamespaceID(accessTokenClaims.Subject) + accessID, err := identity.ParseTypedID(accessTokenClaims.Subject) if err != nil { return nil, errExtJWTInvalidSubject.Errorf("unexpected identity: %s", accessID.String()) } - if !accessID.IsNamespace(authn.NamespaceAccessPolicy) { + if !accessID.IsType(identity.TypeAccessPolicy) { return nil, errExtJWTInvalid.Errorf("unexpected identity: %s", accessID.String()) } - userID, err := authn.ParseNamespaceID(idTokenClaims.Subject) + userID, err := identity.ParseTypedID(idTokenClaims.Subject) if err != nil { return nil, errExtJWTInvalid.Errorf("failed to parse id token subject: %w", err) } - if !userID.IsNamespace(authn.NamespaceUser) { + if !userID.IsType(identity.TypeUser) { return nil, errExtJWTInvalidSubject.Errorf("unexpected identity: %s", userID.String()) } @@ -154,12 +155,12 @@ func (s *ExtendedJWT) authenticateAsService(claims *authlib.Claims[authlib.Acces return nil, errExtJWTDisallowedNamespaceClaim.Errorf("unexpected access token namespace: %s", claims.Rest.Namespace) } - id, err := authn.ParseNamespaceID(claims.Subject) + id, err := identity.ParseTypedID(claims.Subject) if err != nil { return nil, fmt.Errorf("failed to parse access token subject: %w", err) } - if !id.IsNamespace(authn.NamespaceAccessPolicy) { + if !id.IsType(identity.TypeAccessPolicy) { return nil, errExtJWTInvalidSubject.Errorf("unexpected identity: %s", id.String()) } diff --git a/pkg/services/authn/clients/ext_jwt_test.go b/pkg/services/authn/clients/ext_jwt_test.go index 2413fb38e23..de612aecf4c 100644 --- a/pkg/services/authn/clients/ext_jwt_test.go +++ b/pkg/services/authn/clients/ext_jwt_test.go @@ -17,6 +17,7 @@ import ( authnlib "github.com/grafana/authlib/authn" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/setting" ) @@ -207,8 +208,8 @@ func TestExtendedJWT_Authenticate(t *testing.T) { accessToken: &validAccessTokenClaims, orgID: 1, want: &authn.Identity{ - ID: authn.MustParseNamespaceID("access-policy:this-uid"), - UID: authn.MustParseNamespaceID("access-policy:this-uid"), + ID: identity.MustParseTypedID("access-policy:this-uid"), + UID: identity.MustParseTypedID("access-policy:this-uid"), OrgID: 1, AllowedKubernetesNamespace: "default", AuthenticatedBy: "extendedjwt", @@ -223,8 +224,8 @@ func TestExtendedJWT_Authenticate(t *testing.T) { accessToken: &validAcessTokenClaimsWildcard, orgID: 1, want: &authn.Identity{ - ID: authn.MustParseNamespaceID("access-policy:this-uid"), - UID: authn.MustParseNamespaceID("access-policy:this-uid"), + ID: identity.MustParseTypedID("access-policy:this-uid"), + UID: identity.MustParseTypedID("access-policy:this-uid"), OrgID: 1, AllowedKubernetesNamespace: "*", AuthenticatedBy: "extendedjwt", @@ -240,7 +241,7 @@ func TestExtendedJWT_Authenticate(t *testing.T) { idToken: &validIDTokenClaims, orgID: 1, want: &authn.Identity{ - ID: authn.MustParseNamespaceID("user:2"), + ID: identity.MustParseTypedID("user:2"), OrgID: 1, AllowedKubernetesNamespace: "default", AuthenticatedBy: "extendedjwt", @@ -260,7 +261,7 @@ func TestExtendedJWT_Authenticate(t *testing.T) { idToken: &validIDTokenClaims, orgID: 1, want: &authn.Identity{ - ID: authn.MustParseNamespaceID("user:2"), + ID: identity.MustParseTypedID("user:2"), OrgID: 1, AllowedKubernetesNamespace: "*", AuthenticatedBy: "extendedjwt", @@ -285,7 +286,7 @@ func TestExtendedJWT_Authenticate(t *testing.T) { }, }, want: &authn.Identity{ - ID: authn.MustParseNamespaceID("user:2"), + ID: identity.MustParseTypedID("user:2"), OrgID: 1, AllowedKubernetesNamespace: "stack-1234", AuthenticatedBy: "extendedjwt", diff --git a/pkg/services/authn/clients/grafana.go b/pkg/services/authn/clients/grafana.go index 6d2a2747776..32ce5725a08 100644 --- a/pkg/services/authn/clients/grafana.go +++ b/pkg/services/authn/clients/grafana.go @@ -6,6 +6,7 @@ import ( "errors" "net/mail" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" @@ -105,7 +106,7 @@ func (c *Grafana) AuthenticatePassword(ctx context.Context, r *authn.Request, us } return &authn.Identity{ - ID: authn.NewNamespaceID(authn.NamespaceUser, usr.ID), + ID: identity.NewTypedID(identity.TypeUser, usr.ID), OrgID: r.OrgID, ClientParams: authn.ClientParams{FetchSyncedUser: true, SyncPermissions: true}, AuthenticatedBy: login.PasswordAuthModule, diff --git a/pkg/services/authn/clients/grafana_test.go b/pkg/services/authn/clients/grafana_test.go index d77afb239a2..4a1f5f2955c 100644 --- a/pkg/services/authn/clients/grafana_test.go +++ b/pkg/services/authn/clients/grafana_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/assert" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" @@ -140,7 +141,7 @@ func TestGrafana_AuthenticatePassword(t *testing.T) { password: "password", findUser: true, expectedIdentity: &authn.Identity{ - ID: authn.MustParseNamespaceID("user:1"), + ID: identity.MustParseTypedID("user:1"), OrgID: 1, AuthenticatedBy: login.PasswordAuthModule, ClientParams: authn.ClientParams{FetchSyncedUser: true, SyncPermissions: true}, diff --git a/pkg/services/authn/clients/oauth.go b/pkg/services/authn/clients/oauth.go index 5929b0d8382..0bca27e12be 100644 --- a/pkg/services/authn/clients/oauth.go +++ b/pkg/services/authn/clients/oauth.go @@ -245,10 +245,10 @@ func (c *OAuth) RedirectURL(ctx context.Context, r *authn.Request) (*authn.Redir }, nil } -func (c *OAuth) Logout(ctx context.Context, user authn.Requester) (*authn.Redirect, bool) { +func (c *OAuth) Logout(ctx context.Context, user identity.Requester) (*authn.Redirect, bool) { token := c.oauthService.GetCurrentOAuthToken(ctx, user) - namespace, id := user.GetNamespacedID() + namespace, id := user.GetTypedID() userID, err := identity.UserIdentifier(namespace, id) if err != nil { c.log.FromContext(ctx).Error("Failed to parse user id", "namespace", namespace, "id", id, "error", err) @@ -260,7 +260,7 @@ func (c *OAuth) Logout(ctx context.Context, user authn.Requester) (*authn.Redire AuthId: user.GetAuthID(), AuthModule: user.GetAuthenticatedBy(), }); err != nil { - namespace, id := user.GetNamespacedID() + namespace, id := user.GetTypedID() c.log.FromContext(ctx).Error("Failed to invalidate tokens", "namespace", namespace, "id", id, "error", err) } diff --git a/pkg/services/authn/clients/password_test.go b/pkg/services/authn/clients/password_test.go index 76d9d87dc00..4719ee2cc9c 100644 --- a/pkg/services/authn/clients/password_test.go +++ b/pkg/services/authn/clients/password_test.go @@ -6,6 +6,7 @@ import ( "github.com/stretchr/testify/assert" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/authn/authntest" "github.com/grafana/grafana/pkg/services/loginattempt/loginattempttest" @@ -29,16 +30,16 @@ func TestPassword_AuthenticatePassword(t *testing.T) { username: "test", password: "test", req: &authn.Request{}, - clients: []authn.PasswordClient{authntest.FakePasswordClient{ExpectedIdentity: &authn.Identity{ID: authn.MustParseNamespaceID("user:1")}}}, - expectedIdentity: &authn.Identity{ID: authn.MustParseNamespaceID("user:1")}, + clients: []authn.PasswordClient{authntest.FakePasswordClient{ExpectedIdentity: &authn.Identity{ID: identity.MustParseTypedID("user:1")}}}, + expectedIdentity: &authn.Identity{ID: identity.MustParseTypedID("user:1")}, }, { desc: "should success when found in second client", username: "test", password: "test", req: &authn.Request{}, - clients: []authn.PasswordClient{authntest.FakePasswordClient{ExpectedErr: errIdentityNotFound}, authntest.FakePasswordClient{ExpectedIdentity: &authn.Identity{ID: authn.MustParseNamespaceID("user:2")}}}, - expectedIdentity: &authn.Identity{ID: authn.MustParseNamespaceID("user:2")}, + clients: []authn.PasswordClient{authntest.FakePasswordClient{ExpectedErr: errIdentityNotFound}, authntest.FakePasswordClient{ExpectedIdentity: &authn.Identity{ID: identity.MustParseTypedID("user:2")}}}, + expectedIdentity: &authn.Identity{ID: identity.MustParseTypedID("user:2")}, }, { desc: "should fail for empty password", diff --git a/pkg/services/authn/clients/proxy.go b/pkg/services/authn/clients/proxy.go index dd691e4df32..9f853ccc889 100644 --- a/pkg/services/authn/clients/proxy.go +++ b/pkg/services/authn/clients/proxy.go @@ -13,6 +13,7 @@ import ( "time" "github.com/grafana/grafana/pkg/apimachinery/errutil" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/remotecache" "github.com/grafana/grafana/pkg/services/authn" @@ -124,7 +125,7 @@ func (c *Proxy) retrieveIDFromCache(ctx context.Context, cacheKey string, r *aut } return &authn.Identity{ - ID: authn.NewNamespaceID(authn.NamespaceUser, uid), + ID: identity.NewTypedID(identity.TypeUser, uid), OrgID: r.OrgID, // FIXME: This does not match the actual auth module used, but should not have any impact // Maybe caching the auth module used with the user ID would be a good idea @@ -144,18 +145,18 @@ func (c *Proxy) Priority() uint { return 50 } -func (c *Proxy) Hook(ctx context.Context, identity *authn.Identity, r *authn.Request) error { - if identity.ClientParams.CacheAuthProxyKey == "" { +func (c *Proxy) Hook(ctx context.Context, id *authn.Identity, r *authn.Request) error { + if id.ClientParams.CacheAuthProxyKey == "" { return nil } - if !identity.ID.IsNamespace(authn.NamespaceUser) { + if !id.ID.IsType(identity.TypeUser) { return nil } - id, err := identity.ID.ParseInt() + internalId, err := id.ID.ParseInt() if err != nil { - c.log.Warn("Failed to cache proxy user", "error", err, "userId", identity.ID.ID(), "err", err) + c.log.Warn("Failed to cache proxy user", "error", err, "userId", id.ID.ID(), "err", err) return nil } @@ -175,15 +176,15 @@ func (c *Proxy) Hook(ctx context.Context, identity *authn.Identity, r *authn.Req } } - c.log.FromContext(ctx).Debug("Cache proxy user", "userId", id) - bytes := []byte(strconv.FormatInt(id, 10)) + c.log.FromContext(ctx).Debug("Cache proxy user", "userId", internalId) + bytes := []byte(strconv.FormatInt(internalId, 10)) duration := time.Duration(c.cfg.AuthProxy.SyncTTL) * time.Minute - if err := c.cache.Set(ctx, identity.ClientParams.CacheAuthProxyKey, bytes, duration); err != nil { - c.log.Warn("Failed to cache proxy user", "error", err, "userId", id) + if err := c.cache.Set(ctx, id.ClientParams.CacheAuthProxyKey, bytes, duration); err != nil { + c.log.Warn("Failed to cache proxy user", "error", err, "userId", internalId) } // store current cacheKey for the user - return c.cache.Set(ctx, userKey, []byte(identity.ClientParams.CacheAuthProxyKey), duration) + return c.cache.Set(ctx, userKey, []byte(id.ClientParams.CacheAuthProxyKey), duration) } func (c *Proxy) isAllowedIP(r *authn.Request) bool { diff --git a/pkg/services/authn/clients/proxy_test.go b/pkg/services/authn/clients/proxy_test.go index 92086a6d87e..59bec50fbaf 100644 --- a/pkg/services/authn/clients/proxy_test.go +++ b/pkg/services/authn/clients/proxy_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/authn/authntest" "github.com/grafana/grafana/pkg/setting" @@ -203,7 +204,7 @@ func TestProxy_Hook(t *testing.T) { } cache := &fakeCache{data: make(map[string][]byte)} userId := int64(1) - userID := authn.NewNamespaceID(authn.NamespaceUser, userId) + userID := identity.NewTypedID(identity.TypeUser, userId) // withRole creates a test case for a user with a specific role. withRole := func(role string) func(t *testing.T) { diff --git a/pkg/services/authn/clients/render.go b/pkg/services/authn/clients/render.go index fc3040401cf..9491d946e39 100644 --- a/pkg/services/authn/clients/render.go +++ b/pkg/services/authn/clients/render.go @@ -5,6 +5,7 @@ import ( "time" "github.com/grafana/grafana/pkg/apimachinery/errutil" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" @@ -42,7 +43,7 @@ func (c *Render) Authenticate(ctx context.Context, r *authn.Request) (*authn.Ide if renderUsr.UserID <= 0 { return &authn.Identity{ - ID: authn.NewNamespaceID(authn.NamespaceRenderService, 0), + ID: identity.NewTypedID(identity.TypeRenderService, 0), OrgID: renderUsr.OrgID, OrgRoles: map[int64]org.RoleType{renderUsr.OrgID: org.RoleType(renderUsr.OrgRole)}, ClientParams: authn.ClientParams{SyncPermissions: true}, @@ -52,7 +53,7 @@ func (c *Render) Authenticate(ctx context.Context, r *authn.Request) (*authn.Ide } return &authn.Identity{ - ID: authn.NewNamespaceID(authn.NamespaceUser, renderUsr.UserID), + ID: identity.NewTypedID(identity.TypeUser, renderUsr.UserID), LastSeenAt: time.Now(), AuthenticatedBy: login.RenderModule, ClientParams: authn.ClientParams{FetchSyncedUser: true, SyncPermissions: true}, diff --git a/pkg/services/authn/clients/render_test.go b/pkg/services/authn/clients/render_test.go index 8e815de266e..7ab176e2aeb 100644 --- a/pkg/services/authn/clients/render_test.go +++ b/pkg/services/authn/clients/render_test.go @@ -9,6 +9,7 @@ import ( "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/org" @@ -35,7 +36,7 @@ func TestRender_Authenticate(t *testing.T) { }, }, expectedIdentity: &authn.Identity{ - ID: authn.MustParseNamespaceID("render:0"), + ID: identity.MustParseTypedID("render:0"), OrgID: 1, OrgRoles: map[int64]org.RoleType{1: org.RoleViewer}, AuthenticatedBy: login.RenderModule, @@ -56,7 +57,7 @@ func TestRender_Authenticate(t *testing.T) { }, }, expectedIdentity: &authn.Identity{ - ID: authn.MustParseNamespaceID("user:1"), + ID: identity.MustParseTypedID("user:1"), AuthenticatedBy: login.RenderModule, ClientParams: authn.ClientParams{FetchSyncedUser: true, SyncPermissions: true}, }, diff --git a/pkg/services/authn/clients/session.go b/pkg/services/authn/clients/session.go index 603a3bf64bc..3527d372d98 100644 --- a/pkg/services/authn/clients/session.go +++ b/pkg/services/authn/clients/session.go @@ -6,6 +6,7 @@ import ( "net/url" "time" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/authn" @@ -57,7 +58,7 @@ func (s *Session) Authenticate(ctx context.Context, r *authn.Request) (*authn.Id } ident := &authn.Identity{ - ID: authn.NewNamespaceID(authn.NamespaceUser, token.UserId), + ID: identity.NewTypedID(identity.TypeUser, token.UserId), SessionToken: token, ClientParams: authn.ClientParams{ FetchSyncedUser: true, diff --git a/pkg/services/authn/clients/session_test.go b/pkg/services/authn/clients/session_test.go index c8fbc329277..28b14b5bf41 100644 --- a/pkg/services/authn/clients/session_test.go +++ b/pkg/services/authn/clients/session_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/models/usertoken" "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/auth/authtest" @@ -96,7 +97,7 @@ func TestSession_Authenticate(t *testing.T) { }, args: args{r: &authn.Request{HTTPRequest: validHTTPReq}}, wantID: &authn.Identity{ - ID: authn.MustParseNamespaceID("user:1"), + ID: identity.MustParseTypedID("user:1"), SessionToken: validToken, ClientParams: authn.ClientParams{ SyncPermissions: true, @@ -129,7 +130,7 @@ func TestSession_Authenticate(t *testing.T) { }, args: args{r: &authn.Request{HTTPRequest: validHTTPReq}}, wantID: &authn.Identity{ - ID: authn.MustParseNamespaceID("user:1"), + ID: identity.MustParseTypedID("user:1"), SessionToken: validToken, ClientParams: authn.ClientParams{ SyncPermissions: true, @@ -148,7 +149,7 @@ func TestSession_Authenticate(t *testing.T) { }, args: args{r: &authn.Request{HTTPRequest: validHTTPReq}}, wantID: &authn.Identity{ - ID: authn.MustParseNamespaceID("user:1"), + ID: identity.MustParseTypedID("user:1"), AuthID: "1", AuthenticatedBy: "oauth_azuread", SessionToken: validToken, @@ -170,7 +171,7 @@ func TestSession_Authenticate(t *testing.T) { }, args: args{r: &authn.Request{HTTPRequest: validHTTPReq}}, wantID: &authn.Identity{ - ID: authn.MustParseNamespaceID("user:1"), + ID: identity.MustParseTypedID("user:1"), SessionToken: validToken, ClientParams: authn.ClientParams{ diff --git a/pkg/services/authn/identity.go b/pkg/services/authn/identity.go index d1094025c5c..2ad7534ff4a 100644 --- a/pkg/services/authn/identity.go +++ b/pkg/services/authn/identity.go @@ -15,16 +15,14 @@ import ( const GlobalOrgID = int64(0) -type Requester = identity.Requester - -var _ Requester = (*Identity)(nil) +var _ identity.Requester = (*Identity)(nil) type Identity struct { // ID is the unique identifier for the entity in the Grafana database. // If the entity is not found in the DB or this entity is non-persistent, this field will be empty. - ID NamespaceID + ID identity.TypedID // UID is a unique identifier stored for the entity in Grafana database. Not all entities support uid so it can be empty. - UID NamespaceID + UID identity.TypedID // OrgID is the active organization for the entity. OrgID int64 // OrgName is the name of the active organization. @@ -74,15 +72,15 @@ type Identity struct { IDToken string } -func (i *Identity) GetID() NamespaceID { +func (i *Identity) GetID() identity.TypedID { return i.ID } -func (i *Identity) GetNamespacedID() (namespace identity.Namespace, identifier string) { - return i.ID.Namespace(), i.ID.ID() +func (i *Identity) GetTypedID() (namespace identity.IdentityType, identifier string) { + return i.ID.Type(), i.ID.ID() } -func (i *Identity) GetUID() NamespaceID { +func (i *Identity) GetUID() identity.TypedID { return i.UID } @@ -95,7 +93,7 @@ func (i *Identity) GetAuthenticatedBy() string { } func (i *Identity) GetCacheKey() string { - namespace, id := i.GetNamespacedID() + namespace, id := i.GetTypedID() if !i.HasUniqueId() { // Hack use the org role as id for identities that do not have a unique id // e.g. anonymous and render key. @@ -191,8 +189,10 @@ func (i *Identity) HasRole(role org.RoleType) bool { } func (i *Identity) HasUniqueId() bool { - namespace, _ := i.GetNamespacedID() - return namespace == NamespaceUser || namespace == NamespaceServiceAccount || namespace == NamespaceAPIKey + namespace, _ := i.GetTypedID() + return namespace == identity.TypeUser || + namespace == identity.TypeServiceAccount || + namespace == identity.TypeAPIKey } func (i *Identity) IsAuthenticatedBy(providers ...string) bool { @@ -220,7 +220,7 @@ func (i *Identity) SignedInUser() *user.SignedInUser { AuthID: i.AuthID, AuthenticatedBy: i.AuthenticatedBy, IsGrafanaAdmin: i.GetIsGrafanaAdmin(), - IsAnonymous: i.ID.IsNamespace(NamespaceAnonymous), + IsAnonymous: i.ID.IsType(identity.TypeAnonymous), IsDisabled: i.IsDisabled, HelpFlags1: i.HelpFlags1, LastSeenAt: i.LastSeenAt, @@ -230,14 +230,14 @@ func (i *Identity) SignedInUser() *user.SignedInUser { NamespacedID: i.ID, } - if i.ID.IsNamespace(NamespaceAPIKey) { + if i.ID.IsType(identity.TypeAPIKey) { id, _ := i.ID.ParseInt() u.ApiKeyID = id } else { id, _ := i.ID.UserID() u.UserID = id u.UserUID = i.UID.ID() - u.IsServiceAccount = i.ID.IsNamespace(NamespaceServiceAccount) + u.IsServiceAccount = i.ID.IsType(identity.TypeServiceAccount) } return u diff --git a/pkg/services/authn/namespace.go b/pkg/services/authn/namespace.go deleted file mode 100644 index 336fec15c46..00000000000 --- a/pkg/services/authn/namespace.go +++ /dev/null @@ -1,27 +0,0 @@ -package authn - -import ( - "github.com/grafana/grafana/pkg/apimachinery/identity" -) - -const ( - NamespaceUser = identity.NamespaceUser - NamespaceAPIKey = identity.NamespaceAPIKey - NamespaceServiceAccount = identity.NamespaceServiceAccount - NamespaceAnonymous = identity.NamespaceAnonymous - NamespaceRenderService = identity.NamespaceRenderService - NamespaceAccessPolicy = identity.NamespaceAccessPolicy -) - -var AnonymousNamespaceID = NewNamespaceID(NamespaceAnonymous, 0) - -type Namespace = identity.Namespace -type NamespaceID = identity.NamespaceID - -var ( - ParseNamespaceID = identity.ParseNamespaceID - MustParseNamespaceID = identity.MustParseNamespaceID - NewNamespaceID = identity.NewNamespaceID - NewNamespaceIDString = identity.NewNamespaceIDString - ErrInvalidNamespaceID = identity.ErrInvalidNamespaceID -) diff --git a/pkg/services/authz/server.go b/pkg/services/authz/server.go index 8403ef4500a..0c1b033ed64 100644 --- a/pkg/services/authz/server.go +++ b/pkg/services/authz/server.go @@ -5,6 +5,7 @@ import ( authzv1 "github.com/grafana/authlib/authz/proto/v1" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/services/accesscontrol" @@ -54,7 +55,16 @@ func (s *legacyServer) Read(ctx context.Context, req *authzv1.ReadRequest) (*aut ctxLogger := s.logger.FromContext(ctx) ctxLogger.Debug("Read", "action", action, "subject", subject, "stackID", stackID) - permissions, err := s.acSvc.SearchUserPermissions(ctx, stackID, accesscontrol.SearchOptions{NamespacedID: subject, Action: action}) + var err error + opts := accesscontrol.SearchOptions{Action: action} + if subject != "" { + opts.TypedID, err = identity.ParseTypedID(subject) + if err != nil { + return nil, err + } + } + + permissions, err := s.acSvc.SearchUserPermissions(ctx, stackID, opts) if err != nil { ctxLogger.Error("failed to search user permissions", "error", err) return nil, tracing.Errorf(span, "failed to search user permissions: %w", err) diff --git a/pkg/services/contexthandler/contexthandler.go b/pkg/services/contexthandler/contexthandler.go index a58425e54ff..7ae68ac59c0 100644 --- a/pkg/services/contexthandler/contexthandler.go +++ b/pkg/services/contexthandler/contexthandler.go @@ -6,10 +6,11 @@ import ( "fmt" "net/http" - authnClients "github.com/grafana/grafana/pkg/services/authn/clients" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" + authnClients "github.com/grafana/grafana/pkg/services/authn/clients" + "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" @@ -153,12 +154,12 @@ func (h *ContextHandler) addIDHeaderEndOfRequestFunc(ident identity.Requester) w return } - namespace, id := ident.GetNamespacedID() - if !identity.IsNamespace( + namespace, id := ident.GetTypedID() + if !identity.IsIdentityType( namespace, - identity.NamespaceUser, - identity.NamespaceServiceAccount, - identity.NamespaceAPIKey, + identity.TypeUser, + identity.TypeServiceAccount, + identity.TypeAPIKey, ) || id == "0" { return } diff --git a/pkg/services/contexthandler/contexthandler_test.go b/pkg/services/contexthandler/contexthandler_test.go index c237660e237..ae8807ad209 100644 --- a/pkg/services/contexthandler/contexthandler_test.go +++ b/pkg/services/contexthandler/contexthandler_test.go @@ -43,7 +43,7 @@ func TestContextHandler(t *testing.T) { }) t.Run("should set identity on successful authentication", func(t *testing.T) { - id := &authn.Identity{ID: authn.NewNamespaceID(authn.NamespaceUser, 1), OrgID: 1} + id := &authn.Identity{ID: identity.NewTypedID(identity.TypeUser, 1), OrgID: 1} handler := contexthandler.ProvideService( setting.NewCfg(), tracing.InitializeTracerForTest(), @@ -68,7 +68,7 @@ func TestContextHandler(t *testing.T) { }) t.Run("should not set IsSignedIn on anonymous identity", func(t *testing.T) { - identity := &authn.Identity{ID: authn.AnonymousNamespaceID, OrgID: 1} + identity := &authn.Identity{ID: identity.AnonymousTypedID, OrgID: 1} handler := contexthandler.ProvideService( setting.NewCfg(), tracing.InitializeTracerForTest(), @@ -148,7 +148,7 @@ func TestContextHandler(t *testing.T) { handler := contexthandler.ProvideService( cfg, tracing.InitializeTracerForTest(), - &authntest.FakeService{ExpectedIdentity: &authn.Identity{ID: authn.MustParseNamespaceID(id)}}, + &authntest.FakeService{ExpectedIdentity: &authn.Identity{ID: identity.MustParseTypedID(id)}}, ) server := webtest.NewServer(t, routing.NewRouteRegister()) diff --git a/pkg/services/dashboardimport/service/service.go b/pkg/services/dashboardimport/service/service.go index 7eeefed6585..aff5dc0800c 100644 --- a/pkg/services/dashboardimport/service/service.go +++ b/pkg/services/dashboardimport/service/service.go @@ -109,10 +109,10 @@ func (s *ImportDashboardService) ImportDashboard(ctx context.Context, req *dashb req.FolderUid = folder.UID } - namespace, identifier := req.User.GetNamespacedID() + namespace, identifier := req.User.GetTypedID() userID := int64(0) - if namespace == identity.NamespaceUser || namespace == identity.NamespaceServiceAccount { + if namespace == identity.TypeUser || namespace == identity.TypeServiceAccount { userID, _ = identity.IntIdentifier(namespace, identifier) } diff --git a/pkg/services/dashboardimport/service/service_test.go b/pkg/services/dashboardimport/service/service_test.go index 1aa249ddeac..42cd3f9503d 100644 --- a/pkg/services/dashboardimport/service/service_test.go +++ b/pkg/services/dashboardimport/service/service_test.go @@ -83,7 +83,7 @@ func TestImportDashboardService(t *testing.T) { require.NotNil(t, resp) require.Equal(t, "UDdpyzz7z", resp.UID) - userID, err := identity.IntIdentifier(importDashboardArg.User.GetNamespacedID()) + userID, err := identity.IntIdentifier(importDashboardArg.User.GetTypedID()) require.NoError(t, err) require.NotNil(t, importDashboardArg) @@ -149,7 +149,7 @@ func TestImportDashboardService(t *testing.T) { require.NotNil(t, resp) require.Equal(t, "UDdpyzz7z", resp.UID) - userID, err := identity.IntIdentifier(importDashboardArg.User.GetNamespacedID()) + userID, err := identity.IntIdentifier(importDashboardArg.User.GetTypedID()) require.NoError(t, err) require.NotNil(t, importDashboardArg) diff --git a/pkg/services/dashboards/database/database.go b/pkg/services/dashboards/database/database.go index 87d37ab0e68..523bf2918b0 100644 --- a/pkg/services/dashboards/database/database.go +++ b/pkg/services/dashboards/database/database.go @@ -878,7 +878,7 @@ func (d *dashboardStore) FindDashboards(ctx context.Context, query *dashboards.F } // only list k6 folders when requested by a service account - prevents showing k6 folders in the UI for users - if query.SignedInUser == nil || query.SignedInUser.GetID().Namespace() != identity.NamespaceServiceAccount { + if query.SignedInUser == nil || query.SignedInUser.GetID().Type() != identity.TypeServiceAccount { filters = append(filters, searchstore.K6FolderFilter{}) } diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index 6937a1ea4ad..d72b657d87c 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -234,8 +234,8 @@ func (dr *DashboardServiceImpl) BuildSaveDashboardCommand(ctx context.Context, d func resolveUserID(user identity.Requester, log log.Logger) (int64, error) { userID := int64(0) - namespaceID, identifier := user.GetNamespacedID() - if namespaceID != identity.NamespaceUser && namespaceID != identity.NamespaceServiceAccount { + namespaceID, identifier := user.GetTypedID() + if namespaceID != identity.TypeUser && namespaceID != identity.TypeServiceAccount { log.Debug("User does not belong to a user or service account namespace", "namespaceID", namespaceID, "userID", identifier) } else { var err error @@ -503,12 +503,12 @@ func (dr *DashboardServiceImpl) setDefaultPermissions(ctx context.Context, dto * var permissions []accesscontrol.SetResourcePermissionCommand if !provisioned { - namespaceID, userIDstr := dto.User.GetNamespacedID() + namespaceID, userIDstr := dto.User.GetTypedID() userID, err := identity.IntIdentifier(namespaceID, userIDstr) if err != nil { dr.log.Error("Could not make user admin", "dashboard", dash.Title, "namespaceID", namespaceID, "userID", userID, "error", err) - } else if namespaceID == identity.NamespaceUser && userID > 0 { + } else if namespaceID == identity.TypeUser && userID > 0 { permissions = append(permissions, accesscontrol.SetResourcePermissionCommand{ UserID: userID, Permission: dashboardaccess.PERMISSION_ADMIN.String(), }) @@ -541,12 +541,12 @@ func (dr *DashboardServiceImpl) setDefaultFolderPermissions(ctx context.Context, var permissions []accesscontrol.SetResourcePermissionCommand if !provisioned { - namespaceID, userIDstr := cmd.SignedInUser.GetNamespacedID() + namespaceID, userIDstr := cmd.SignedInUser.GetTypedID() userID, err := identity.IntIdentifier(namespaceID, userIDstr) if err != nil { dr.log.Error("Could not make user admin", "folder", cmd.Title, "namespaceID", namespaceID, "userID", userID, "error", err) - } else if namespaceID == identity.NamespaceUser && userID > 0 { + } else if namespaceID == identity.TypeUser && userID > 0 { permissions = append(permissions, accesscontrol.SetResourcePermissionCommand{ UserID: userID, Permission: dashboardaccess.PERMISSION_ADMIN.String(), }) diff --git a/pkg/services/dashboards/service/dashboard_service_integration_test.go b/pkg/services/dashboards/service/dashboard_service_integration_test.go index a9d6d0a817e..899f5ce031d 100644 --- a/pkg/services/dashboards/service/dashboard_service_integration_test.go +++ b/pkg/services/dashboards/service/dashboard_service_integration_test.go @@ -116,7 +116,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { err := callSaveWithError(t, cmd, sqlStore) assert.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - userID, err := identity.IntIdentifier(sc.dashboardGuardianMock.User.GetNamespacedID()) + userID, err := identity.IntIdentifier(sc.dashboardGuardianMock.User.GetTypedID()) require.NoError(t, err) assert.Equal(t, "", sc.dashboardGuardianMock.DashUID) @@ -139,7 +139,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { err := callSaveWithError(t, cmd, sc.sqlStore) require.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - userID, err := identity.IntIdentifier(sc.dashboardGuardianMock.User.GetNamespacedID()) + userID, err := identity.IntIdentifier(sc.dashboardGuardianMock.User.GetTypedID()) require.NoError(t, err) assert.Equal(t, sc.otherSavedFolder.ID, sc.dashboardGuardianMock.DashID) @@ -162,7 +162,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { err := callSaveWithError(t, cmd, sc.sqlStore) require.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - userID, err := identity.IntIdentifier(sc.dashboardGuardianMock.User.GetNamespacedID()) + userID, err := identity.IntIdentifier(sc.dashboardGuardianMock.User.GetTypedID()) require.NoError(t, err) assert.Equal(t, sc.savedDashInFolder.UID, sc.dashboardGuardianMock.DashUID) @@ -186,7 +186,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { err := callSaveWithError(t, cmd, sc.sqlStore) require.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - userID, err := identity.IntIdentifier(sc.dashboardGuardianMock.User.GetNamespacedID()) + userID, err := identity.IntIdentifier(sc.dashboardGuardianMock.User.GetTypedID()) require.NoError(t, err) assert.Equal(t, sc.savedDashInFolder.UID, sc.dashboardGuardianMock.DashUID) @@ -210,7 +210,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { err := callSaveWithError(t, cmd, sc.sqlStore) assert.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - userID, err := identity.IntIdentifier(sc.dashboardGuardianMock.User.GetNamespacedID()) + userID, err := identity.IntIdentifier(sc.dashboardGuardianMock.User.GetTypedID()) require.NoError(t, err) assert.Equal(t, sc.savedDashInGeneralFolder.UID, sc.dashboardGuardianMock.DashUID) @@ -234,7 +234,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { err := callSaveWithError(t, cmd, sc.sqlStore) require.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - userID, err := identity.IntIdentifier(sc.dashboardGuardianMock.User.GetNamespacedID()) + userID, err := identity.IntIdentifier(sc.dashboardGuardianMock.User.GetTypedID()) require.NoError(t, err) assert.Equal(t, sc.savedDashInFolder.UID, sc.dashboardGuardianMock.DashUID) @@ -258,7 +258,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { err := callSaveWithError(t, cmd, sc.sqlStore) require.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - userID, err := identity.IntIdentifier(sc.dashboardGuardianMock.User.GetNamespacedID()) + userID, err := identity.IntIdentifier(sc.dashboardGuardianMock.User.GetTypedID()) require.NoError(t, err) assert.Equal(t, sc.savedDashInGeneralFolder.UID, sc.dashboardGuardianMock.DashUID) @@ -282,7 +282,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { err := callSaveWithError(t, cmd, sc.sqlStore) assert.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - userID, err := identity.IntIdentifier(sc.dashboardGuardianMock.User.GetNamespacedID()) + userID, err := identity.IntIdentifier(sc.dashboardGuardianMock.User.GetTypedID()) require.NoError(t, err) assert.Equal(t, sc.savedDashInFolder.UID, sc.dashboardGuardianMock.DashUID) @@ -306,7 +306,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { err := callSaveWithError(t, cmd, sc.sqlStore) require.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - userID, err := identity.IntIdentifier(sc.dashboardGuardianMock.User.GetNamespacedID()) + userID, err := identity.IntIdentifier(sc.dashboardGuardianMock.User.GetTypedID()) require.NoError(t, err) assert.Equal(t, sc.savedDashInGeneralFolder.UID, sc.dashboardGuardianMock.DashUID) @@ -330,7 +330,7 @@ func TestIntegrationIntegratedDashboardService(t *testing.T) { err := callSaveWithError(t, cmd, sc.sqlStore) require.Equal(t, dashboards.ErrDashboardUpdateAccessDenied, err) - userID, err := identity.IntIdentifier(sc.dashboardGuardianMock.User.GetNamespacedID()) + userID, err := identity.IntIdentifier(sc.dashboardGuardianMock.User.GetTypedID()) require.NoError(t, err) assert.Equal(t, sc.savedDashInFolder.UID, sc.dashboardGuardianMock.DashUID) diff --git a/pkg/services/dashboardsnapshots/database/database.go b/pkg/services/dashboardsnapshots/database/database.go index dcd71ccb55a..4dabdb3f0ea 100644 --- a/pkg/services/dashboardsnapshots/database/database.go +++ b/pkg/services/dashboardsnapshots/database/database.go @@ -135,10 +135,10 @@ func (d *DashboardSnapshotStore) SearchDashboardSnapshots(ctx context.Context, q sess.Where("name LIKE ?", query.Name) } - namespace, id := query.SignedInUser.GetNamespacedID() + namespace, id := query.SignedInUser.GetTypedID() var userID int64 - if namespace == identity.NamespaceServiceAccount || namespace == identity.NamespaceUser { + if namespace == identity.TypeServiceAccount || namespace == identity.TypeUser { var err error userID, err = identity.IntIdentifier(namespace, id) if err != nil { @@ -150,7 +150,7 @@ func (d *DashboardSnapshotStore) SearchDashboardSnapshots(ctx context.Context, q switch { case query.SignedInUser.GetOrgRole() == org.RoleAdmin: sess.Where("org_id = ?", query.SignedInUser.GetOrgID()) - case namespace != identity.NamespaceAnonymous: + case namespace != identity.TypeAnonymous: sess.Where("org_id = ? AND user_id = ?", query.OrgID, userID) default: queryResult = snapshots diff --git a/pkg/services/dashboardsnapshots/service.go b/pkg/services/dashboardsnapshots/service.go index 80f8e58197c..f3b83b355e4 100644 --- a/pkg/services/dashboardsnapshots/service.go +++ b/pkg/services/dashboardsnapshots/service.go @@ -58,7 +58,7 @@ func CreateDashboardSnapshot(c *contextmodel.ReqContext, cfg dashboardsnapshot.S cmd.DashboardCreateCommand.Name = "Unnamed snapshot" } - userID, err := identity.UserIdentifier(c.SignedInUser.GetNamespacedID()) + userID, err := identity.UserIdentifier(c.SignedInUser.GetTypedID()) if err != nil { c.JsonApiErr(http.StatusInternalServerError, "Failed to create external snapshot", err) diff --git a/pkg/services/folder/folderimpl/folder.go b/pkg/services/folder/folderimpl/folder.go index d3821095367..fa30e3bcd99 100644 --- a/pkg/services/folder/folderimpl/folder.go +++ b/pkg/services/folder/folderimpl/folder.go @@ -580,8 +580,8 @@ func (s *Service) Create(ctx context.Context, cmd *folder.CreateFolderCommand) ( userID := int64(0) var err error - namespaceID, userIDstr := user.GetNamespacedID() - if namespaceID != identity.NamespaceUser && namespaceID != identity.NamespaceServiceAccount { + namespaceID, userIDstr := user.GetTypedID() + if namespaceID != identity.TypeUser && namespaceID != identity.TypeServiceAccount { s.log.Debug("User does not belong to a user or service account namespace, using 0 as user ID", "namespaceID", namespaceID, "userID", userIDstr) } else { userID, err = identity.IntIdentifier(namespaceID, userIDstr) @@ -668,7 +668,7 @@ func (s *Service) Update(ctx context.Context, cmd *folder.UpdateFolderCommand) ( } if cmd.NewTitle != nil { - namespace, id := cmd.SignedInUser.GetNamespacedID() + namespace, id := cmd.SignedInUser.GetTypedID() metrics.MFolderIDsServiceCount.WithLabelValues(metrics.Folder).Inc() if err := s.bus.Publish(ctx, &events.FolderTitleUpdated{ @@ -725,8 +725,8 @@ func (s *Service) legacyUpdate(ctx context.Context, cmd *folder.UpdateFolderComm } var userID int64 - namespace, id := cmd.SignedInUser.GetNamespacedID() - if namespace == identity.NamespaceUser || namespace == identity.NamespaceServiceAccount { + namespace, id := cmd.SignedInUser.GetTypedID() + if namespace == identity.TypeUser || namespace == identity.TypeServiceAccount { userID, err = identity.IntIdentifier(namespace, id) if err != nil { s.log.ErrorContext(ctx, "failed to parse user ID", "namespace", namespace, "userID", id, "error", err) @@ -1142,8 +1142,8 @@ func (s *Service) buildSaveDashboardCommand(ctx context.Context, dto *dashboards } userID := int64(0) - namespaceID, userIDstr := dto.User.GetNamespacedID() - if namespaceID != identity.NamespaceUser && namespaceID != identity.NamespaceServiceAccount { + namespaceID, userIDstr := dto.User.GetTypedID() + if namespaceID != identity.TypeUser && namespaceID != identity.TypeServiceAccount { s.log.Warn("User does not belong to a user or service account namespace, using 0 as user ID", "namespaceID", namespaceID, "userID", userIDstr) } else { userID, err = identity.IntIdentifier(namespaceID, userIDstr) diff --git a/pkg/services/folder/folderimpl/sqlstore.go b/pkg/services/folder/folderimpl/sqlstore.go index 41f732b8ccc..d5c1e1c54af 100644 --- a/pkg/services/folder/folderimpl/sqlstore.go +++ b/pkg/services/folder/folderimpl/sqlstore.go @@ -323,7 +323,7 @@ func (ss *sqlStore) GetChildren(ctx context.Context, q folder.GetChildrenQuery) } // only list k6 folders when requested by a service account - prevents showing k6 folders in the UI for users - if q.SignedInUser == nil || q.SignedInUser.GetID().Namespace() != identity.NamespaceServiceAccount { + if q.SignedInUser == nil || q.SignedInUser.GetID().Type() != identity.TypeServiceAccount { sql.WriteString(" AND uid != ?") args = append(args, accesscontrol.K6FolderUID) } @@ -484,7 +484,7 @@ func (ss *sqlStore) GetFolders(ctx context.Context, q getFoldersQuery) ([]*folde } // only list k6 folders when requested by a service account - prevents showing k6 folders in the UI for users - if q.SignedInUser == nil || q.SignedInUser.GetID().Namespace() != identity.NamespaceServiceAccount { + if q.SignedInUser == nil || q.SignedInUser.GetID().Type() != identity.TypeServiceAccount { s.WriteString(" AND f0.uid != ? AND (f0.parent_uid != ? OR f0.parent_uid IS NULL)") args = append(args, accesscontrol.K6FolderUID, accesscontrol.K6FolderUID) } diff --git a/pkg/services/guardian/accesscontrol_guardian.go b/pkg/services/guardian/accesscontrol_guardian.go index d9d80b660c6..dccc61277ac 100644 --- a/pkg/services/guardian/accesscontrol_guardian.go +++ b/pkg/services/guardian/accesscontrol_guardian.go @@ -309,7 +309,7 @@ func (a *accessControlFolderGuardian) CanCreate(folderID int64, isFolder bool) ( func (a *accessControlDashboardGuardian) evaluate(evaluator accesscontrol.Evaluator) (bool, error) { ok, err := a.ac.Evaluate(a.ctx, a.user, evaluator) - namespaceID, userID := a.user.GetNamespacedID() + namespaceID, userID := a.user.GetTypedID() if err != nil { id := 0 if a.dashboard != nil { @@ -331,7 +331,7 @@ func (a *accessControlDashboardGuardian) evaluate(evaluator accesscontrol.Evalua func (a *accessControlFolderGuardian) evaluate(evaluator accesscontrol.Evaluator) (bool, error) { ok, err := a.ac.Evaluate(a.ctx, a.user, evaluator) - namespaceID, userID := a.user.GetNamespacedID() + namespaceID, userID := a.user.GetTypedID() if err != nil { uid := "" orgID := 0 diff --git a/pkg/services/libraryelements/database.go b/pkg/services/libraryelements/database.go index 16150219e17..32b6f627c2c 100644 --- a/pkg/services/libraryelements/database.go +++ b/pkg/services/libraryelements/database.go @@ -139,8 +139,8 @@ func (l *LibraryElementService) createLibraryElement(c context.Context, signedIn } userID := int64(0) - namespaceID, identifier := signedInUser.GetNamespacedID() - if namespaceID == identity.NamespaceUser || namespaceID == identity.NamespaceServiceAccount { + namespaceID, identifier := signedInUser.GetTypedID() + if namespaceID == identity.TypeUser || namespaceID == identity.TypeServiceAccount { userID, err = identity.IntIdentifier(namespaceID, identifier) if err != nil { l.log.Warn("Error while parsing userID", "namespaceID", namespaceID, "userID", identifier) @@ -593,8 +593,8 @@ func (l *LibraryElementService) patchLibraryElement(c context.Context, signedInU } var userID int64 - namespaceID, identifier := signedInUser.GetNamespacedID() - if namespaceID == identity.NamespaceUser || namespaceID == identity.NamespaceServiceAccount { + namespaceID, identifier := signedInUser.GetTypedID() + if namespaceID == identity.TypeUser || namespaceID == identity.TypeServiceAccount { var errID error userID, errID = identity.IntIdentifier(namespaceID, identifier) if errID != nil { @@ -800,9 +800,9 @@ func (l *LibraryElementService) connectElementsToDashboardID(c context.Context, return err } - namespaceID, identifier := signedInUser.GetNamespacedID() + namespaceID, identifier := signedInUser.GetTypedID() userID := int64(0) - if namespaceID == identity.NamespaceUser || namespaceID == identity.NamespaceServiceAccount { + if namespaceID == identity.TypeUser || namespaceID == identity.TypeServiceAccount { userID, err = identity.IntIdentifier(namespaceID, identifier) if err != nil { l.log.Warn("Failed to parse user ID from namespace identifier", "namespace", namespaceID, "identifier", identifier, "error", err) diff --git a/pkg/services/live/features/dashboard.go b/pkg/services/live/features/dashboard.go index c91a54c84f0..57040d9b214 100644 --- a/pkg/services/live/features/dashboard.go +++ b/pkg/services/live/features/dashboard.go @@ -38,7 +38,7 @@ type userDisplayDTO struct { // Static function to parse a requester into a userDisplayDTO func newUserDisplayDTOFromRequester(requester identity.Requester) *userDisplayDTO { uid := "" - if requester.GetUID().IsNamespace(identity.NamespaceUser, identity.NamespaceServiceAccount) { + if requester.GetUID().IsType(identity.TypeUser, identity.TypeServiceAccount) { uid = requester.GetUID().ID() } diff --git a/pkg/services/live/live.go b/pkg/services/live/live.go index 25632794924..1b4473807f1 100644 --- a/pkg/services/live/live.go +++ b/pkg/services/live/live.go @@ -284,7 +284,7 @@ func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, r g.websocketHandler = func(ctx *contextmodel.ReqContext) { user := ctx.SignedInUser - _, identifier := user.GetNamespacedID() + _, identifier := user.GetTypedID() // Centrifuge expects Credentials in context with a current user ID. cred := ¢rifuge.Credentials{ @@ -955,7 +955,7 @@ func (g *GrafanaLive) HandleHTTPPublish(ctx *contextmodel.ReqContext) response.R return response.Error(http.StatusBadRequest, "invalid channel ID", nil) } - namespaceID, userID := ctx.SignedInUser.GetNamespacedID() + namespaceID, userID := ctx.SignedInUser.GetTypedID() logger.Debug("Publish API cmd", "namespaceID", namespaceID, "userID", userID, "channel", cmd.Channel) user := ctx.SignedInUser channel := cmd.Channel diff --git a/pkg/services/live/runstream/manager_test.go b/pkg/services/live/runstream/manager_test.go index 084794bbe6d..3b0d1b6107d 100644 --- a/pkg/services/live/runstream/manager_test.go +++ b/pkg/services/live/runstream/manager_test.go @@ -73,7 +73,7 @@ func TestStreamManager_SubmitStream_Send(t *testing.T) { } mockContextGetter.EXPECT().GetPluginContext(context.Background(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, user identity.Requester, pluginID string, datasourceUID string, skipCache bool) (backend.PluginContext, bool, error) { - userID, err := identity.IntIdentifier(user.GetNamespacedID()) + userID, err := identity.IntIdentifier(user.GetTypedID()) require.NoError(t, err) require.Equal(t, int64(2), userID) require.Equal(t, int64(1), user.GetOrgID()) @@ -258,7 +258,7 @@ func TestStreamManager_SubmitStream_ErrorRestartsRunStream(t *testing.T) { } mockContextGetter.EXPECT().GetPluginContext(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, user identity.Requester, pluginID string, datasourceUID string, skipCache bool) (backend.PluginContext, bool, error) { - userID, err := identity.IntIdentifier(user.GetNamespacedID()) + userID, err := identity.IntIdentifier(user.GetTypedID()) require.NoError(t, err) require.Equal(t, int64(2), userID) require.Equal(t, int64(1), user.GetOrgID()) @@ -343,7 +343,7 @@ func TestStreamManager_HandleDatasourceUpdate(t *testing.T) { } mockContextGetter.EXPECT().GetPluginContext(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, user identity.Requester, pluginID string, datasourceUID string, skipCache bool) (backend.PluginContext, bool, error) { - userID, err := identity.IntIdentifier(user.GetNamespacedID()) + userID, err := identity.IntIdentifier(user.GetTypedID()) require.NoError(t, err) require.Equal(t, int64(2), userID) @@ -412,7 +412,7 @@ func TestStreamManager_HandleDatasourceDelete(t *testing.T) { } mockContextGetter.EXPECT().GetPluginContext(context.Background(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, user identity.Requester, pluginID string, datasourceUID string, skipCache bool) (backend.PluginContext, bool, error) { - userID, err := identity.IntIdentifier(user.GetNamespacedID()) + userID, err := identity.IntIdentifier(user.GetTypedID()) require.NoError(t, err) require.Equal(t, int64(2), userID) require.Equal(t, int64(1), user.GetOrgID()) diff --git a/pkg/services/navtree/navtreeimpl/navtree.go b/pkg/services/navtree/navtreeimpl/navtree.go index 6abf4c2f62f..dadb110e0c7 100644 --- a/pkg/services/navtree/navtreeimpl/navtree.go +++ b/pkg/services/navtree/navtreeimpl/navtree.go @@ -297,7 +297,7 @@ func (s *ServiceImpl) getProfileNode(c *contextmodel.ReqContext) *navtree.NavLin func (s *ServiceImpl) buildStarredItemsNavLinks(c *contextmodel.ReqContext) ([]*navtree.NavLink, error) { starredItemsChildNavs := []*navtree.NavLink{} - userID, _ := identity.UserIdentifier(c.SignedInUser.GetNamespacedID()) + userID, _ := identity.UserIdentifier(c.SignedInUser.GetTypedID()) query := star.GetUserStarsQuery{ UserID: userID, } diff --git a/pkg/services/ngalert/api/api_ruler.go b/pkg/services/ngalert/api/api_ruler.go index b9d9da1fc9e..2f9f4273cc5 100644 --- a/pkg/services/ngalert/api/api_ruler.go +++ b/pkg/services/ngalert/api/api_ruler.go @@ -76,7 +76,7 @@ func (srv RulerSrv) RouteDeleteAlertRules(c *contextmodel.ReqContext, namespaceU return toNamespaceErrorResponse(err) } - userNamespace, id := c.SignedInUser.GetNamespacedID() + userNamespace, id := c.SignedInUser.GetTypedID() var loggerCtx = []any{ "userId", id, @@ -283,7 +283,7 @@ func (srv RulerSrv) RouteGetRulesConfig(c *contextmodel.ReqContext) response.Res for groupKey, rules := range configs { folder, ok := namespaceMap[groupKey.NamespaceUID] if !ok { - userNamespace, id := c.SignedInUser.GetNamespacedID() + userNamespace, id := c.SignedInUser.GetTypedID() srv.log.Error("Namespace not visible to the user", "user", id, "userNamespace", userNamespace, "namespace", groupKey.NamespaceUID) continue } @@ -359,7 +359,7 @@ func (srv RulerSrv) updateAlertRulesInGroup(c *contextmodel.ReqContext, groupKey var finalChanges *store.GroupDelta var dbConfig *ngmodels.AlertConfiguration err := srv.xactManager.InTransaction(c.Req.Context(), func(tranCtx context.Context) error { - userNamespace, id := c.SignedInUser.GetNamespacedID() + userNamespace, id := c.SignedInUser.GetTypedID() logger := srv.log.New("namespace_uid", groupKey.NamespaceUID, "group", groupKey.RuleGroup, "org_id", groupKey.OrgID, "user_id", id, "userNamespace", userNamespace) groupChanges, err := store.CalculateChanges(tranCtx, srv.store, groupKey, rules) @@ -454,7 +454,7 @@ func (srv RulerSrv) updateAlertRulesInGroup(c *contextmodel.ReqContext, groupKey } if len(finalChanges.New) > 0 { - userID, _ := identity.UserIdentifier(c.SignedInUser.GetNamespacedID()) + userID, _ := identity.UserIdentifier(c.SignedInUser.GetTypedID()) limitReached, err := srv.QuotaService.CheckQuotaReached(tranCtx, ngmodels.QuotaTargetSrv, "a.ScopeParameters{ OrgID: c.SignedInUser.GetOrgID(), UserID: userID, diff --git a/pkg/services/ngalert/provisioning/alert_rules.go b/pkg/services/ngalert/provisioning/alert_rules.go index c984b5a9730..0a29a8478ea 100644 --- a/pkg/services/ngalert/provisioning/alert_rules.go +++ b/pkg/services/ngalert/provisioning/alert_rules.go @@ -660,7 +660,7 @@ func (service *AlertRuleService) DeleteAlertRule(ctx context.Context, user ident func (service *AlertRuleService) checkLimitsTransactionCtx(ctx context.Context, user identity.Requester) error { // default to 0 if there is no user userID := int64(0) - u, err := identity.UserIdentifier(user.GetNamespacedID()) + u, err := identity.UserIdentifier(user.GetTypedID()) if err != nil { return fmt.Errorf("failed to check alert rule quota: %w", err) } diff --git a/pkg/services/oauthtoken/oauth_token.go b/pkg/services/oauthtoken/oauth_token.go index 79820f391ff..4974e7de7b5 100644 --- a/pkg/services/oauthtoken/oauth_token.go +++ b/pkg/services/oauthtoken/oauth_token.go @@ -94,8 +94,8 @@ func (o *Service) HasOAuthEntry(ctx context.Context, usr identity.Requester) (*l return nil, false, nil } - namespace, id := usr.GetNamespacedID() - if namespace != identity.NamespaceUser { + namespace, id := usr.GetTypedID() + if namespace != identity.TypeUser { // Not a user, therefore no token. return nil, false, nil } @@ -136,8 +136,8 @@ func (o *Service) TryTokenRefresh(ctx context.Context, usr identity.Requester) e return nil } - namespace, id := usr.GetNamespacedID() - if namespace != identity.NamespaceUser { + namespace, id := usr.GetTypedID() + if namespace != identity.TypeUser { // Not a user, therefore no token. logger.Warn("Can only refresh OAuth tokens for users", "namespace", namespace, "userId", id) return nil diff --git a/pkg/services/oauthtoken/oauth_token_test.go b/pkg/services/oauthtoken/oauth_token_test.go index 4cc0694598e..aee92134f95 100644 --- a/pkg/services/oauthtoken/oauth_token_test.go +++ b/pkg/services/oauthtoken/oauth_token_test.go @@ -174,13 +174,13 @@ func TestService_TryTokenRefresh(t *testing.T) { { desc: "should skip sync when identity is not a user", setup: func(env *environment) { - env.identity = &authn.Identity{ID: authn.MustParseNamespaceID("service-account:1")} + env.identity = &authn.Identity{ID: identity.MustParseTypedID("service-account:1")} }, }, { desc: "should skip token refresh and return nil if namespace and id cannot be converted to user ID", setup: func(env *environment) { - env.identity = &authn.Identity{ID: authn.MustParseNamespaceID("user:invalidIdentifierFormat")} + env.identity = &authn.Identity{ID: identity.MustParseTypedID("user:invalidIdentifierFormat")} }, }, { @@ -203,28 +203,28 @@ func TestService_TryTokenRefresh(t *testing.T) { env.identity = &authn.Identity{ AuthenticatedBy: login.GenericOAuthModule, - ID: authn.MustParseNamespaceID("user:1234"), + ID: identity.MustParseTypedID("user:1234"), } }, }, { desc: "should skip token refresh if the expiration check has already been cached", setup: func(env *environment) { - env.identity = &authn.Identity{ID: authn.MustParseNamespaceID("user:1234")} + env.identity = &authn.Identity{ID: identity.MustParseTypedID("user:1234")} env.cache.Set("oauth-refresh-token-1234", true, 1*time.Minute) }, }, { desc: "should skip token refresh if there's an unexpected error while looking up the user oauth entry, additionally, no error should be returned", setup: func(env *environment) { - env.identity = &authn.Identity{ID: authn.MustParseNamespaceID("user:1234")} + env.identity = &authn.Identity{ID: identity.MustParseTypedID("user:1234")} env.authInfoService.ExpectedError = errors.New("some error") }, }, { desc: "should skip token refresh if the user doesn't have an oauth entry", setup: func(env *environment) { - env.identity = &authn.Identity{ID: authn.MustParseNamespaceID("user:1234")} + env.identity = &authn.Identity{ID: identity.MustParseTypedID("user:1234")} env.authInfoService.ExpectedUserAuth = &login.UserAuth{ AuthModule: login.SAMLAuthModule, } @@ -233,7 +233,7 @@ func TestService_TryTokenRefresh(t *testing.T) { { desc: "should do token refresh if access token or id token have not expired yet", setup: func(env *environment) { - env.identity = &authn.Identity{ID: authn.MustParseNamespaceID("user:1234")} + env.identity = &authn.Identity{ID: identity.MustParseTypedID("user:1234")} env.authInfoService.ExpectedUserAuth = &login.UserAuth{ AuthModule: login.GenericOAuthModule, } @@ -242,7 +242,7 @@ func TestService_TryTokenRefresh(t *testing.T) { { desc: "should skip token refresh when no oauth provider was found", setup: func(env *environment) { - env.identity = &authn.Identity{ID: authn.MustParseNamespaceID("user:1234")} + env.identity = &authn.Identity{ID: identity.MustParseTypedID("user:1234")} env.authInfoService.ExpectedUserAuth = &login.UserAuth{ AuthModule: login.GenericOAuthModule, OAuthIdToken: EXPIRED_JWT, @@ -252,7 +252,7 @@ func TestService_TryTokenRefresh(t *testing.T) { { desc: "should skip token refresh when oauth provider token handling is disabled (UseRefreshToken is false)", setup: func(env *environment) { - env.identity = &authn.Identity{ID: authn.MustParseNamespaceID("user:1234")} + env.identity = &authn.Identity{ID: identity.MustParseTypedID("user:1234")} env.authInfoService.ExpectedUserAuth = &login.UserAuth{ AuthModule: login.GenericOAuthModule, OAuthIdToken: EXPIRED_JWT, @@ -265,7 +265,7 @@ func TestService_TryTokenRefresh(t *testing.T) { { desc: "should skip token refresh when there is no refresh token", setup: func(env *environment) { - env.identity = &authn.Identity{ID: authn.MustParseNamespaceID("user:1234")} + env.identity = &authn.Identity{ID: identity.MustParseTypedID("user:1234")} env.authInfoService.ExpectedUserAuth = &login.UserAuth{ AuthModule: login.GenericOAuthModule, OAuthIdToken: EXPIRED_JWT, @@ -285,7 +285,7 @@ func TestService_TryTokenRefresh(t *testing.T) { Expiry: time.Now().Add(-time.Hour), TokenType: "Bearer", } - env.identity = &authn.Identity{ID: authn.MustParseNamespaceID("user:1234")} + env.identity = &authn.Identity{ID: identity.MustParseTypedID("user:1234")} env.socialService.ExpectedAuthInfoProvider = &social.OAuthInfo{ UseRefreshToken: true, } @@ -310,7 +310,7 @@ func TestService_TryTokenRefresh(t *testing.T) { Expiry: time.Now().Add(time.Hour), TokenType: "Bearer", } - env.identity = &authn.Identity{ID: authn.MustParseNamespaceID("user:1234")} + env.identity = &authn.Identity{ID: identity.MustParseTypedID("user:1234")} env.socialService.ExpectedAuthInfoProvider = &social.OAuthInfo{ UseRefreshToken: true, } diff --git a/pkg/services/pluginsintegration/clientmiddleware/user_header_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/user_header_middleware.go index 466ee2f2d90..ae6592eb958 100644 --- a/pkg/services/pluginsintegration/clientmiddleware/user_header_middleware.go +++ b/pkg/services/pluginsintegration/clientmiddleware/user_header_middleware.go @@ -35,8 +35,8 @@ func (m *UserHeaderMiddleware) applyUserHeader(ctx context.Context, h backend.Fo } h.DeleteHTTPHeader(proxyutil.UserHeaderName) - namespace, _ := reqCtx.SignedInUser.GetNamespacedID() - if namespace != identity.NamespaceAnonymous { + namespace, _ := reqCtx.SignedInUser.GetTypedID() + if namespace != identity.TypeAnonymous { h.SetHTTPHeader(proxyutil.UserHeaderName, reqCtx.SignedInUser.GetLogin()) } } diff --git a/pkg/services/serviceaccounts/api/api.go b/pkg/services/serviceaccounts/api/api.go index 69b9c31e413..3fbfe8dde40 100644 --- a/pkg/services/serviceaccounts/api/api.go +++ b/pkg/services/serviceaccounts/api/api.go @@ -7,10 +7,10 @@ import ( "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/middleware/requestmeta" "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/authn" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/org" @@ -99,7 +99,8 @@ func (api *ServiceAccountsAPI) CreateServiceAccount(c *contextmodel.ReqContext) } if api.cfg.RBAC.PermissionsOnCreation("service-account") { - if c.SignedInUser.GetID().IsNamespace(authn.NamespaceUser) { + t, _ := c.SignedInUser.GetTypedID() + if t == identity.TypeUser { userID, err := c.SignedInUser.GetID().ParseInt() if err != nil { return response.Error(http.StatusInternalServerError, "Failed to parse user id", err) diff --git a/pkg/services/sqlstore/permissions/dashboard.go b/pkg/services/sqlstore/permissions/dashboard.go index 0b782348351..f80c4a8f3ee 100644 --- a/pkg/services/sqlstore/permissions/dashboard.go +++ b/pkg/services/sqlstore/permissions/dashboard.go @@ -166,8 +166,8 @@ func (f *accessControlDashboardPermissionFilter) buildClauses() { folderWildcards := accesscontrol.WildcardsFromPrefix(dashboards.ScopeFoldersPrefix) userID := int64(0) - namespace, identifier := f.user.GetNamespacedID() - if namespace == identity.NamespaceUser || namespace == identity.NamespaceServiceAccount { + namespace, identifier := f.user.GetTypedID() + if namespace == identity.TypeUser || namespace == identity.TypeServiceAccount { userID, _ = identity.IntIdentifier(namespace, identifier) } diff --git a/pkg/services/sqlstore/permissions/dashboard_filter_no_subquery.go b/pkg/services/sqlstore/permissions/dashboard_filter_no_subquery.go index 5cba8a0184e..79fcb84faf5 100644 --- a/pkg/services/sqlstore/permissions/dashboard_filter_no_subquery.go +++ b/pkg/services/sqlstore/permissions/dashboard_filter_no_subquery.go @@ -34,8 +34,8 @@ func (f *accessControlDashboardPermissionFilterNoFolderSubquery) buildClauses() folderWildcards := accesscontrol.WildcardsFromPrefix(dashboards.ScopeFoldersPrefix) userID := int64(0) - namespace, identifier := f.user.GetNamespacedID() - if namespace == identity.NamespaceUser || namespace == identity.NamespaceServiceAccount { + namespace, identifier := f.user.GetTypedID() + if namespace == identity.TypeUser || namespace == identity.TypeServiceAccount { userID, _ = identity.IntIdentifier(namespace, identifier) } diff --git a/pkg/services/star/api/api.go b/pkg/services/star/api/api.go index 516b8d08a74..8c6cbda3ed2 100644 --- a/pkg/services/star/api/api.go +++ b/pkg/services/star/api/api.go @@ -47,8 +47,8 @@ func (api *API) getDashboardHelper(ctx context.Context, orgID int64, id int64, u } func (api *API) GetStars(c *contextmodel.ReqContext) response.Response { - namespace, identifier := c.SignedInUser.GetNamespacedID() - if namespace != identity.NamespaceUser && namespace != identity.NamespaceServiceAccount { + namespace, identifier := c.SignedInUser.GetTypedID() + if namespace != identity.TypeUser && namespace != identity.TypeServiceAccount { return response.Error(http.StatusBadRequest, "Only users and service accounts can star dashboards", nil) } @@ -101,8 +101,8 @@ func (api *API) GetStars(c *contextmodel.ReqContext) response.Response { // 403: forbiddenError // 500: internalServerError func (api *API) StarDashboard(c *contextmodel.ReqContext) response.Response { - namespace, identifier := c.SignedInUser.GetNamespacedID() - if namespace != identity.NamespaceUser && namespace != identity.NamespaceServiceAccount { + namespace, identifier := c.SignedInUser.GetTypedID() + if namespace != identity.TypeUser && namespace != identity.TypeServiceAccount { return response.Error(http.StatusBadRequest, "Only users and service accounts can star dashboards", nil) } @@ -146,8 +146,8 @@ func (api *API) StarDashboardByUID(c *contextmodel.ReqContext) response.Response return response.Error(http.StatusBadRequest, "Invalid dashboard UID", nil) } - namespace, identifier := c.SignedInUser.GetNamespacedID() - if namespace != identity.NamespaceUser && namespace != identity.NamespaceServiceAccount { + namespace, identifier := c.SignedInUser.GetTypedID() + if namespace != identity.TypeUser && namespace != identity.TypeServiceAccount { return response.Error(http.StatusBadRequest, "Only users and service accounts can star dashboards", nil) } @@ -193,8 +193,8 @@ func (api *API) UnstarDashboard(c *contextmodel.ReqContext) response.Response { return response.Error(http.StatusBadRequest, "Invalid dashboard ID", nil) } - namespace, identifier := c.SignedInUser.GetNamespacedID() - if namespace != identity.NamespaceUser && namespace != identity.NamespaceServiceAccount { + namespace, identifier := c.SignedInUser.GetTypedID() + if namespace != identity.TypeUser && namespace != identity.TypeServiceAccount { return response.Error(http.StatusBadRequest, "Only users and service accounts can star dashboards", nil) } @@ -233,8 +233,8 @@ func (api *API) UnstarDashboardByUID(c *contextmodel.ReqContext) response.Respon return response.Error(http.StatusBadRequest, "Invalid dashboard UID", nil) } - namespace, identifier := c.SignedInUser.GetNamespacedID() - if namespace != identity.NamespaceUser && namespace != identity.NamespaceServiceAccount { + namespace, identifier := c.SignedInUser.GetTypedID() + if namespace != identity.TypeUser && namespace != identity.TypeServiceAccount { return response.Error(http.StatusBadRequest, "Only users and service accounts can star dashboards", nil) } diff --git a/pkg/services/team/teamapi/team.go b/pkg/services/team/teamapi/team.go index 79b697cd99c..eea490618ef 100644 --- a/pkg/services/team/teamapi/team.go +++ b/pkg/services/team/teamapi/team.go @@ -49,9 +49,9 @@ func (tapi *TeamAPI) createTeam(c *contextmodel.ReqContext) response.Response { // if the request is authenticated using API tokens // the SignedInUser is an empty struct therefore // an additional check whether it is an actual user is required - namespace, identifier := c.SignedInUser.GetNamespacedID() + namespace, identifier := c.SignedInUser.GetTypedID() switch namespace { - case identity.NamespaceUser: + case identity.TypeUser: userID, err := strconv.ParseInt(identifier, 10, 64) if err != nil { c.Logger.Error("Could not add creator to team because user id is not a number", "error", err) diff --git a/pkg/services/user/identity.go b/pkg/services/user/identity.go index acb12b59a6c..a6100ded51e 100644 --- a/pkg/services/user/identity.go +++ b/pkg/services/user/identity.go @@ -43,7 +43,7 @@ type SignedInUser struct { // IDToken is a signed token representing the identity that can be forwarded to plugins and external services. // Will only be set when featuremgmt.FlagIdForwarding is enabled. IDToken string `json:"-" xorm:"-"` - NamespacedID identity.NamespaceID + NamespacedID identity.TypedID } func (u *SignedInUser) ShouldUpdateLastSeenAt() bool { @@ -98,7 +98,7 @@ func (u *SignedInUser) GetAllowedKubernetesNamespace() string { // GetCacheKey returns a unique key for the entity. // Add an extra prefix to avoid collisions with other caches func (u *SignedInUser) GetCacheKey() string { - namespace, id := u.GetNamespacedID() + namespace, id := u.GetTypedID() if !u.HasUniqueId() { // Hack use the org role as id for identities that do not have a unique id // e.g. anonymous and render key. @@ -173,46 +173,46 @@ func (u *SignedInUser) GetOrgRole() identity.RoleType { } // GetID returns namespaced id for the entity -func (u *SignedInUser) GetID() identity.NamespaceID { - ns, id := u.GetNamespacedID() - return identity.NewNamespaceIDString(ns, id) +func (u *SignedInUser) GetID() identity.TypedID { + ns, id := u.GetTypedID() + return identity.NewTypedIDString(ns, id) } -// GetNamespacedID returns the namespace and ID of the active entity +// GetTypedID returns the namespace and ID of the active entity // The namespace is one of the constants defined in pkg/apimachinery/identity -func (u *SignedInUser) GetNamespacedID() (identity.Namespace, string) { +func (u *SignedInUser) GetTypedID() (identity.IdentityType, string) { switch { case u.ApiKeyID != 0: - return identity.NamespaceAPIKey, strconv.FormatInt(u.ApiKeyID, 10) + return identity.TypeAPIKey, strconv.FormatInt(u.ApiKeyID, 10) case u.IsServiceAccount: - return identity.NamespaceServiceAccount, strconv.FormatInt(u.UserID, 10) + return identity.TypeServiceAccount, strconv.FormatInt(u.UserID, 10) case u.UserID > 0: - return identity.NamespaceUser, strconv.FormatInt(u.UserID, 10) + return identity.TypeUser, strconv.FormatInt(u.UserID, 10) case u.IsAnonymous: - return identity.NamespaceAnonymous, "0" + return identity.TypeAnonymous, "0" case u.AuthenticatedBy == "render" && u.UserID == 0: - return identity.NamespaceRenderService, "0" + return identity.TypeRenderService, "0" } - return u.NamespacedID.Namespace(), u.NamespacedID.ID() + return u.NamespacedID.Type(), u.NamespacedID.ID() } // GetUID returns namespaced uid for the entity -func (u *SignedInUser) GetUID() identity.NamespaceID { +func (u *SignedInUser) GetUID() identity.TypedID { switch { case u.ApiKeyID != 0: - return identity.NewNamespaceIDString(identity.NamespaceAPIKey, strconv.FormatInt(u.ApiKeyID, 10)) + return identity.NewTypedIDString(identity.TypeAPIKey, strconv.FormatInt(u.ApiKeyID, 10)) case u.IsServiceAccount: - return identity.NewNamespaceIDString(identity.NamespaceServiceAccount, u.UserUID) + return identity.NewTypedIDString(identity.TypeServiceAccount, u.UserUID) case u.UserID > 0: - return identity.NewNamespaceIDString(identity.NamespaceUser, u.UserUID) + return identity.NewTypedIDString(identity.TypeUser, u.UserUID) case u.IsAnonymous: - return identity.NewNamespaceIDString(identity.NamespaceAnonymous, "0") + return identity.NewTypedIDString(identity.TypeAnonymous, "0") case u.AuthenticatedBy == "render" && u.UserID == 0: - return identity.NewNamespaceIDString(identity.NamespaceRenderService, "0") + return identity.NewTypedIDString(identity.TypeRenderService, "0") } - return identity.NewNamespaceIDString(identity.NamespaceEmpty, "0") + return identity.NewTypedIDString(identity.TypeEmpty, "0") } func (u *SignedInUser) GetAuthID() string { diff --git a/pkg/services/user/userimpl/verifier.go b/pkg/services/user/userimpl/verifier.go index 500216208d0..4bbb9b6ea3c 100644 --- a/pkg/services/user/userimpl/verifier.go +++ b/pkg/services/user/userimpl/verifier.go @@ -8,6 +8,7 @@ import ( "time" "github.com/grafana/grafana/pkg/apimachinery/errutil" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/authn" "github.com/grafana/grafana/pkg/services/notifications" @@ -152,6 +153,6 @@ func (s *Verifier) Complete(ctx context.Context, cmd user.CompleteEmailVerifyCom // remove the current token, so a new one can be generated with correct values. return s.is.RemoveIDToken( ctx, - &authn.Identity{ID: authn.NewNamespaceID(authn.NamespaceUser, usr.ID), OrgID: usr.OrgID}, + &authn.Identity{ID: identity.NewTypedID(identity.TypeUser, usr.ID), OrgID: usr.OrgID}, ) } diff --git a/pkg/storage/unified/apistore/store_test.go b/pkg/storage/unified/apistore/store_test.go index 5c9c12745bf..3ac93500675 100644 --- a/pkg/storage/unified/apistore/store_test.go +++ b/pkg/storage/unified/apistore/store_test.go @@ -32,7 +32,7 @@ func init() { // Make sure there is a user in every context storagetesting.NewContext = func() context.Context { testUserA := &identity.StaticRequester{ - Namespace: identity.NamespaceUser, + Type: identity.TypeUser, Login: "testuser", UserID: 123, UserUID: "u123", diff --git a/pkg/storage/unified/resource/grpc/authenticator.go b/pkg/storage/unified/resource/grpc/authenticator.go index 7aed3e5b3bb..e4c59996eaa 100644 --- a/pkg/storage/unified/resource/grpc/authenticator.go +++ b/pkg/storage/unified/resource/grpc/authenticator.go @@ -76,7 +76,7 @@ func (f *Authenticator) decodeMetadata(ctx context.Context, meta metadata.MD) (i // TODO, remove after this has been deployed to unified storage if getter(mdUserID) == "" { var err error - user.Namespace = identity.NamespaceUser + user.Type = identity.TypeUser user.UserID, err = strconv.ParseInt(getter("grafana-userid"), 10, 64) if err != nil { return nil, fmt.Errorf("invalid user id: %w", err) @@ -88,17 +88,17 @@ func (f *Authenticator) decodeMetadata(ctx context.Context, meta metadata.MD) (i return user, nil } - ns, err := identity.ParseNamespaceID(getter(mdUserID)) + ns, err := identity.ParseTypedID(getter(mdUserID)) if err != nil { return nil, fmt.Errorf("invalid user id: %w", err) } - user.Namespace = ns.Namespace() + user.Type = ns.Type() user.UserID, err = ns.ParseInt() if err != nil { return nil, fmt.Errorf("invalid user id: %w", err) } - ns, err = identity.ParseNamespaceID(getter(mdUserUID)) + ns, err = identity.ParseTypedID(getter(mdUserUID)) if err != nil { return nil, fmt.Errorf("invalid user id: %w", err) } diff --git a/pkg/storage/unified/resource/grpc/authenticator_test.go b/pkg/storage/unified/resource/grpc/authenticator_test.go index 2167b996991..bcd4aadca72 100644 --- a/pkg/storage/unified/resource/grpc/authenticator_test.go +++ b/pkg/storage/unified/resource/grpc/authenticator_test.go @@ -11,13 +11,13 @@ import ( func TestBasicEncodeDecode(t *testing.T) { before := &identity.StaticRequester{ - UserID: 123, - UserUID: "abc", - Login: "test", - Namespace: identity.NamespaceUser, - OrgID: 456, - OrgName: "org", - OrgRole: identity.RoleAdmin, + UserID: 123, + UserUID: "abc", + Login: "test", + Type: identity.TypeUser, + OrgID: 456, + OrgName: "org", + OrgRole: identity.RoleAdmin, } auth := &Authenticator{} diff --git a/pkg/storage/unified/resource/server.go b/pkg/storage/unified/resource/server.go index 45b5c3ea084..5ec6f2e4015 100644 --- a/pkg/storage/unified/resource/server.go +++ b/pkg/storage/unified/resource/server.go @@ -108,7 +108,7 @@ func NewResourceServer(opts ResourceServerOptions) (ResourceServer, error) { // Make this cancelable ctx, cancel := context.WithCancel(identity.WithRequester(context.Background(), &identity.StaticRequester{ - Namespace: identity.NamespaceServiceAccount, + Type: identity.TypeServiceAccount, Login: "watcher", // admin user for watch UserID: 1, IsGrafanaAdmin: true, diff --git a/pkg/storage/unified/resource/server_test.go b/pkg/storage/unified/resource/server_test.go index 62c2a85e77d..54a2d51dfb8 100644 --- a/pkg/storage/unified/resource/server_test.go +++ b/pkg/storage/unified/resource/server_test.go @@ -19,7 +19,7 @@ import ( func TestSimpleServer(t *testing.T) { testUserA := &identity.StaticRequester{ - Namespace: identity.NamespaceUser, + Type: identity.TypeUser, Login: "testuser", UserID: 123, UserUID: "u123", diff --git a/pkg/util/proxyutil/proxyutil.go b/pkg/util/proxyutil/proxyutil.go index 59b69a3f1ab..6cc16b3bd2f 100644 --- a/pkg/util/proxyutil/proxyutil.go +++ b/pkg/util/proxyutil/proxyutil.go @@ -114,8 +114,8 @@ func ApplyUserHeader(sendUserHeader bool, req *http.Request, user identity.Reque return } - namespace, _ := user.GetNamespacedID() - if namespace == identity.NamespaceUser || namespace == identity.NamespaceServiceAccount { + namespace, _ := user.GetTypedID() + if namespace == identity.TypeUser || namespace == identity.TypeServiceAccount { req.Header.Set(UserHeaderName, user.GetLogin()) } } diff --git a/pkg/util/proxyutil/proxyutil_test.go b/pkg/util/proxyutil/proxyutil_test.go index 14111cd49ac..1e1ca69c13d 100644 --- a/pkg/util/proxyutil/proxyutil_test.go +++ b/pkg/util/proxyutil/proxyutil_test.go @@ -175,7 +175,7 @@ func TestApplyUserHeader(t *testing.T) { require.NoError(t, err) req.Header.Set("X-Grafana-User", "admin") - ApplyUserHeader(false, req, &user.SignedInUser{Login: "admin", NamespacedID: identity.MustParseNamespaceID("user:1")}) + ApplyUserHeader(false, req, &user.SignedInUser{Login: "admin", NamespacedID: identity.MustParseTypedID("user:1")}) require.NotContains(t, req.Header, "X-Grafana-User") }) @@ -192,7 +192,7 @@ func TestApplyUserHeader(t *testing.T) { req, err := http.NewRequest(http.MethodGet, "/", nil) require.NoError(t, err) - ApplyUserHeader(true, req, &user.SignedInUser{IsAnonymous: true, NamespacedID: identity.MustParseNamespaceID("anonymous:0")}) + ApplyUserHeader(true, req, &user.SignedInUser{IsAnonymous: true, NamespacedID: identity.MustParseTypedID("anonymous:0")}) require.NotContains(t, req.Header, "X-Grafana-User") }) @@ -200,7 +200,7 @@ func TestApplyUserHeader(t *testing.T) { req, err := http.NewRequest(http.MethodGet, "/", nil) require.NoError(t, err) - ApplyUserHeader(true, req, &user.SignedInUser{Login: "admin", NamespacedID: identity.MustParseNamespaceID("user:1")}) + ApplyUserHeader(true, req, &user.SignedInUser{Login: "admin", NamespacedID: identity.MustParseTypedID("user:1")}) require.Equal(t, "admin", req.Header.Get("X-Grafana-User")) }) }