Skip to content

Commit

Permalink
Merge pull request #2564 from target/slack-easy-link
Browse files Browse the repository at this point in the history
auth: link slack user
  • Loading branch information
tony-tvu authored Sep 6, 2022
2 parents 4ae9be1 + 1630158 commit 31bdea6
Show file tree
Hide file tree
Showing 28 changed files with 1,053 additions and 32 deletions.
9 changes: 6 additions & 3 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/target/goalert/alert/alertmetrics"
"github.com/target/goalert/app/lifecycle"
"github.com/target/goalert/auth"
"github.com/target/goalert/auth/authlink"
"github.com/target/goalert/auth/basic"
"github.com/target/goalert/auth/nonce"
"github.com/target/goalert/calsub"
Expand Down Expand Up @@ -116,16 +117,18 @@ type App struct {
LimitStore *limit.Store
HeartbeatStore *heartbeat.Store

OAuthKeyring keyring.Keyring
SessionKeyring keyring.Keyring
APIKeyring keyring.Keyring
OAuthKeyring keyring.Keyring
SessionKeyring keyring.Keyring
APIKeyring keyring.Keyring
AuthLinkKeyring keyring.Keyring

NonceStore *nonce.Store
LabelStore *label.Store
OnCallStore *oncall.Store
NCStore *notificationchannel.Store
TimeZoneStore *timezone.Store
NoticeStore *notice.Store
AuthLinkStore *authlink.Store
}

// NewApp constructs a new App and binds the listening socket.
Expand Down
1 change: 1 addition & 0 deletions app/initengine.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ func (app *App) initEngine(ctx context.Context) error {
NCStore: app.NCStore,
OnCallStore: app.OnCallStore,
ScheduleStore: app.ScheduleStore,
AuthLinkStore: app.AuthLinkStore,

ConfigSource: app.ConfigStore,

Expand Down
6 changes: 3 additions & 3 deletions app/initgraphql.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
)

func (app *App) initGraphQL(ctx context.Context) error {

app.graphql2 = &graphqlapp.App{
DB: app.db,
AuthBasicStore: app.AuthBasicStore,
Expand Down Expand Up @@ -35,11 +34,12 @@ func (app *App) initGraphQL(ctx context.Context) error {
NotificationStore: app.NotificationStore,
SlackStore: app.slackChan,
HeartbeatStore: app.HeartbeatStore,
NoticeStore: *app.NoticeStore,
NoticeStore: app.NoticeStore,
Twilio: app.twilioConfig,
AuthHandler: app.AuthHandler,
FormatDestFunc: app.notificationManager.FormatDestValue,
NotificationManager: *app.notificationManager,
NotificationManager: app.notificationManager,
AuthLinkStore: app.AuthLinkStore,
}

return nil
Expand Down
23 changes: 23 additions & 0 deletions app/initstores.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/target/goalert/alert"
"github.com/target/goalert/alert/alertlog"
"github.com/target/goalert/alert/alertmetrics"
"github.com/target/goalert/auth/authlink"
"github.com/target/goalert/auth/basic"
"github.com/target/goalert/auth/nonce"
"github.com/target/goalert/calsub"
Expand Down Expand Up @@ -78,6 +79,18 @@ func (app *App) initStores(ctx context.Context) error {
return errors.Wrap(err, "init oauth state keyring")
}

if app.AuthLinkKeyring == nil {
app.AuthLinkKeyring, err = keyring.NewDB(ctx, app.cfg.Logger, app.db, &keyring.Config{
Name: "auth-link",
RotationDays: 1,
MaxOldKeys: 1,
Keys: app.cfg.EncryptionKeys,
})
}
if err != nil {
return errors.Wrap(err, "init oauth state keyring")
}

if app.SessionKeyring == nil {
app.SessionKeyring, err = keyring.NewDB(ctx, app.cfg.Logger, app.db, &keyring.Config{
Name: "browser-sessions",
Expand All @@ -101,9 +114,19 @@ func (app *App) initStores(ctx context.Context) error {
return errors.Wrap(err, "init API keyring")
}

if app.AuthLinkStore == nil {
app.AuthLinkStore, err = authlink.NewStore(ctx, app.db, app.AuthLinkKeyring)
}
if err != nil {
return errors.Wrap(err, "init auth link store")
}

if app.AlertMetricsStore == nil {
app.AlertMetricsStore, err = alertmetrics.NewStore(ctx, app.db)
}
if err != nil {
return errors.Wrap(err, "init alert metrics store")
}

if app.AlertLogStore == nil {
app.AlertLogStore, err = alertlog.NewStore(ctx, app.db)
Expand Down
1 change: 1 addition & 0 deletions app/shutdown.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ func (app *App) _Shutdown(ctx context.Context) error {
shut(app.SessionKeyring, "session keyring")
shut(app.OAuthKeyring, "oauth keyring")
shut(app.APIKeyring, "API keyring")
shut(app.AuthLinkKeyring, "auth link keyring")
shut(app.NonceStore, "nonce store")
shut(app.ConfigStore, "config store")
shut(app.requestLock, "context locker")
Expand Down
186 changes: 186 additions & 0 deletions auth/authlink/store.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
package authlink

import (
"context"
"database/sql"
"encoding/json"
"errors"
"net/url"
"time"

"github.com/golang-jwt/jwt/v4"
"github.com/google/uuid"
"github.com/target/goalert/config"
"github.com/target/goalert/keyring"
"github.com/target/goalert/permission"
"github.com/target/goalert/util"
"github.com/target/goalert/validation"
"github.com/target/goalert/validation/validate"
)

type Store struct {
db *sql.DB

k keyring.Keyring

newLink *sql.Stmt
rmLink *sql.Stmt
addSubject *sql.Stmt
findLink *sql.Stmt
}

type Metadata struct {
UserDetails string
AlertID int `json:",omitempty"`
AlertAction string `json:",omitempty"`
}

func (m Metadata) Validate() error {
return validate.Many(
validate.ASCII("UserDetails", m.UserDetails, 1, 255),
validate.OneOf("AlertAction", m.AlertAction, "", "ResultAcknowledge", "ResultResolve"),
)
}

func NewStore(ctx context.Context, db *sql.DB, k keyring.Keyring) (*Store, error) {
p := &util.Prepare{
DB: db,
Ctx: ctx,
}

return &Store{
db: db,
k: k,
newLink: p.P(`insert into auth_link_requests (id, provider_id, subject_id, expires_at, metadata) values ($1, $2, $3, $4, $5)`),
rmLink: p.P(`delete from auth_link_requests where id = $1 and expires_at > now() returning provider_id, subject_id`),
addSubject: p.P(`insert into auth_subjects (provider_id, subject_id, user_id) values ($1, $2, $3)`),
findLink: p.P(`select metadata from auth_link_requests where id = $1 and expires_at > now()`),
}, p.Err
}

func (s *Store) FindLinkMetadata(ctx context.Context, token string) (*Metadata, error) {
err := permission.LimitCheckAny(ctx, permission.User)
if err != nil {
return nil, err
}

tokID, err := s.tokenID(ctx, token)
if err != nil {
// don't return anything, treat it as not found
return nil, nil
}

var meta Metadata
var data json.RawMessage
err = s.findLink.QueryRowContext(ctx, tokID).Scan(&data)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
if err != nil {
return nil, err
}

err = json.Unmarshal(data, &meta)
if err != nil {
return nil, err
}

return &meta, nil
}

func (s *Store) tokenID(ctx context.Context, token string) (string, error) {
var c jwt.RegisteredClaims
_, err := s.k.VerifyJWT(token, &c)
if err != nil {
return "", validation.WrapError(err)
}

if !c.VerifyIssuer("goalert", true) {
return "", validation.NewGenericError("invalid issuer")
}
if !c.VerifyAudience("auth-link", true) {
return "", validation.NewGenericError("invalid audience")
}
err = validate.UUID("ID", c.ID)
if err != nil {
return "", err
}

return c.ID, nil
}

func (s *Store) LinkAccount(ctx context.Context, token string) error {
err := permission.LimitCheckAny(ctx, permission.User)
if err != nil {
return err
}

tokID, err := s.tokenID(ctx, token)
if err != nil {
return err
}

tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()

var providerID, subjectID string
err = tx.StmtContext(ctx, s.rmLink).QueryRowContext(ctx, tokID).Scan(&providerID, &subjectID)
if errors.Is(err, sql.ErrNoRows) {
return validation.NewGenericError("invalid link token")
}
if err != nil {
return err
}

_, err = tx.StmtContext(ctx, s.addSubject).ExecContext(ctx, providerID, subjectID, permission.UserID(ctx))
if err != nil {
return err
}

return tx.Commit()
}

func (s *Store) AuthLinkURL(ctx context.Context, providerID, subjectID string, meta Metadata) (string, error) {
err := permission.LimitCheckAny(ctx, permission.System)
if err != nil {
return "", err
}
err = validate.Many(
validate.SubjectID("ProviderID", providerID),
validate.SubjectID("SubjectID", subjectID),
meta.Validate(),
)
if err != nil {
return "", err
}

id := uuid.New()
now := time.Now()
expires := now.Add(5 * time.Minute)

var c jwt.RegisteredClaims
c.ID = id.String()
c.Audience = jwt.ClaimStrings{"auth-link"}
c.Issuer = "goalert"
c.NotBefore = jwt.NewNumericDate(now.Add(-2 * time.Minute))
c.ExpiresAt = jwt.NewNumericDate(expires)
c.IssuedAt = jwt.NewNumericDate(now)

token, err := s.k.SignJWT(c)
if err != nil {
return "", err
}

_, err = s.newLink.ExecContext(ctx, id, providerID, subjectID, expires, meta)
if err != nil {
return "", err
}

cfg := config.FromContext(ctx)
p := make(url.Values)
p.Set("authLinkToken", token)
return cfg.CallbackURL("/profile", p), nil
}
53 changes: 50 additions & 3 deletions devtools/mockslack/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ type actionBody struct {
Name string
TeamID string `json:"team_id"`
}
Team struct {
ID string
Domain string
}
ResponseURL string `json:"response_url"`
Actions []actionItem
}
Expand All @@ -42,6 +46,18 @@ func (s *Server) ServeActionResponse(w http.ResponseWriter, r *http.Request) {
var req struct {
Text string
Type string `json:"response_type"`

Blocks []struct {
Type string
Text struct{ Text string }
Elements []struct {
Type string
Text struct{ Text string }
Value string
ActionID string `json:"action_id"`
URL string
}
}
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
Expand All @@ -60,11 +76,40 @@ func (s *Server) ServeActionResponse(w http.ResponseWriter, r *http.Request) {
return
}

msg, err := s.API().ChatPostMessage(r.Context(), ChatPostMessageOptions{
opts := ChatPostMessageOptions{
ChannelID: a.ChannelID,
Text: req.Text,
User: r.URL.Query().Get("user"),
})
}

if len(req.Blocks) > 0 {
// new API
for _, block := range req.Blocks {
switch block.Type {
case "section":
opts.Text = block.Text.Text
case "actions":
for _, action := range block.Elements {
if action.Type != "button" {
continue
}

opts.Actions = append(opts.Actions, Action{
ChannelID: a.ChannelID,
TeamID: a.TeamID,
AppID: a.AppID,
ActionID: action.ActionID,
Text: action.Text.Text,
Value: action.Value,
URL: action.URL,
})
}
}
}
} else {
opts.Text = req.Text
}

msg, err := s.API().ChatPostMessage(r.Context(), opts)
if respondErr(w, err) {
return
}
Expand Down Expand Up @@ -106,6 +151,8 @@ func (s *Server) PerformActionAs(userID string, a Action) error {
p.User.Username = usr.Name
p.User.Name = usr.Name
p.User.TeamID = a.TeamID
p.Team.ID = a.TeamID
p.Team.Domain = "example.com"
p.Channel.ID = a.ChannelID
p.AppID = a.AppID

Expand Down
Loading

0 comments on commit 31bdea6

Please sign in to comment.