Rewrite identity and access management

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
Bryan Frimin
2025-12-03 19:23:15 +01:00
parent 4ed3f5a067
commit 74fc3b8cd1
201 changed files with 32895 additions and 23649 deletions

View File

@@ -25,6 +25,7 @@ type (
Password passwordConfig `json:"password"`
DisableSignup bool `json:"disable-signup"`
InvitationConfirmationTokenValidity int `json:"invitation-confirmation-token-validity"`
PasswordResetTokenValidity int `json:"password-reset-token-validity"`
SAML samlConfig `json:"saml"`
}

View File

@@ -17,8 +17,10 @@ package probod
import (
"context"
"crypto"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"net"
@@ -27,6 +29,8 @@ import (
"sync"
"time"
pemutil "go.probo.inc/probo/pkg/crypto/pem"
"github.com/aws/aws-sdk-go-v2/service/s3"
proxyproto "github.com/pires/go-proxyproto"
"github.com/prometheus/client_golang/prometheus"
@@ -38,8 +42,6 @@ import (
"go.gearno.de/kit/unit"
"go.opentelemetry.io/otel/trace"
"go.probo.inc/probo/pkg/agents"
"go.probo.inc/probo/pkg/auth"
"go.probo.inc/probo/pkg/authz"
"go.probo.inc/probo/pkg/awsconfig"
"go.probo.inc/probo/pkg/baseurl"
"go.probo.inc/probo/pkg/certmanager"
@@ -48,14 +50,13 @@ import (
"go.probo.inc/probo/pkg/crypto/cipher"
"go.probo.inc/probo/pkg/crypto/keys"
"go.probo.inc/probo/pkg/crypto/passwdhash"
"go.probo.inc/probo/pkg/crypto/pem"
"go.probo.inc/probo/pkg/filemanager"
"go.probo.inc/probo/pkg/html2pdf"
"go.probo.inc/probo/pkg/iam"
"go.probo.inc/probo/pkg/mailer"
"go.probo.inc/probo/pkg/probo"
"go.probo.inc/probo/pkg/saferedirect"
"go.probo.inc/probo/pkg/securecookie"
"go.probo.inc/probo/pkg/server"
"go.probo.inc/probo/pkg/server/api"
"go.probo.inc/probo/pkg/slack"
"go.probo.inc/probo/pkg/trust"
"golang.org/x/sync/errgroup"
@@ -123,6 +124,7 @@ func New() *Implm {
},
DisableSignup: false,
InvitationConfirmationTokenValidity: 3600,
PasswordResetTokenValidity: 3600,
SAML: samlConfig{
SessionDuration: 604800,
CleanupIntervalSeconds: 86400,
@@ -277,51 +279,60 @@ func (impl *Implm) Run(
agent := agents.NewAgent(l.Named("agent"), agentConfig)
authService, err := auth.NewService(
ctx,
pgClient,
impl.cfg.EncryptionKey,
hp,
impl.cfg.Auth.Cookie.Secret,
impl.cfg.BaseURL.String(),
impl.cfg.Auth.DisableSignup,
time.Duration(impl.cfg.Auth.InvitationConfirmationTokenValidity)*time.Second,
)
if err != nil {
return fmt.Errorf("cannot create auth service: %w", err)
}
authzService, err := authz.NewService(
ctx,
pgClient,
impl.cfg.BaseURL.String(),
impl.cfg.Auth.Cookie.Secret,
time.Duration(impl.cfg.Auth.InvitationConfirmationTokenValidity)*time.Second,
)
if err != nil {
return fmt.Errorf("cannot create authz service: %w", err)
}
fileManagerService := filemanager.NewService(s3Client)
samlService, err := auth.NewSAMLService(
var samlCert *x509.Certificate
var samlKey *rsa.PrivateKey
if impl.cfg.Auth.SAML.Certificate != "" && impl.cfg.Auth.SAML.PrivateKey != "" {
// Decode certificate
certBlock, _ := pem.Decode([]byte(impl.cfg.Auth.SAML.Certificate))
if certBlock == nil {
return fmt.Errorf("cannot decode SAML certificate PEM block")
}
var err error
samlCert, err = x509.ParseCertificate(certBlock.Bytes)
if err != nil {
return fmt.Errorf("cannot parse SAML certificate: %w", err)
}
// Decode private key
signer, err := pemutil.DecodePrivateKey([]byte(impl.cfg.Auth.SAML.PrivateKey))
if err != nil {
return fmt.Errorf("cannot decode SAML private key: %w", err)
}
var ok bool
samlKey, ok = signer.(*rsa.PrivateKey)
if !ok {
return fmt.Errorf("SAML private key is not an RSA key")
}
}
iamService, err := iam.NewService(
ctx,
pgClient,
impl.cfg.EncryptionKey,
impl.cfg.BaseURL.String(),
impl.cfg.Auth.SAML.SessionDurationTime(),
impl.cfg.Auth.Cookie.Name,
impl.cfg.Auth.Cookie.Secret,
impl.cfg.Auth.SAML.Certificate,
impl.cfg.Auth.SAML.PrivateKey,
l.Named("saml"),
fileManagerService,
hp,
iam.Config{
DisableSignup: impl.cfg.Auth.DisableSignup,
InvitationTokenValidity: time.Duration(impl.cfg.Auth.InvitationConfirmationTokenValidity) * time.Second,
PasswordResetTokenValidity: time.Duration(impl.cfg.Auth.PasswordResetTokenValidity) * time.Second,
SessionDuration: time.Duration(impl.cfg.Auth.Cookie.Duration) * time.Hour,
Bucket: impl.cfg.AWS.Bucket,
TokenSecret: impl.cfg.Auth.Cookie.Secret,
BaseURL: impl.cfg.BaseURL.String(),
EncryptionKey: impl.cfg.EncryptionKey,
Certificate: samlCert,
PrivateKey: samlKey,
Logger: l.Named("iam"),
},
)
if err != nil {
return fmt.Errorf("cannot create SAML service: %w", err)
return fmt.Errorf("cannot create iam service: %w", err)
}
var accountKey crypto.Signer
if impl.cfg.CustomDomains.ACME.AccountKey != "" {
accountKey, err = pem.DecodePrivateKey([]byte(impl.cfg.CustomDomains.ACME.AccountKey))
accountKey, err = pemutil.DecodePrivateKey([]byte(impl.cfg.CustomDomains.ACME.AccountKey))
if err != nil {
return fmt.Errorf("cannot decode ACME account key: %w", err)
}
@@ -370,8 +381,6 @@ func (impl *Implm) Run(
html2pdfConverter,
acmeService,
fileManagerService,
authService,
authzService,
l.Named("probo"),
slackService,
)
@@ -386,7 +395,8 @@ func (impl *Implm) Run(
impl.cfg.BaseURL.String(),
impl.cfg.EncryptionKey,
impl.cfg.TrustAuth.TokenSecret,
authService,
impl.cfg.GetSlackSigningSecret(),
iamService,
html2pdfConverter,
fileManagerService,
l,
@@ -403,40 +413,23 @@ func (impl *Implm) Run(
AllowedOrigins: impl.cfg.Api.Cors.AllowedOrigins,
ExtraHeaderFields: impl.cfg.Api.ExtraHeaderFields,
Probo: proboService,
Auth: authService,
Authz: authzService,
IAM: iamService,
Trust: trustService,
Slack: slackService,
SAML: samlService,
ConnectorRegistry: defaultConnectorRegistry,
BaseURL: impl.cfg.BaseURL,
Agent: agent,
SafeRedirect: &saferedirect.SafeRedirect{AllowedHost: impl.cfg.BaseURL.Host()},
CustomDomainCname: impl.cfg.CustomDomains.CnameTarget,
FileManager: fileManagerService,
PGClient: pgClient,
Logger: l.Named("http.server"),
ConsoleAuth: api.ConsoleAuthConfig{
CookieName: impl.cfg.Auth.Cookie.Name,
CookieDomain: impl.cfg.Auth.Cookie.Domain,
SessionDuration: time.Duration(impl.cfg.Auth.Cookie.Duration) * time.Hour,
CookieSecret: impl.cfg.Auth.Cookie.Secret,
CookieSecure: impl.cfg.Auth.Cookie.Secure,
},
TrustAuth: api.TrustAuthConfig{
CookieName: impl.cfg.TrustAuth.CookieName,
CookieDomain: impl.cfg.TrustAuth.CookieDomain,
CookieDuration: time.Duration(impl.cfg.TrustAuth.CookieDuration) * time.Hour,
TokenDuration: time.Duration(impl.cfg.TrustAuth.TokenDuration) * time.Hour,
ReportURLDuration: time.Duration(impl.cfg.TrustAuth.ReportURLDuration) * time.Minute,
TokenSecret: impl.cfg.TrustAuth.TokenSecret,
Scope: impl.cfg.TrustAuth.Scope,
TokenType: impl.cfg.TrustAuth.TokenType,
CookieSecure: impl.cfg.Auth.Cookie.Secure,
},
MCPConfig: api.MCPConfig{
Version: "0.0.1",
RequestTimeout: 30 * time.Second,
MaxRequestSize: 10 * 1024 * 1024, // 10MB
Cookie: securecookie.Config{
Name: impl.cfg.Auth.Cookie.Name,
Domain: impl.cfg.Auth.Cookie.Domain,
Path: "/",
MaxAge: int(time.Duration(impl.cfg.Auth.Cookie.Duration) * time.Hour),
Secret: impl.cfg.Auth.Cookie.Secret,
Secure: impl.cfg.Auth.Cookie.Secure,
HTTPOnly: true,
SameSite: http.SameSiteLaxMode,
},
},
)
@@ -455,16 +448,20 @@ func (impl *Implm) Run(
)
mailerCtx, stopMailer := context.WithCancel(context.Background())
mailer := mailer.NewMailer(pgClient, l, mailer.Config{
SenderEmail: impl.cfg.Notifications.Mailer.SenderEmail,
SenderName: impl.cfg.Notifications.Mailer.SenderName,
Addr: impl.cfg.Notifications.Mailer.SMTP.Addr,
User: impl.cfg.Notifications.Mailer.SMTP.User,
Password: impl.cfg.Notifications.Mailer.SMTP.Password,
TLSRequired: impl.cfg.Notifications.Mailer.SMTP.TLSRequired,
Timeout: time.Second * 10,
Interval: time.Duration(impl.cfg.Notifications.Mailer.MailerInterval) * time.Second,
})
mailer := mailer.NewMailer(
pgClient,
l,
mailer.Config{
SenderEmail: impl.cfg.Notifications.Mailer.SenderEmail,
SenderName: impl.cfg.Notifications.Mailer.SenderName,
Addr: impl.cfg.Notifications.Mailer.SMTP.Addr,
User: impl.cfg.Notifications.Mailer.SMTP.User,
Password: impl.cfg.Notifications.Mailer.SMTP.Password,
TLSRequired: impl.cfg.Notifications.Mailer.SMTP.TLSRequired,
Timeout: time.Second * 10,
Interval: time.Duration(impl.cfg.Notifications.Mailer.MailerInterval) * time.Second,
},
)
wg.Go(
func() {
if err := mailer.Run(mailerCtx); err != nil {
@@ -494,16 +491,13 @@ func (impl *Implm) Run(
},
)
samlCleanerCtx, stopSAMLCleaner := context.WithCancel(context.Background())
samlCleaner := auth.NewCleaner(
pgClient,
impl.cfg.Auth.SAML.CleanupInterval(),
l.Named("saml-cleaner"),
)
samlServiceCtx, stopSAMLService := context.WithCancel(context.Background())
wg.Go(
func() {
if err := samlCleaner.Run(samlCleanerCtx); err != nil {
cancel(fmt.Errorf("saml cleaner crashed: %w", err))
if iamService.SAMLService != nil {
if err := iamService.SAMLService.Run(samlServiceCtx); err != nil {
cancel(fmt.Errorf("saml service crashed: %w", err))
}
}
},
)
@@ -523,7 +517,7 @@ func (impl *Implm) Run(
stopMailer()
stopSlackSender()
stopExportJobExporter()
stopSAMLCleaner()
stopSAMLService()
stopApiServer()
stopTrustCenterServer()