Add OAuth2 API scope registration and enforcement
Register v1 API scopes in coredata, advertise them in OIDC discovery and protected-resource metadata, show them on the consent screen, and enforce scope-to-action mapping in the IAM Authorizer before policy evaluation. Signed-off-by: Ludovic Vielle <ludovic@probo.com>
This commit is contained in:
@@ -16,7 +16,6 @@ import (
|
||||
"go.probo.inc/probo/pkg/agentrun"
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/iam"
|
||||
"go.probo.inc/probo/pkg/probo"
|
||||
"go.probo.inc/probo/pkg/server/api/authn"
|
||||
"go.probo.inc/probo/pkg/server/api/console/v1/schema"
|
||||
@@ -33,7 +32,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error
|
||||
|
||||
switch id.EntityType() {
|
||||
case coredata.OrganizationEntityType:
|
||||
action = iam.ActionOrganizationGet
|
||||
action = probo.ActionOrganizationGet
|
||||
loadNode = func(ctx context.Context, scope *coredata.Scope, id gid.GID) (types.Node, error) {
|
||||
organization, err := r.probo.Organizations.Get(ctx, scope, id)
|
||||
if err != nil {
|
||||
@@ -547,7 +546,7 @@ func (r *queryResolver) CommonThirdParties(ctx context.Context, name string) ([]
|
||||
func (r *queryResolver) AccessReviewDrivers(ctx context.Context) ([]*types.ConnectorProviderInfo, error) {
|
||||
identity := authn.IdentityFromContext(ctx)
|
||||
|
||||
if _, err := r.authorize(ctx, identity.ID, probo.ActionAccessReviewDriverCatalogList); err != nil {
|
||||
if _, err := r.authorize(ctx, identity.ID, accessreview.ActionDriverCatalogList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
"go.probo.inc/probo/pkg/connector"
|
||||
)
|
||||
|
||||
// oauthClientMetadata is the OAuth Client ID Metadata Document (CIMD)
|
||||
// oauth2ClientMetadata is the OAuth2 Client ID Metadata Document (CIMD)
|
||||
// published for public-client connectors. The deployment's
|
||||
// (baseURL + CIMDMetadataPath) URL is the OAuth client_id; providers such as
|
||||
// PostHog fetch this document server-to-server during authorization to learn
|
||||
@@ -37,7 +37,7 @@ const (
|
||||
proboLogoURI = "https://www.probo.com/probo-logo-only.svg"
|
||||
)
|
||||
|
||||
type oauthClientMetadata struct {
|
||||
type oauth2ClientMetadata struct {
|
||||
ClientID string `json:"client_id"`
|
||||
ClientName string `json:"client_name"`
|
||||
ClientURI string `json:"client_uri"`
|
||||
@@ -48,11 +48,11 @@ type oauthClientMetadata struct {
|
||||
ResponseTypes []string `json:"response_types"`
|
||||
}
|
||||
|
||||
// handleConnectorOAuthClientMetadata serves the public, unauthenticated CIMD
|
||||
// document. It is intentionally outside the auth middleware group: the OAuth
|
||||
// handleConnectorOAuth2ClientMetadata serves the public, unauthenticated CIMD
|
||||
// document. It is intentionally outside the auth middleware group: the OAuth2
|
||||
// provider fetches it without any Probo credentials.
|
||||
func handleConnectorOAuthClientMetadata(baseURL *baseurl.BaseURL) http.HandlerFunc {
|
||||
doc := oauthClientMetadata{
|
||||
func handleConnectorOAuth2ClientMetadata(baseURL *baseurl.BaseURL) http.HandlerFunc {
|
||||
doc := oauth2ClientMetadata{
|
||||
ClientID: baseURL.WithPath(connector.CIMDMetadataPath).MustString(),
|
||||
ClientName: "Probo",
|
||||
ClientURI: proboBrandURI,
|
||||
@@ -25,18 +25,18 @@ import (
|
||||
"go.probo.inc/probo/pkg/baseurl"
|
||||
)
|
||||
|
||||
// TestHandleConnectorOAuthClientMetadata verifies the public CIMD document:
|
||||
// TestHandleConnectorOAuth2ClientMetadata verifies the public CIMD document:
|
||||
// PostHog fetches it server-to-server during authorization, so client_id,
|
||||
// redirect_uris (derived from the deployment base URL) and the public-client
|
||||
// token_endpoint_auth_method must be exactly right or the OAuth flow breaks.
|
||||
func TestHandleConnectorOAuthClientMetadata(t *testing.T) {
|
||||
func TestHandleConnectorOAuth2ClientMetadata(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
base, err := baseurl.Parse("https://probo.example.com")
|
||||
require.NoError(t, err)
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
handleConnectorOAuthClientMetadata(base)(
|
||||
handleConnectorOAuth2ClientMetadata(base)(
|
||||
rec,
|
||||
httptest.NewRequest(http.MethodGet, "/api/console/v1/connectors/oauth-client-metadata", nil),
|
||||
)
|
||||
@@ -35,7 +35,7 @@ func NewAuthorizeFunc(logger *log.Logger) authz.AuthorizeFunc {
|
||||
return func(
|
||||
ctx context.Context,
|
||||
objectID gid.GID,
|
||||
action string,
|
||||
action iam.Action,
|
||||
options ...authz.AuthorizeFuncOption,
|
||||
) (*coredata.Scope, error) {
|
||||
loaders := FromContext(ctx)
|
||||
@@ -66,6 +66,10 @@ func NewAuthorizeFunc(logger *log.Logger) authz.AuthorizeFunc {
|
||||
return nil, gqlutils.Forbidden(ctx, err)
|
||||
}
|
||||
|
||||
if _, ok := errors.AsType[*iam.ErrInsufficientOAuth2Scope](err); ok {
|
||||
return nil, gqlutils.Forbidden(ctx, err)
|
||||
}
|
||||
|
||||
if errors.Is(err, coredata.ErrResourceNotFound) {
|
||||
return nil, gqlutils.NotFoundf(ctx, "resource not found")
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ type (
|
||||
// batch together.
|
||||
AuthorizeKey struct {
|
||||
ResourceID gid.GID
|
||||
Action string
|
||||
Action iam.Action
|
||||
ResourceAttributes string
|
||||
DryRun bool
|
||||
SkipAssumptionCheck bool
|
||||
|
||||
@@ -142,7 +142,7 @@ func NewMux(
|
||||
// is fetched server-to-server by public-client providers (PostHog)
|
||||
// during authorization, with no Probo credentials. Mounted outside the
|
||||
// auth group above.
|
||||
r.Get("/connectors/oauth-client-metadata", handleConnectorOAuthClientMetadata(baseURL))
|
||||
r.Get("/connectors/oauth-client-metadata", handleConnectorOAuth2ClientMetadata(baseURL))
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user