Skip to content

Commit

Permalink
Embedded dashboards (#3590)
Browse files Browse the repository at this point in the history
* backend embed api

Updating embedded route

Fix host for local dev mode

add validations

refactoring

docs

docs

remove comment

prettier

prettier

* svc account docs

* Add expliit `white` background color to whole app

* Hide top navbar for embedded dashboards

* review comments docs

* review comments docs

* review comments docs

* review comments

* lint errors

* non admin user for no attrs

* fix docs

* docs

* fix ui

---------

Co-authored-by: Eric P Green <ericpgreen2@gmail.com>
  • Loading branch information
pjain1 and ericpgreen2 authored Dec 7, 2023
1 parent b5ce9df commit 8086775
Show file tree
Hide file tree
Showing 36 changed files with 4,736 additions and 2,554 deletions.
225 changes: 204 additions & 21 deletions admin/server/deployment.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ package server

import (
"context"
"errors"
"net/http"
"strconv"
"strings"
"time"

"github.com/rilldata/rill/admin/database"
"github.com/rilldata/rill/admin/pkg/urlutil"
"github.com/rilldata/rill/admin/server/auth"
adminv1 "github.com/rilldata/rill/proto/gen/rill/admin/v1"
"github.com/rilldata/rill/runtime/pkg/observability"
Expand Down Expand Up @@ -159,6 +163,7 @@ func (s *Server) GetDeploymentCredentials(ctx context.Context, req *adminv1.GetD
attribute.String("args.organization", req.Organization),
attribute.String("args.project", req.Project),
attribute.String("args.branch", req.Branch),
attribute.String("args.ttl_seconds", strconv.FormatUint(uint64(req.TtlSeconds), 10)),
)

proj, err := s.admin.DB.FindProjectByName(ctx, req.Organization, req.Project)
Expand Down Expand Up @@ -188,33 +193,56 @@ func (s *Server) GetDeploymentCredentials(ctx context.Context, req *adminv1.GetD
}

var attr map[string]any
switch forVal := req.For.(type) {
case *adminv1.GetDeploymentCredentialsRequest_UserId:
forOrgPerms, err := s.admin.OrganizationPermissionsForUser(ctx, proj.OrganizationID, forVal.UserId)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
if req.For != nil {
switch forVal := req.For.(type) {
case *adminv1.GetDeploymentCredentialsRequest_UserId:
attr, err = s.getAttributesFor(ctx, forVal.UserId, proj.OrganizationID, proj.ID)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
case *adminv1.GetDeploymentCredentialsRequest_UserEmail:
user, err := s.admin.DB.FindUserByEmail(ctx, forVal.UserEmail)
// if email is not found in the database, we assume it is a non-admin user
if errors.Is(err, database.ErrNotFound) {
attr = map[string]any{
"email": forVal.UserEmail,
"domain": forVal.UserEmail[strings.LastIndex(forVal.UserEmail, "@")+1:],
"admin": false,
}
break
}
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
attr, err = s.getAttributesFor(ctx, user.ID, proj.OrganizationID, proj.ID)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
case *adminv1.GetDeploymentCredentialsRequest_Attributes:
attr = forVal.Attributes.AsMap()
default:
return nil, status.Error(codes.InvalidArgument, "invalid 'for' type")
}

forProjPerms, err := s.admin.ProjectPermissionsForUser(ctx, proj.ID, forVal.UserId, forOrgPerms)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
// if no attributes found, we add standard non-admin user attrs to ensure security policies are applied correctly
if len(attr) == 0 {
attr = map[string]any{
"email": "",
"domain": "",
"admin": false,
}
}

attr, err = s.jwtAttributesForUser(ctx, forVal.UserId, proj.OrganizationID, forProjPerms)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
case *adminv1.GetDeploymentCredentialsRequest_Attrs:
attr = forVal.Attrs.AsMap()
default:
return nil, status.Error(codes.InvalidArgument, "invalid 'for' type")
ttlDuration := time.Hour
if req.TtlSeconds > 0 {
ttlDuration = time.Duration(req.TtlSeconds) * time.Second
}

// Generate JWT
jwt, err := s.issuer.NewToken(runtimeauth.TokenOptions{
AudienceURL: prodDepl.RuntimeAudience,
Subject: claims.OwnerID(),
TTL: time.Hour,
TTL: ttlDuration,
InstancePermissions: map[string][]runtimeauth.Permission{
prodDepl.RuntimeInstanceID: {
// TODO: Remove ReadProfiling and ReadRepo (may require frontend changes)
Expand All @@ -233,8 +261,163 @@ func (s *Server) GetDeploymentCredentials(ctx context.Context, req *adminv1.GetD
s.admin.Used.Deployment(prodDepl.ID)

return &adminv1.GetDeploymentCredentialsResponse{
RuntimeHost: prodDepl.RuntimeHost,
RuntimeInstanceId: prodDepl.RuntimeInstanceID,
Jwt: jwt,
RuntimeHost: prodDepl.RuntimeHost,
InstanceId: prodDepl.RuntimeInstanceID,
AccessToken: jwt,
TtlSeconds: uint32(ttlDuration.Seconds()),
}, nil
}

func (s *Server) GetIFrame(ctx context.Context, req *adminv1.GetIFrameRequest) (*adminv1.GetIFrameResponse, error) {
observability.AddRequestAttributes(ctx,
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.resource", req.Resource),
attribute.String("args.ttl_seconds", strconv.FormatUint(uint64(req.TtlSeconds), 10)),
attribute.String("args.state", req.State),
)

if req.Resource == "" {
return nil, status.Error(codes.InvalidArgument, "resource must be specified")
}

proj, err := s.admin.DB.FindProjectByName(ctx, req.Organization, req.Project)
if err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}

if proj.ProdDeploymentID == nil {
return nil, status.Error(codes.InvalidArgument, "project does not have a deployment")
}

prodDepl, err := s.admin.DB.FindDeployment(ctx, *proj.ProdDeploymentID)
if err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}

if req.Branch != "" && req.Branch != prodDepl.Branch {
return nil, status.Error(codes.InvalidArgument, "project does not have a deployment for given branch")
}

claims := auth.GetClaims(ctx)
permissions := claims.ProjectPermissions(ctx, proj.OrganizationID, proj.ID)

// If the user is not a superuser, they must have ManageProd permissions
if !permissions.ManageProd && !claims.Superuser(ctx) {
return nil, status.Error(codes.PermissionDenied, "does not have permission to manage deployment")
}

var attr map[string]any
if req.For != nil {
switch forVal := req.For.(type) {
case *adminv1.GetIFrameRequest_UserId:
attr, err = s.getAttributesFor(ctx, forVal.UserId, proj.OrganizationID, proj.ID)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
case *adminv1.GetIFrameRequest_UserEmail:
user, err := s.admin.DB.FindUserByEmail(ctx, forVal.UserEmail)
// if email is not found in the database, we assume it is a non-admin user
if errors.Is(err, database.ErrNotFound) {
attr = map[string]any{
"email": forVal.UserEmail,
"domain": forVal.UserEmail[strings.LastIndex(forVal.UserEmail, "@")+1:],
"admin": false,
}
break
}
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
attr, err = s.getAttributesFor(ctx, user.ID, proj.OrganizationID, proj.ID)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
case *adminv1.GetIFrameRequest_Attributes:
attr = forVal.Attributes.AsMap()
default:
return nil, status.Error(codes.InvalidArgument, "invalid 'for' type")
}
}
// if no attributes found, we add standard non-admin user attrs to ensure security policies are applied correctly
if len(attr) == 0 {
attr = map[string]any{
"email": "",
"domain": "",
"admin": false,
}
}

// default here is higher than GetDeploymentCredentials as most embedders probably won't implement refresh and state management
ttlDuration := 24 * time.Hour
if req.TtlSeconds > 0 {
ttlDuration = time.Duration(req.TtlSeconds) * time.Second
}

// Generate JWT
jwt, err := s.issuer.NewToken(runtimeauth.TokenOptions{
AudienceURL: prodDepl.RuntimeAudience,
Subject: claims.OwnerID(),
TTL: ttlDuration,
InstancePermissions: map[string][]runtimeauth.Permission{
prodDepl.RuntimeInstanceID: {
// TODO: Remove ReadProfiling and ReadRepo (may require frontend changes)
runtimeauth.ReadObjects,
runtimeauth.ReadMetrics,
runtimeauth.ReadProfiling,
runtimeauth.ReadRepo,
},
},
Attributes: attr,
})
if err != nil {
return nil, status.Errorf(codes.Internal, "could not issue jwt: %s", err.Error())
}

s.admin.Used.Deployment(prodDepl.ID)

if req.Kind == "" {
req.Kind = "MetricsView"
}

iFrameURL, err := urlutil.WithQuery(urlutil.MustJoinURL(s.opts.FrontendURL, "/-/embed"), map[string]string{
"runtime_host": prodDepl.RuntimeHost,
"instance_id": prodDepl.RuntimeInstanceID,
"access_token": jwt,
"kind": req.Kind,
"resource": req.Resource,
"state": "",
"theme": req.Query["theme"],
})
if err != nil {
return nil, status.Errorf(codes.Internal, "could not construct iframe url: %s", err.Error())
}

return &adminv1.GetIFrameResponse{
IframeSrc: iFrameURL,
RuntimeHost: prodDepl.RuntimeHost,
InstanceId: prodDepl.RuntimeInstanceID,
AccessToken: jwt,
TtlSeconds: uint32(ttlDuration.Seconds()),
}, nil
}

func (s *Server) getAttributesFor(ctx context.Context, userID, orgID, projID string) (map[string]any, error) {
forOrgPerms, err := s.admin.OrganizationPermissionsForUser(ctx, orgID, userID)
if err != nil {
return nil, err
}

forProjPerms, err := s.admin.ProjectPermissionsForUser(ctx, projID, userID, forOrgPerms)
if err != nil {
return nil, err
}

attr, err := s.jwtAttributesForUser(ctx, userID, orgID, forProjPerms)
if err != nil {
return nil, err
}
return attr, nil
}
1 change: 0 additions & 1 deletion cli/cmd/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ func ServiceCmd(ch *cmdutil.Helper) *cobra.Command {
serviceCmd := &cobra.Command{
Use: "service",
Short: "Manage service accounts",
Hidden: !cfg.IsDev(),
PersistentPreRunE: cmdutil.CheckChain(cmdutil.CheckAuth(cfg), cmdutil.CheckOrganization(cfg)),
}

Expand Down
Loading

1 comment on commit 8086775

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.