diff --git a/pkg/registry/apis/provisioning/register.go b/pkg/registry/apis/provisioning/register.go index 53f1e8c1717..77b4d9b7d50 100644 --- a/pkg/registry/apis/provisioning/register.go +++ b/pkg/registry/apis/provisioning/register.go @@ -50,7 +50,14 @@ import ( const repoControllerWorkers = 1 -var _ builder.APIGroupBuilder = (*APIBuilder)(nil) +var ( + _ builder.APIGroupBuilder = (*APIBuilder)(nil) + _ builder.APIGroupMutation = (*APIBuilder)(nil) + _ builder.APIGroupValidation = (*APIBuilder)(nil) + _ builder.APIGroupRouteProvider = (*APIBuilder)(nil) + _ builder.APIGroupPostStartHookProvider = (*APIBuilder)(nil) + _ builder.OpenAPIPostProcessor = (*APIBuilder)(nil) +) type APIBuilder struct { urlProvider func(namespace string) string diff --git a/pkg/services/apiserver/auth/authorizer/org_id.go b/pkg/services/apiserver/auth/authorizer/org_id.go index 87b38d2f9c2..a76f55e73d7 100644 --- a/pkg/services/apiserver/auth/authorizer/org_id.go +++ b/pkg/services/apiserver/auth/authorizer/org_id.go @@ -60,6 +60,11 @@ func (auth orgIDAuthorizer) Authorize(ctx context.Context, a authorizer.Attribut return authorizer.DecisionNoOpinion, "", nil } + // If we have an anonymous user, let the next authorizers decide. + if signedInUser.GetIdentityType() == claims.TypeAnonymous { + return authorizer.DecisionNoOpinion, "", nil + } + // Check if the user has access to the specified org // nolint:staticcheck userId, err := signedInUser.GetInternalID() diff --git a/pkg/services/apiserver/auth/authorizer/stack_id.go b/pkg/services/apiserver/auth/authorizer/stack_id.go index 99a53bef997..2232335984d 100644 --- a/pkg/services/apiserver/auth/authorizer/stack_id.go +++ b/pkg/services/apiserver/auth/authorizer/stack_id.go @@ -37,6 +37,11 @@ func (auth stackIDAuthorizer) Authorize(ctx context.Context, a authorizer.Attrib return authorizer.DecisionDeny, fmt.Sprintf("error getting signed in user: %v", err), nil } + // If we have an anonymous user, let the next authorizers decide. + if signedInUser.GetIdentityType() == claims.TypeAnonymous { + return authorizer.DecisionNoOpinion, "", nil + } + info, err := claims.ParseNamespace(a.GetNamespace()) if err != nil { return authorizer.DecisionDeny, fmt.Sprintf("error reading namespace: %v", err), nil diff --git a/pkg/services/authn/authn.go b/pkg/services/authn/authn.go index fecfc17bf3a..d7fab0a65b3 100644 --- a/pkg/services/authn/authn.go +++ b/pkg/services/authn/authn.go @@ -31,6 +31,7 @@ const ( ClientSAML = "auth.client.saml" ClientPasswordless = "auth.client.passwordless" ClientLDAP = "ldap" + ClientProvisioning = "auth.client.apiserver.provisioning" ) const ( diff --git a/pkg/services/authn/authnimpl/registration.go b/pkg/services/authn/authnimpl/registration.go index 593a0d823fc..e86fe283da7 100644 --- a/pkg/services/authn/authnimpl/registration.go +++ b/pkg/services/authn/authnimpl/registration.go @@ -124,6 +124,10 @@ func ProvideRegistration( authnSvc.RegisterClient(clients.ProvideOAuth(clientName, cfg, oauthTokenService, socialService, settingsProviderService, features)) } + if features.IsEnabledGlobally(featuremgmt.FlagProvisioning) { + authnSvc.RegisterClient(clients.ProvideProvisioning()) + } + // FIXME (jguer): move to User package userSync := sync.ProvideUserSync(userService, userProtectionService, authInfoService, quotaService, tracer, features) orgSync := sync.ProvideOrgSync(userService, orgService, accessControlService, cfg, tracer) diff --git a/pkg/services/authn/clients/provisioning.go b/pkg/services/authn/clients/provisioning.go new file mode 100644 index 00000000000..c396f999d65 --- /dev/null +++ b/pkg/services/authn/clients/provisioning.go @@ -0,0 +1,62 @@ +package clients + +import ( + "context" + "fmt" + "regexp" + "time" + + claims "github.com/grafana/authlib/types" + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/services/authn" +) + +var ( + _ authn.ContextAwareClient = (*Provisioning)(nil) + _ fmt.Stringer = (*Provisioning)(nil) // for debugging +) + +type Provisioning struct { + webhookRegexp *regexp.Regexp +} + +func ProvideProvisioning() *Provisioning { + // It's fine to compile a regexp here. The function is only called once per instance of APIBuilder, of which there should only ever be 1. + // Assumption: APIVERSION has no leading or trailing slashes. + webhookRegexp := regexp.MustCompile("^/apis/" + regexp.QuoteMeta(provisioning.APIVERSION) + "/namespaces/[^/]+/repositories/[^/]+/webhook$") + return &Provisioning{webhookRegexp} +} + +func (p *Provisioning) String() string { + return p.Name() +} + +func (*Provisioning) Name() string { + return authn.ClientProvisioning +} + +func (p *Provisioning) Authenticate(ctx context.Context, r *authn.Request) (*authn.Identity, error) { + return &authn.Identity{ + Type: claims.TypeAnonymous, + Name: p.Name(), + UID: p.Name(), + Login: p.Name(), + AuthID: p.Name(), + OrgID: r.OrgID, + AuthenticatedBy: authn.ClientProvisioning, + LastSeenAt: time.Now(), + }, nil +} + +func (*Provisioning) IsEnabled() bool { + return true +} + +func (p *Provisioning) Test(ctx context.Context, r *authn.Request) bool { + path := r.HTTPRequest.URL.Path + return p.webhookRegexp.MatchString(path) +} + +func (*Provisioning) Priority() uint { + return 5 // let most other clients go first +} diff --git a/pkg/services/authn/clients/provisioning_test.go b/pkg/services/authn/clients/provisioning_test.go new file mode 100644 index 00000000000..0788e713cfa --- /dev/null +++ b/pkg/services/authn/clients/provisioning_test.go @@ -0,0 +1,72 @@ +package clients + +import ( + "context" + "net/http" + "net/url" + "strings" + "testing" + + "github.com/grafana/authlib/types" + provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1" + "github.com/grafana/grafana/pkg/services/authn" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestProvisioning_Test(t *testing.T) { + t.Parallel() + ctx := context.Background() + + for _, tt := range []struct { + Name, URL string + Valid bool + }{ + {"Root path is invalid", "https://grafana.localhost/", false}, + {"Non-provisioning path", "https://grafana.localhost/hello/world", false}, + {"Provisioning path that isn't webhook", "https://grafana.localhost/apis/provisioning.grafana.app/v0alpha1/namespaces/x/repositories/y/unittest", false}, + {"Webhook path", "https://grafana.localhost/apis/provisioning.grafana.app/v0alpha1/namespaces/x/repositories/y/webhook", true}, + {"Webhook path with subpath", "https://grafana.localhost/apis/provisioning.grafana.app/v0alpha1/namespaces/x/repositories/y/webhook/unittest", false}, // this'll have to change if we ever want subpaths + } { + t.Run(tt.Name, func(t *testing.T) { + t.Parallel() + svc := ProvideProvisioning() + + url, err := url.Parse(tt.URL) + require.NoError(t, err, "couldn't parse input URL") + req := &authn.Request{HTTPRequest: &http.Request{ + URL: url, + }} + + assert.Equal(t, tt.Valid, svc.Test(ctx, req)) + }) + } +} + +func TestProvisioning_Authenticate(t *testing.T) { + t.Parallel() + ctx := context.Background() + + url, err := url.Parse("https://grafana.localhost/apis/provisioning.grafana.app/v0alpha1/namespaces/x/repositories/y/webhook") + require.NoError(t, err, "couldn't parse URL known to be good?") + req := &authn.Request{HTTPRequest: &http.Request{ + URL: url, + }} + + svc := ProvideProvisioning() + identity, err := svc.Authenticate(ctx, req) + require.NoError(t, err, "Authenticate shouldn't err") + + assert.Equal(t, types.TypeAnonymous, identity.GetIdentityType(), "IdentityType") + assert.Equal(t, "auth.client.apiserver.provisioning", identity.UID, "UID") + assert.Equal(t, "auth.client.apiserver.provisioning", identity.Login, "Login") + assert.Equal(t, "auth.client.apiserver.provisioning", identity.AuthenticatedBy, "AuthenticatedBy") + assert.Equal(t, false, identity.GetIsGrafanaAdmin(), "IsAdmin") +} + +func TestProvisioning_Assumptions(t *testing.T) { + t.Run("APIVERSION constant has no leading or trailing slashes", func(t *testing.T) { + assert.False(t, strings.HasPrefix(provisioning.APIVERSION, "/"), "found leading / in APIVERSION: %s", provisioning.APIVERSION) + assert.False(t, strings.HasSuffix(provisioning.APIVERSION, "/"), "found trailing / in APIVERSION: %s", provisioning.APIVERSION) + }) +} diff --git a/public/app/features/provisioning/SetupWarnings.tsx b/public/app/features/provisioning/SetupWarnings.tsx index 26cc98b7947..86db36e1a48 100644 --- a/public/app/features/provisioning/SetupWarnings.tsx +++ b/public/app/features/provisioning/SetupWarnings.tsx @@ -33,10 +33,7 @@ dualWriterMode = 5 # For Github webhook support, you will need something like: [server] -root_url = https://supreme-exact-beetle.ngrok-free.app - -[auth.anonymous] -enabled = true`; +root_url = https://supreme-exact-beetle.ngrok-free.app`; const ngrok_example = `ngrok http 3000 @@ -61,10 +58,7 @@ HTTP Requests const webhook_ini = `... [server] -root_url = https://d60d-83-33-235-27.ngrok-free.app - -[auth.anonymous] -enabled = true`; +root_url = https://d60d-83-33-235-27.ngrok-free.app`; export function SetupWarnings() { const [isCustomIniOpen, setCustomIniOpen] = useLocalStorage('collapse_custom_ini', true); @@ -112,9 +106,7 @@ export function SetupWarnings() { - - Webhook support requires the server to run on a public URL — and (for now) anonymous access - + Webhook support requires the server to run on a public URL.
             {webhook_ini}