Files
warmbly/internal/api/handler/mailbox_grants.go
T

297 lines
8.4 KiB
Go

package handler
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/warmbly/warmbly/internal/api/middleware"
"github.com/warmbly/warmbly/internal/app/delegation"
"github.com/warmbly/warmbly/internal/app/mailboximport"
"github.com/warmbly/warmbly/internal/errx"
"github.com/warmbly/warmbly/internal/models"
)
// importCaller is the caller's workspace and user for the session-only mailbox source routes.
func importCaller(c *gin.Context) (uuid.UUID, uuid.UUID, bool) {
orgID := middleware.GetOrganizationID(c)
if orgID == nil {
errx.Handle(c, errx.ErrNoOrganization)
return uuid.Nil, uuid.Nil, false
}
userID, err := middleware.GetUserUUID(c)
if err != nil {
errx.Handle(c, errx.ErrUser)
return uuid.Nil, uuid.Nil, false
}
return *orgID, userID, true
}
func (h *Handler) grantsReady(c *gin.Context) (uuid.UUID, uuid.UUID, bool) {
if h.DelegationService == nil {
errx.Handle(c, errx.ErrNotFound)
return uuid.Nil, uuid.Nil, false
}
return importCaller(c)
}
// GetMailboxGrantConfig is GET /emails/grants/config: which admin connections
// this instance can take, and what a Google administrator has to authorize.
func (h *Handler) GetMailboxGrantConfig(c *gin.Context) {
if _, _, ok := h.grantsReady(c); !ok {
return
}
c.JSON(http.StatusOK, h.DelegationService.Config())
}
// ListMailboxGrants is GET /emails/grants.
func (h *Handler) ListMailboxGrants(c *gin.Context) {
orgID, _, ok := h.grantsReady(c)
if !ok {
return
}
list, xerr := h.DelegationService.List(c.Request.Context(), orgID)
if xerr != nil {
errx.Handle(c, xerr)
return
}
c.JSON(http.StatusOK, gin.H{"data": list})
}
type startGoogleGrantRequest struct {
Domain string `json:"domain"`
AdminEmail string `json:"admin_email"`
}
// StartGoogleMailboxGrant is POST /emails/grants/google/start: how this
// workspace proves it controls the domain (an administrator's sign-in, or a
// DNS TXT record unique to the workspace).
func (h *Handler) StartGoogleMailboxGrant(c *gin.Context) {
orgID, userID, ok := h.grantsReady(c)
if !ok {
return
}
var req startGoogleGrantRequest
if err := c.ShouldBindJSON(&req); err != nil {
errx.Handle(c, errx.ErrInvalid)
return
}
start, xerr := h.DelegationService.StartGoogle(c.Request.Context(), orgID, userID, req.Domain, req.AdminEmail)
if xerr != nil {
errx.Handle(c, xerr)
return
}
c.JSON(http.StatusOK, start)
}
// FinishGoogleMailboxGrant is POST /emails/grants/google/finish. A sign-in
// state is single-use; repeating a DNS finish re-verifies the same grant.
func (h *Handler) FinishGoogleMailboxGrant(c *gin.Context) {
orgID, userID, ok := h.grantsReady(c)
if !ok {
return
}
var req delegation.GoogleFinish
if err := c.ShouldBindJSON(&req); err != nil {
errx.Handle(c, errx.ErrInvalid)
return
}
g, xerr := h.DelegationService.FinishGoogle(c.Request.Context(), orgID, userID, req)
if xerr != nil {
errx.Handle(c, xerr)
return
}
h.auditOrg(c, models.AuditActionCreate, models.AuditEntityMailboxGrant, &g.ID, nil, map[string]string{"provider": g.Provider, "tenant": g.Tenant})
c.JSON(http.StatusCreated, g)
}
// StartMicrosoftMailboxGrant is POST /emails/grants/microsoft/start: the admin consent URL.
func (h *Handler) StartMicrosoftMailboxGrant(c *gin.Context) {
orgID, userID, ok := h.grantsReady(c)
if !ok {
return
}
u, state, xerr := h.DelegationService.StartMicrosoft(c.Request.Context(), orgID, userID)
if xerr != nil {
errx.Handle(c, xerr)
return
}
c.JSON(http.StatusOK, gin.H{"url": u, "state": state})
}
type finishMicrosoftGrantRequest struct {
State string `json:"state"`
Code string `json:"code"`
}
// FinishMicrosoftMailboxGrant is POST /emails/grants/microsoft/finish. The state
// is single-use, so a repeat is refused rather than recorded twice.
func (h *Handler) FinishMicrosoftMailboxGrant(c *gin.Context) {
orgID, userID, ok := h.grantsReady(c)
if !ok {
return
}
var req finishMicrosoftGrantRequest
if err := c.ShouldBindJSON(&req); err != nil {
errx.Handle(c, errx.ErrInvalid)
return
}
g, xerr := h.DelegationService.FinishMicrosoft(c.Request.Context(), orgID, userID, req.State, req.Code)
if xerr != nil {
errx.Handle(c, xerr)
return
}
h.auditOrg(c, models.AuditActionCreate, models.AuditEntityMailboxGrant, &g.ID, nil, map[string]string{"provider": g.Provider, "tenant": g.Tenant})
c.JSON(http.StatusCreated, g)
}
func (h *Handler) grantID(c *gin.Context) (uuid.UUID, uuid.UUID, uuid.UUID, bool) {
orgID, userID, ok := h.grantsReady(c)
if !ok {
return uuid.Nil, uuid.Nil, uuid.Nil, false
}
id, err := uuid.Parse(c.Param("id"))
if err != nil {
errx.Handle(c, errx.ErrUuid)
return uuid.Nil, uuid.Nil, uuid.Nil, false
}
return orgID, userID, id, true
}
// GetMailboxGrant is GET /emails/grants/:id.
func (h *Handler) GetMailboxGrant(c *gin.Context) {
orgID, _, id, ok := h.grantID(c)
if !ok {
return
}
g, xerr := h.DelegationService.Get(c.Request.Context(), orgID, id)
if xerr != nil {
errx.Handle(c, xerr)
return
}
c.JSON(http.StatusOK, g)
}
// CheckMailboxGrant is POST /emails/grants/:id/check: verify again, and put
// the grant's mailboxes back to work if it recovered.
func (h *Handler) CheckMailboxGrant(c *gin.Context) {
orgID, _, id, ok := h.grantID(c)
if !ok {
return
}
g, xerr := h.DelegationService.Recheck(c.Request.Context(), orgID, id)
if xerr != nil {
errx.Handle(c, xerr)
return
}
c.JSON(http.StatusOK, g)
}
// DeleteMailboxGrant is DELETE /emails/grants/:id. Its mailboxes stop.
func (h *Handler) DeleteMailboxGrant(c *gin.Context) {
orgID, userID, id, ok := h.grantID(c)
if !ok {
return
}
if xerr := h.DelegationService.Delete(c.Request.Context(), orgID, userID, id); xerr != nil {
errx.Handle(c, xerr)
return
}
h.auditOrg(c, models.AuditActionDelete, models.AuditEntityMailboxGrant, &id, nil, nil)
c.Status(http.StatusNoContent)
}
// ListMailboxGrantUsers is GET /emails/grants/:id/users: the granted directory.
func (h *Handler) ListMailboxGrantUsers(c *gin.Context) {
orgID, _, id, ok := h.grantID(c)
if !ok {
return
}
users, xerr := h.DelegationService.Users(c.Request.Context(), orgID, id)
if xerr != nil {
errx.Handle(c, xerr)
return
}
c.JSON(http.StatusOK, gin.H{"data": users})
}
type connectGrantRequest struct {
UserIDs []string `json:"user_ids"`
All bool `json:"all"`
Options models.MailboxImportOptions `json:"options"`
}
// ConnectMailboxGrantUsers is POST /emails/grants/:id/connect: import the picked
// users (or every enabled one not yet here) as a background import.
func (h *Handler) ConnectMailboxGrantUsers(c *gin.Context) {
orgID, userID, id, ok := h.grantID(c)
if !ok {
return
}
var req connectGrantRequest
if err := c.ShouldBindJSON(&req); err != nil {
errx.Handle(c, errx.ErrInvalid)
return
}
g, xerr := h.DelegationService.Get(c.Request.Context(), orgID, id)
if xerr != nil {
errx.Handle(c, xerr)
return
}
users, xerr := h.DelegationService.Users(c.Request.Context(), orgID, id)
if xerr != nil {
errx.Handle(c, xerr)
return
}
want := map[string]bool{}
for _, u := range req.UserIDs {
want[u] = true
}
rows := make([]mailboximport.ListRow, 0)
for _, u := range users {
pick := want[u.ID] || (req.All && u.Enabled && (!u.Connected || u.Upgrade))
if !pick {
continue
}
gid := g.ID
rows = append(rows, mailboximport.ListRow{Email: u.Email, Name: u.Name, GrantID: &gid})
}
label := g.Tenant
if g.Provider == models.GrantProviderMicrosoft && len(g.Domains) > 0 {
label = strings.Join(g.Domains, ", ")
}
imp, xerr := h.MailboxImportService.CreateFromList(c.Request.Context(), mailboximport.ListInput{
OrgID: orgID, UserID: userID, Source: "workspace", Vendor: g.Provider, Label: label, Rows: rows, Options: req.Options,
})
if xerr != nil {
errx.Handle(c, xerr)
return
}
c.JSON(http.StatusCreated, imp)
}
// GetSigninMigration is GET /emails/grants/migration: the workspace's mailboxes
// still on per-mailbox Google sign-in, by domain, with where each moves.
func (h *Handler) GetSigninMigration(c *gin.Context) {
orgID, _, ok := importCaller(c)
if !ok {
return
}
if h.DelegationService == nil {
c.JSON(http.StatusOK, gin.H{"data": []models.SigninMigration{}, "total": 0})
return
}
groups, xerr := h.DelegationService.SigninMigration(c.Request.Context(), orgID)
if xerr != nil {
errx.Handle(c, xerr)
return
}
total := 0
for _, g := range groups {
total += len(g.Mailboxes)
}
c.JSON(http.StatusOK, gin.H{"data": groups, "total": total})
}