Skip to content

Commit

Permalink
Magic tokens for sharing filtered dashboards to non-users (#5071)
Browse files Browse the repository at this point in the history
* Magic tokens for sharing filtered dashboards to non-users

* Change preset_filter to filter

* Review comments

* Add token attributes

* Fix test

* Add dedicated permissions for bookmarks to show magic tokens can't use them

* Progress commit

* Logic for intersecting include/excludes

* Fix lint

* Self review

* CLI commands

* Self review 2

* Review 3

* Move share-url cmd to root

* Fix downloads

* Fix merge

* Fix test
  • Loading branch information
begelundmuller authored Jun 19, 2024
1 parent 11ace61 commit c422dd8
Show file tree
Hide file tree
Showing 77 changed files with 10,904 additions and 5,959 deletions.
89 changes: 89 additions & 0 deletions admin/auth_token.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
// AuthToken is the interface package admin uses to provide a consolidated view of a token string and its DB model.
type AuthToken interface {
Token() *authtoken.Token
TokenModel() any
OwnerID() string
}

Expand All @@ -26,6 +27,10 @@ func (t *userAuthToken) Token() *authtoken.Token {
return t.token
}

func (t *userAuthToken) TokenModel() any {
return t.model
}

func (t *userAuthToken) OwnerID() string {
if t.model.RepresentingUserID != nil {
return *t.model.RepresentingUserID
Expand Down Expand Up @@ -70,6 +75,10 @@ func (t *serviceAuthToken) Token() *authtoken.Token {
return t.token
}

func (t *serviceAuthToken) TokenModel() any {
return t.model
}

func (t *serviceAuthToken) OwnerID() string {
return t.model.ServiceID
}
Expand Down Expand Up @@ -107,6 +116,10 @@ func (t *deploymentAuthToken) Token() *authtoken.Token {
return t.token
}

func (t *deploymentAuthToken) TokenModel() any {
return t.model
}

func (t *deploymentAuthToken) OwnerID() string {
return t.model.DeploymentID
}
Expand Down Expand Up @@ -134,6 +147,63 @@ func (s *Service) IssueDeploymentAuthToken(ctx context.Context, deploymentID str
return &deploymentAuthToken{model: dat, token: tkn}, nil
}

// magicAuthToken implements AuthToken for magic tokens belonging to a project.
type magicAuthToken struct {
model *database.MagicAuthToken
token *authtoken.Token
}

func (t *magicAuthToken) Token() *authtoken.Token {
return t.token
}

func (t *magicAuthToken) TokenModel() any {
return t.model
}

func (t *magicAuthToken) OwnerID() string {
return t.model.ID
}

// 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
}

// IssueMagicAuthToken generates and persists a new magic auth token for a project.
func (s *Service) IssueMagicAuthToken(ctx context.Context, opts *IssueMagicAuthTokenOptions) (AuthToken, error) {
tkn := authtoken.NewRandom(authtoken.TypeMagic)

var expiresOn *time.Time
if opts.TTL != nil {
t := time.Now().Add(*opts.TTL)
expiresOn = &t
}

dat, err := s.DB.InsertMagicAuthToken(ctx, &database.InsertMagicAuthTokenOptions{
ID: tkn.ID.String(),
SecretHash: tkn.SecretHash(),
ProjectID: opts.ProjectID,
ExpiresOn: expiresOn,
CreatedByUserID: opts.CreatedByUserID,
Attributes: opts.Attributes,
MetricsView: opts.MetricsView,
MetricsViewFilterJSON: opts.MetricsViewFilterJSON,
MetricsViewFields: opts.MetricsViewFields,
})
if err != nil {
return nil, err
}

return &magicAuthToken{model: dat, token: tkn}, nil
}

// ValidateAuthToken validates an auth token against persistent storage.
func (s *Service) ValidateAuthToken(ctx context.Context, token string) (AuthToken, error) {
parsed, err := authtoken.FromString(token)
Expand Down Expand Up @@ -196,6 +266,23 @@ func (s *Service) ValidateAuthToken(ctx context.Context, token string) (AuthToke
s.Used.Deployment(dat.DeploymentID)

return &deploymentAuthToken{model: dat, token: parsed}, nil
case authtoken.TypeMagic:
mat, err := s.DB.FindMagicAuthToken(ctx, parsed.ID.String())
if err != nil {
return nil, err
}

if mat.ExpiresOn != nil && mat.ExpiresOn.Before(time.Now()) {
return nil, fmt.Errorf("auth token is expired")
}

if !bytes.Equal(mat.SecretHash, parsed.SecretHash()) {
return nil, fmt.Errorf("invalid auth token")
}

s.Used.MagicAuthToken(mat.ID)

return &magicAuthToken{model: mat, token: parsed}, nil
default:
return nil, fmt.Errorf("unknown auth token type %q", parsed.Type)
}
Expand All @@ -214,6 +301,8 @@ func (s *Service) RevokeAuthToken(ctx context.Context, token string) error {
return s.DB.DeleteServiceAuthToken(ctx, parsed.ID.String())
case authtoken.TypeDeployment:
return fmt.Errorf("deployment auth tokens cannot be revoked")
case authtoken.TypeMagic:
return s.DB.DeleteMagicAuthToken(ctx, parsed.ID.String())
default:
return fmt.Errorf("unknown auth token type %q", parsed.Type)
}
Expand Down
77 changes: 61 additions & 16 deletions admin/database/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,13 @@ type DB interface {
UpdateDeploymentAuthTokenUsedOn(ctx context.Context, ids []string) error
DeleteExpiredDeploymentAuthTokens(ctx context.Context, retention time.Duration) error

FindMagicAuthTokensWithUser(ctx context.Context, projectID string, createdByUserID *string, afterID string, limit int) ([]*MagicAuthTokenWithUser, error)
FindMagicAuthToken(ctx context.Context, id string) (*MagicAuthToken, error)
InsertMagicAuthToken(ctx context.Context, opts *InsertMagicAuthTokenOptions) (*MagicAuthToken, error)
UpdateMagicAuthTokenUsedOn(ctx context.Context, ids []string) error
DeleteMagicAuthToken(ctx context.Context, id string) error
DeleteExpiredMagicAuthTokens(ctx context.Context, retention time.Duration) error

FindDeviceAuthCodeByDeviceCode(ctx context.Context, deviceCode string) (*DeviceAuthCode, error)
FindPendingDeviceAuthCodeByUserCode(ctx context.Context, userCode string) (*DeviceAuthCode, error)
InsertDeviceAuthCode(ctx context.Context, deviceCode, userCode, clientID string, expiresOn time.Time) (*DeviceAuthCode, error)
Expand Down Expand Up @@ -507,6 +514,40 @@ type InsertDeploymentAuthTokenOptions struct {
ExpiresOn *time.Time
}

// MagicAuthToken is a persistent API token for accessing a specific (filtered) resource in a project.
type MagicAuthToken struct {
ID string
SecretHash []byte `db:"secret_hash"`
ProjectID string `db:"project_id"`
CreatedOn time.Time `db:"created_on"`
ExpiresOn *time.Time `db:"expires_on"`
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"`
}

// MagicAuthTokenWithUser is a MagicAuthToken with additional information about the user who created it.
type MagicAuthTokenWithUser struct {
*MagicAuthToken
CreatedByUserEmail string `db:"created_by_user_email"`
}

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

// AuthClient is a client that requests and consumes auth tokens.
type AuthClient struct {
ID string
Expand Down Expand Up @@ -585,22 +626,26 @@ type OrganizationRole struct {

// ProjectRole represents roles for projects.
type ProjectRole struct {
ID string
Name string
ReadProject bool `db:"read_project"`
ManageProject bool `db:"manage_project"`
ReadProd bool `db:"read_prod"`
ReadProdStatus bool `db:"read_prod_status"`
ManageProd bool `db:"manage_prod"`
ReadDev bool `db:"read_dev"`
ReadDevStatus bool `db:"read_dev_status"`
ManageDev bool `db:"manage_dev"`
ReadProjectMembers bool `db:"read_project_members"`
ManageProjectMembers bool `db:"manage_project_members"`
CreateReports bool `db:"create_reports"`
ManageReports bool `db:"manage_reports"`
CreateAlerts bool `db:"create_alerts"`
ManageAlerts bool `db:"manage_alerts"`
ID string
Name string
ReadProject bool `db:"read_project"`
ManageProject bool `db:"manage_project"`
ReadProd bool `db:"read_prod"`
ReadProdStatus bool `db:"read_prod_status"`
ManageProd bool `db:"manage_prod"`
ReadDev bool `db:"read_dev"`
ReadDevStatus bool `db:"read_dev_status"`
ManageDev bool `db:"manage_dev"`
ReadProjectMembers bool `db:"read_project_members"`
ManageProjectMembers bool `db:"manage_project_members"`
CreateMagicAuthTokens bool `db:"create_magic_auth_tokens"`
ManageMagicAuthTokens bool `db:"manage_magic_auth_tokens"`
CreateReports bool `db:"create_reports"`
ManageReports bool `db:"manage_reports"`
CreateAlerts bool `db:"create_alerts"`
ManageAlerts bool `db:"manage_alerts"`
CreateBookmarks bool `db:"create_bookmarks"`
ManageBookmarks bool `db:"manage_bookmarks"`
}

// Member is a convenience type used for display-friendly representation of an org or project member.
Expand Down
28 changes: 28 additions & 0 deletions admin/database/postgres/migrations/0029.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
CREATE TABLE magic_auth_tokens (
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
secret_hash BYTEA NOT NULL,
project_id UUID NOT NULL REFERENCES projects (id) ON DELETE CASCADE,
created_on TIMESTAMPTZ DEFAULT now() NOT NULL,
expires_on TIMESTAMPTZ,
used_on TIMESTAMPTZ DEFAULT now() NOT NULL,
created_by_user_id UUID REFERENCES users (id) ON DELETE SET NULL,
attributes JSONB DEFAULT '{}'::JSONB NOT NULL,
metrics_view TEXT NOT NULL,
metrics_view_filter_json TEXT NOT NULL,
metrics_view_fields TEXT[] NOT NULL
);

CREATE INDEX magic_auth_tokens_project_id_idx ON magic_auth_tokens (project_id);
CREATE INDEX magic_auth_tokens_created_by_user_id_idx ON magic_auth_tokens (created_by_user_id) WHERE created_by_user_id IS NOT NULL;

ALTER TABLE project_roles ADD create_magic_auth_tokens BOOLEAN DEFAULT false NOT NULL;
UPDATE project_roles SET create_magic_auth_tokens = manage_project_members;

ALTER TABLE project_roles ADD manage_magic_auth_tokens BOOLEAN DEFAULT false NOT NULL;
UPDATE project_roles SET manage_magic_auth_tokens = manage_project_members;

ALTER TABLE project_roles ADD create_bookmarks BOOLEAN DEFAULT false NOT NULL;
UPDATE project_roles SET create_bookmarks = read_project;

ALTER TABLE project_roles ADD manage_bookmarks BOOLEAN DEFAULT false NOT NULL;
UPDATE project_roles SET manage_bookmarks = manage_project;
Loading

1 comment on commit c422dd8

@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.