Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate metrics_view to explore resources (breaking) #5722

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 23 additions & 21 deletions admin/auth_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,15 +167,16 @@ func (t *magicAuthToken) OwnerID() string {

// IssueMagicAuthTokenOptions provides options for IssueMagicAuthToken.
type IssueMagicAuthTokenOptions struct {
ProjectID string
TTL *time.Duration
CreatedByUserID *string
Attributes map[string]any
MetricsView string
MetricsViewFilterJSON string
MetricsViewFields []string
State string
Title string
ProjectID string
TTL *time.Duration
CreatedByUserID *string
Attributes map[string]any
ResourceType string
ResourceName string
FilterJSON string
Fields []string
State string
Title string
}

// IssueMagicAuthToken generates and persists a new magic auth token for a project.
Expand All @@ -189,18 +190,19 @@ func (s *Service) IssueMagicAuthToken(ctx context.Context, opts *IssueMagicAuthT
}

dat, err := s.DB.InsertMagicAuthToken(ctx, &database.InsertMagicAuthTokenOptions{
ID: tkn.ID.String(),
SecretHash: tkn.SecretHash(),
Secret: tkn.Secret[:],
ProjectID: opts.ProjectID,
ExpiresOn: expiresOn,
CreatedByUserID: opts.CreatedByUserID,
Attributes: opts.Attributes,
MetricsView: opts.MetricsView,
MetricsViewFilterJSON: opts.MetricsViewFilterJSON,
MetricsViewFields: opts.MetricsViewFields,
State: opts.State,
Title: opts.Title,
ID: tkn.ID.String(),
SecretHash: tkn.SecretHash(),
Secret: tkn.Secret[:],
ProjectID: opts.ProjectID,
ExpiresOn: expiresOn,
CreatedByUserID: opts.CreatedByUserID,
Attributes: opts.Attributes,
ResourceType: opts.ResourceType,
ResourceName: opts.ResourceName,
FilterJSON: opts.FilterJSON,
Fields: opts.Fields,
State: opts.State,
Title: opts.Title,
})
if err != nil {
return nil, err
Expand Down
32 changes: 17 additions & 15 deletions admin/database/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -622,9 +622,10 @@ type MagicAuthToken struct {
UsedOn time.Time `db:"used_on"`
CreatedByUserID *string `db:"created_by_user_id"`
Attributes map[string]any `db:"attributes"`
MetricsView string `db:"metrics_view"`
MetricsViewFilterJSON string `db:"metrics_view_filter_json"`
MetricsViewFields []string `db:"metrics_view_fields"`
ResourceType string `db:"resource_type"`
ResourceName string `db:"resource_name"`
FilterJSON string `db:"filter_json"`
Fields []string `db:"fields"`
State string `db:"state"`
Title string `db:"title"`
}
Expand All @@ -637,18 +638,19 @@ type MagicAuthTokenWithUser struct {

// InsertMagicAuthTokenOptions defines options for creating a MagicAuthToken.
type InsertMagicAuthTokenOptions struct {
ID string
SecretHash []byte
Secret []byte
ProjectID string `validate:"required"`
ExpiresOn *time.Time
CreatedByUserID *string
Attributes map[string]any
MetricsView string `validate:"required"`
MetricsViewFilterJSON string
MetricsViewFields []string
State string
Title string
ID string
SecretHash []byte
Secret []byte
ProjectID string `validate:"required"`
ExpiresOn *time.Time
CreatedByUserID *string
Attributes map[string]any
ResourceType string `validate:"required"`
ResourceName string `validate:"required"`
FilterJSON string
Fields []string
State string
Title string
}

// AuthClient is a client that requests and consumes auth tokens.
Expand Down
1 change: 0 additions & 1 deletion admin/database/postgres/migrations/0045.sql
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,3 @@ CREATE TABLE billing_issues (
FOREIGN KEY (org_id) REFERENCES orgs (id) ON DELETE CASCADE,
CONSTRAINT billing_issues_org_id_type_unique UNIQUE (org_id, type)
);

6 changes: 6 additions & 0 deletions admin/database/postgres/migrations/0047.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
ALTER TABLE magic_auth_tokens ADD COLUMN resource_type TEXT NOT NULL DEFAULT 'rill.runtime.v1.Explore';
ALTER TABLE magic_auth_tokens RENAME COLUMN metrics_view TO resource_name;
ALTER TABLE magic_auth_tokens RENAME COLUMN metrics_view_filter_json TO filter_json;
ALTER TABLE magic_auth_tokens RENAME COLUMN metrics_view_fields TO fields;

UPDATE bookmarks SET resource_kind = 'rill.runtime.v1.Explore' WHERE resource_kind = 'rill.runtime.v1.MetricsView';
14 changes: 7 additions & 7 deletions admin/database/postgres/postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -1139,8 +1139,8 @@ func (c *connection) InsertMagicAuthToken(ctx context.Context, opts *database.In
return nil, err
}

if opts.MetricsViewFields == nil {
opts.MetricsViewFields = []string{}
if opts.Fields == nil {
opts.Fields = []string{}
}

encSecret, encKeyID, err := c.encrypt(opts.Secret)
Expand All @@ -1150,9 +1150,9 @@ func (c *connection) InsertMagicAuthToken(ctx context.Context, opts *database.In

res := &magicAuthTokenDTO{}
err = c.getDB(ctx).QueryRowxContext(ctx, `
INSERT INTO magic_auth_tokens (id, secret_hash, secret, secret_encryption_key_id, project_id, expires_on, created_by_user_id, attributes, metrics_view, metrics_view_filter_json, metrics_view_fields, state, title)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING *`,
opts.ID, opts.SecretHash, encSecret, encKeyID, opts.ProjectID, opts.ExpiresOn, opts.CreatedByUserID, opts.Attributes, opts.MetricsView, opts.MetricsViewFilterJSON, opts.MetricsViewFields, opts.State, opts.Title,
INSERT INTO magic_auth_tokens (id, secret_hash, secret, secret_encryption_key_id, project_id, expires_on, created_by_user_id, attributes, resource_type, resource_name, filter_json, fields, state, title)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) RETURNING *`,
opts.ID, opts.SecretHash, encSecret, encKeyID, opts.ProjectID, opts.ExpiresOn, opts.CreatedByUserID, opts.Attributes, opts.ResourceType, opts.ResourceName, opts.FilterJSON, opts.Fields, opts.State, opts.Title,
).StructScan(res)
if err != nil {
return nil, parseErr("magic auth token", err)
Expand Down Expand Up @@ -2172,7 +2172,7 @@ func (c *connection) magicAuthTokenFromDTO(dto *magicAuthTokenDTO, fetchSecret b
if err != nil {
return nil, err
}
err = dto.MetricsViewFields.AssignTo(&dto.MagicAuthToken.MetricsViewFields)
err = dto.MetricsViewFields.AssignTo(&dto.MagicAuthToken.Fields)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -2202,7 +2202,7 @@ func (c *connection) magicAuthTokenWithUserFromDTO(dto *magicAuthTokenWithUserDT
if err != nil {
return nil, err
}
err = dto.MetricsViewFields.AssignTo(&dto.MagicAuthToken.MetricsViewFields)
err = dto.MetricsViewFields.AssignTo(&dto.MagicAuthToken.Fields)
if err != nil {
return nil, err
}
Expand Down
19 changes: 15 additions & 4 deletions admin/server/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,8 @@ func (s *Server) GetIFrame(ctx context.Context, req *adminv1.GetIFrameRequest) (
attribute.String("args.organization", req.Organization),
attribute.String("args.project", req.Project),
attribute.String("args.branch", req.Branch),
attribute.String("args.kind", req.Kind),
attribute.String("args.type", req.Type),
attribute.String("args.kind", req.Kind), // nolint:staticcheck // Deprecated but still used
attribute.String("args.resource", req.Resource),
attribute.String("args.ttl_seconds", strconv.FormatUint(uint64(req.TtlSeconds), 10)),
attribute.String("args.state", req.State),
Expand Down Expand Up @@ -272,23 +273,33 @@ func (s *Server) GetIFrame(ctx context.Context, req *adminv1.GetIFrameRequest) (
"instance_id": prodDepl.RuntimeInstanceID,
"access_token": jwt,
}
if req.Kind == "" {
iframeQuery["kind"] = runtime.ResourceKindMetricsView

if req.Type != "" {
iframeQuery["type"] = req.Type
} else if req.Kind != "" { // nolint:staticcheck // Deprecated but still used
iframeQuery["type"] = req.Kind
} else {
iframeQuery["kind"] = req.Kind
// Default to an explore if no type is explicitly provided
iframeQuery["type"] = runtime.ResourceKindExplore
}
iframeQuery["kind"] = iframeQuery["type"] // For backwards compatibility

if req.Resource != "" {
iframeQuery["resource"] = req.Resource
}

if req.Theme != "" {
iframeQuery["theme"] = req.Theme
}

if req.Navigation {
iframeQuery["navigation"] = "true"
}

if req.State != "" {
iframeQuery["state"] = req.State
}

for k, v := range req.Query {
iframeQuery[k] = v
}
Expand Down
43 changes: 23 additions & 20 deletions admin/server/magic_tokens.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,14 @@ import (
"google.golang.org/protobuf/types/known/timestamppb"
)

const magicAuthTokenMetricsViewFilterMaxSize = 1024
const magicAuthTokenFilterMaxSize = 1024

func (s *Server) IssueMagicAuthToken(ctx context.Context, req *adminv1.IssueMagicAuthTokenRequest) (*adminv1.IssueMagicAuthTokenResponse, error) {
observability.AddRequestAttributes(ctx,
attribute.String("args.organization", req.Organization),
attribute.String("args.project", req.Project),
attribute.String("args.metrics_view", req.MetricsView),
attribute.String("args.resource_type", req.ResourceType),
attribute.String("args.resource_name", req.ResourceName),
)

proj, err := s.admin.DB.FindProjectByName(ctx, req.Organization, req.Project)
Expand All @@ -48,11 +49,12 @@ func (s *Server) IssueMagicAuthToken(ctx context.Context, req *adminv1.IssueMagi
}

opts := &admin.IssueMagicAuthTokenOptions{
ProjectID: proj.ID,
MetricsView: req.MetricsView,
MetricsViewFields: req.MetricsViewFields,
State: req.State,
Title: req.Title,
ProjectID: proj.ID,
ResourceType: req.ResourceType,
ResourceName: req.ResourceName,
Fields: req.Fields,
State: req.State,
Title: req.Title,
}

if req.TtlMinutes != 0 {
Expand All @@ -76,17 +78,17 @@ func (s *Server) IssueMagicAuthToken(ctx context.Context, req *adminv1.IssueMagi
opts.Attributes = attrs
}

if req.MetricsViewFilter != nil {
val, err := protojson.Marshal(req.MetricsViewFilter)
if req.Filter != nil {
val, err := protojson.Marshal(req.Filter)
if err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}

if len(val) > magicAuthTokenMetricsViewFilterMaxSize {
return nil, status.Errorf(codes.InvalidArgument, "metrics view filter size exceeds limit (got %d bytes, but the limit is %d bytes)", len(val), magicAuthTokenMetricsViewFilterMaxSize)
if len(val) > magicAuthTokenFilterMaxSize {
return nil, status.Errorf(codes.InvalidArgument, "filter size exceeds limit (got %d bytes, but the limit is %d bytes)", len(val), magicAuthTokenFilterMaxSize)
}

opts.MetricsViewFilterJSON = string(val)
opts.FilterJSON = string(val)
}

token, err := s.admin.IssueMagicAuthToken(ctx, opts)
Expand Down Expand Up @@ -245,12 +247,12 @@ func (s *Server) magicAuthTokenToPB(tkn *database.MagicAuthTokenWithUser, org *d
return nil, fmt.Errorf("failed to convert attributes to structpb: %w", err)
}

var metricsViewFilter *runtimev1.Expression
if tkn.MetricsViewFilterJSON != "" {
metricsViewFilter = &runtimev1.Expression{}
err := protojson.Unmarshal([]byte(tkn.MetricsViewFilterJSON), metricsViewFilter)
var filter *runtimev1.Expression
if tkn.FilterJSON != "" {
filter = &runtimev1.Expression{}
err := protojson.Unmarshal([]byte(tkn.FilterJSON), filter)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal metrics view filter: %w", err)
return nil, fmt.Errorf("failed to unmarshal filter: %w", err)
}
}

Expand Down Expand Up @@ -281,9 +283,10 @@ func (s *Server) magicAuthTokenToPB(tkn *database.MagicAuthTokenWithUser, org *d
CreatedByUserId: safeStr(tkn.CreatedByUserID),
CreatedByUserEmail: tkn.CreatedByUserEmail,
Attributes: attrs,
MetricsView: tkn.MetricsView,
MetricsViewFilter: metricsViewFilter,
MetricsViewFields: tkn.MetricsViewFields,
ResourceType: tkn.ResourceType,
ResourceName: tkn.ResourceName,
Filter: filter,
Fields: tkn.Fields,
State: tkn.State,
Title: tkn.Title,
}
Expand Down
53 changes: 41 additions & 12 deletions admin/server/projects.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,26 +157,55 @@ func (s *Server) GetProject(ctx context.Context, req *adminv1.GetProjectRequest)
return nil, status.Errorf(codes.Internal, "unexpected type %T for magic auth token model", claims.AuthTokenModel())
}

// Build condition for what the magic auth token can access
var condition strings.Builder
// All themes
condition.WriteString(fmt.Sprintf("'{{.self.kind}}'='%s'", runtime.ResourceKindTheme))
// The magic token's resource
condition.WriteString(fmt.Sprintf(" OR '{{.self.kind}}'=%s AND '{{lower .self.name}}'=%s", duckdbsql.EscapeStringValue(mdl.ResourceType), duckdbsql.EscapeStringValue(strings.ToLower(mdl.ResourceName))))
// If the magic token's resource is an Explore, we also need to include its underlying metrics view
if mdl.ResourceType == runtime.ResourceKindExplore {
client, err := s.admin.OpenRuntimeClient(depl)
if err != nil {
return nil, status.Errorf(codes.Internal, "could not open runtime client: %s", err.Error())
}
defer client.Close()

resp, err := client.GetResource(ctx, &runtimev1.GetResourceRequest{
InstanceId: depl.RuntimeInstanceID,
Name: &runtimev1.ResourceName{
Kind: mdl.ResourceType,
Name: mdl.ResourceName,
},
})
if err != nil {
if status.Code(err) == codes.NotFound {
return nil, status.Errorf(codes.NotFound, "resource for magic token not found (name=%q, type=%q)", mdl.ResourceName, mdl.ResourceType)
}
return nil, status.Errorf(codes.Internal, "could not get resource for magic token: %s", err.Error())
}

spec := resp.Resource.GetExplore().State.ValidSpec
if spec != nil {
condition.WriteString(fmt.Sprintf(" OR '{{.self.kind}}'='%s' AND '{{lower .self.name}}'=%s", runtime.ResourceKindMetricsView, duckdbsql.EscapeStringValue(strings.ToLower(spec.MetricsView))))
}
}

attr = mdl.Attributes

// Deny access to all resources except themes and mdl.MetricsView
// Add a rule that denies access to anything that doesn't match the condition.
rules = append(rules, &runtimev1.SecurityRule{
Rule: &runtimev1.SecurityRule_Access{
Access: &runtimev1.SecurityRuleAccess{
Condition: fmt.Sprintf(
"NOT ('{{.self.kind}}'='%s' OR '{{.self.kind}}'='%s' AND '{{ lower .self.name }}'=%s)",
runtime.ResourceKindTheme,
runtime.ResourceKindMetricsView,
duckdbsql.EscapeStringValue(strings.ToLower(mdl.MetricsView)),
),
Allow: false,
Condition: fmt.Sprintf("NOT (%s)", condition.String()),
Allow: false,
},
},
})

if mdl.MetricsViewFilterJSON != "" {
if mdl.FilterJSON != "" {
expr := &runtimev1.Expression{}
err := protojson.Unmarshal([]byte(mdl.MetricsViewFilterJSON), expr)
err := protojson.Unmarshal([]byte(mdl.FilterJSON), expr)
if err != nil {
return nil, status.Errorf(codes.Internal, "could not unmarshal metrics view filter: %s", err.Error())
}
Expand All @@ -190,11 +219,11 @@ func (s *Server) GetProject(ctx context.Context, req *adminv1.GetProjectRequest)
})
}

if len(mdl.MetricsViewFields) > 0 {
if len(mdl.Fields) > 0 {
rules = append(rules, &runtimev1.SecurityRule{
Rule: &runtimev1.SecurityRule_FieldAccess{
FieldAccess: &runtimev1.SecurityRuleFieldAccess{
Fields: mdl.MetricsViewFields,
Fields: mdl.Fields,
Allow: true,
},
},
Expand Down
Loading
Loading