mirror of
https://github.com/grafana/grafana.git
synced 2025-08-01 08:33:00 +08:00
Authz: Setup access claims for service identity (#100986)
* Setup access claims for service identity and add them to identityes without any claims
This commit is contained in:
@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/grafana/authlib/authn"
|
||||
"github.com/grafana/authlib/types"
|
||||
)
|
||||
|
||||
@ -30,27 +31,34 @@ func checkNilRequester(r Requester) bool {
|
||||
return r == nil || (reflect.ValueOf(r).Kind() == reflect.Ptr && reflect.ValueOf(r).IsNil())
|
||||
}
|
||||
|
||||
const serviceName = "service"
|
||||
const serviceNameForProvisioning = "provisioning"
|
||||
const (
|
||||
serviceName = "service"
|
||||
serviceNameForProvisioning = "provisioning"
|
||||
)
|
||||
|
||||
// WithServiceIdentity sets an identity representing the service itself in provided org and store it in context.
|
||||
// This is useful for background tasks that has to communicate with unfied storage. It also returns a Requester with
|
||||
// static permissions so it can be used in legacy code paths.
|
||||
func WithServiceIdentity(ctx context.Context, orgID int64) (context.Context, Requester) {
|
||||
r := &StaticRequester{
|
||||
func newInternalIdentity(name string, namespace string, orgID int64) Requester {
|
||||
return &StaticRequester{
|
||||
Type: types.TypeAccessPolicy,
|
||||
Name: serviceName,
|
||||
UserUID: serviceName,
|
||||
AuthID: serviceName,
|
||||
Login: serviceName,
|
||||
Name: name,
|
||||
UserUID: name,
|
||||
AuthID: name,
|
||||
Login: name,
|
||||
OrgRole: RoleAdmin,
|
||||
Namespace: namespace,
|
||||
IsGrafanaAdmin: true,
|
||||
OrgID: orgID,
|
||||
Permissions: map[int64]map[string][]string{
|
||||
orgID: serviceIdentityPermissions,
|
||||
},
|
||||
AccessTokenClaims: ServiceIdentityClaims,
|
||||
}
|
||||
}
|
||||
|
||||
// WithServiceIdentity sets an identity representing the service itself in provided org and store it in context.
|
||||
// This is useful for background tasks that has to communicate with unfied storage. It also returns a Requester with
|
||||
// static permissions so it can be used in legacy code paths.
|
||||
func WithServiceIdentity(ctx context.Context, orgID int64) (context.Context, Requester) {
|
||||
r := newInternalIdentity(serviceName, "", orgID)
|
||||
return WithRequester(ctx, r), r
|
||||
}
|
||||
|
||||
@ -60,21 +68,7 @@ func WithProvisioningIdentitiy(ctx context.Context, namespace string) (context.C
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
r := &StaticRequester{
|
||||
Type: types.TypeAccessPolicy,
|
||||
Name: serviceNameForProvisioning,
|
||||
UserUID: serviceNameForProvisioning,
|
||||
AuthID: serviceNameForProvisioning,
|
||||
Login: serviceNameForProvisioning,
|
||||
OrgRole: RoleAdmin,
|
||||
IsGrafanaAdmin: true,
|
||||
Namespace: namespace,
|
||||
OrgID: ns.OrgID,
|
||||
Permissions: map[int64]map[string][]string{
|
||||
ns.OrgID: serviceIdentityPermissions,
|
||||
},
|
||||
}
|
||||
|
||||
r := newInternalIdentity(serviceNameForProvisioning, ns.Value, ns.OrgID)
|
||||
return WithRequester(ctx, r), r, nil
|
||||
}
|
||||
|
||||
@ -97,6 +91,14 @@ func getWildcardPermissions(actions ...string) map[string][]string {
|
||||
return permissions
|
||||
}
|
||||
|
||||
func getTokenPermissions(groups ...string) []string {
|
||||
out := make([]string, 0, len(groups))
|
||||
for _, group := range groups {
|
||||
out = append(out, group+":*")
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// serviceIdentityPermissions is a list of wildcard permissions for provided actions.
|
||||
// We should add every action required "internally" here.
|
||||
var serviceIdentityPermissions = getWildcardPermissions(
|
||||
@ -118,6 +120,19 @@ var serviceIdentityPermissions = getWildcardPermissions(
|
||||
"teams:read", // accesscontrol.ActionTeamsRead,
|
||||
)
|
||||
|
||||
var serviceIdentityTokenPermissions = getTokenPermissions(
|
||||
"folder.grafana.app",
|
||||
"dashboard.grafana.app",
|
||||
"secret.grafana.app",
|
||||
)
|
||||
|
||||
var ServiceIdentityClaims = &authn.Claims[authn.AccessTokenClaims]{
|
||||
Rest: authn.AccessTokenClaims{
|
||||
Permissions: serviceIdentityTokenPermissions,
|
||||
DelegatedPermissions: serviceIdentityTokenPermissions,
|
||||
},
|
||||
}
|
||||
|
||||
func IsServiceIdentity(ctx context.Context) bool {
|
||||
ident, ok := types.AuthInfoFrom(ctx)
|
||||
if !ok {
|
||||
|
@ -30,10 +30,11 @@ type StaticRequester struct {
|
||||
Namespace string
|
||||
IsGrafanaAdmin bool
|
||||
// Permissions grouped by orgID and actions
|
||||
Permissions map[int64]map[string][]string
|
||||
IDToken string
|
||||
IDTokenClaims *authnlib.Claims[authnlib.IDTokenClaims]
|
||||
CacheKey string
|
||||
Permissions map[int64]map[string][]string
|
||||
IDToken string
|
||||
IDTokenClaims *authnlib.Claims[authnlib.IDTokenClaims]
|
||||
AccessTokenClaims *authnlib.Claims[authnlib.AccessTokenClaims]
|
||||
CacheKey string
|
||||
}
|
||||
|
||||
// GetID returns typed id for the entity
|
||||
@ -62,10 +63,16 @@ func (u *StaticRequester) GetAudience() []string {
|
||||
}
|
||||
|
||||
func (u *StaticRequester) GetTokenPermissions() []string {
|
||||
if u.AccessTokenClaims != nil {
|
||||
return u.AccessTokenClaims.Rest.Permissions
|
||||
}
|
||||
return []string{}
|
||||
}
|
||||
|
||||
func (u *StaticRequester) GetTokenDelegatedPermissions() []string {
|
||||
if u.AccessTokenClaims != nil {
|
||||
return u.AccessTokenClaims.Rest.DelegatedPermissions
|
||||
}
|
||||
return []string{}
|
||||
}
|
||||
|
||||
|
@ -145,5 +145,7 @@ func ProvideRegistration(
|
||||
|
||||
nsSync := sync.ProvideNamespaceSync(cfg)
|
||||
authnSvc.RegisterPostAuthHook(nsSync.SyncNamespace, 150)
|
||||
authnSvc.RegisterPostAuthHook(sync.AccessClaimsHook, 160)
|
||||
|
||||
return Registration{}
|
||||
}
|
||||
|
32
pkg/services/authn/authnimpl/sync/access_claims.go
Normal file
32
pkg/services/authn/authnimpl/sync/access_claims.go
Normal file
@ -0,0 +1,32 @@
|
||||
package sync
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
authnlib "github.com/grafana/authlib/authn"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/services/authn"
|
||||
)
|
||||
|
||||
func NewAccessClaimsSync() AccessClaimsSync {
|
||||
return AccessClaimsSync{}
|
||||
}
|
||||
|
||||
type AccessClaimsSync struct{}
|
||||
|
||||
func AccessClaimsHook(ctx context.Context, id *authn.Identity, _ *authn.Request) error {
|
||||
if id.AccessTokenClaims == nil {
|
||||
// When normal authencation flows are used withint grafana we don't have any access token e.g. using user
|
||||
// session. This makes it impossible to authorize using AccessClient because we don't have any access claims
|
||||
// with deletegated permissions. To get around this we use the hardcoded delegated
|
||||
// permissions.
|
||||
id.AccessTokenClaims = &authnlib.Claims[authnlib.AccessTokenClaims]{
|
||||
Rest: authnlib.AccessTokenClaims{
|
||||
DelegatedPermissions: identity.ServiceIdentityClaims.Rest.DelegatedPermissions,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -9,6 +9,7 @@ import (
|
||||
"github.com/go-jose/go-jose/v3/jwt"
|
||||
"github.com/grafana/authlib/authn"
|
||||
"github.com/grafana/authlib/types"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
)
|
||||
|
||||
type inProcExchanger struct {
|
||||
@ -37,8 +38,8 @@ func createInProcToken() (*authn.TokenExchangeResponse, error) {
|
||||
},
|
||||
Rest: authn.AccessTokenClaims{
|
||||
Namespace: "*",
|
||||
Permissions: []string{"folder.grafana.app:*", "dashboard.grafana.app:*"},
|
||||
DelegatedPermissions: []string{"folder.grafana.app:*", "dashboard.grafana.app:*"},
|
||||
Permissions: identity.ServiceIdentityClaims.Rest.Permissions,
|
||||
DelegatedPermissions: identity.ServiceIdentityClaims.Rest.DelegatedPermissions,
|
||||
},
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user