182 lines
5.1 KiB
Go
182 lines
5.1 KiB
Go
package model
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/pkg/errors"
|
|
saml2 "github.com/russellhaering/gosaml2"
|
|
"go.signoz.io/signoz/ee/query-service/sso"
|
|
"go.signoz.io/signoz/ee/query-service/sso/saml"
|
|
basemodel "go.signoz.io/signoz/pkg/query-service/model"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
type SSOType string
|
|
|
|
const (
|
|
SAML SSOType = "SAML"
|
|
GoogleAuth SSOType = "GOOGLE_AUTH"
|
|
)
|
|
|
|
// OrgDomain identify org owned web domains for auth and other purposes
|
|
type OrgDomain struct {
|
|
Id uuid.UUID `json:"id"`
|
|
Name string `json:"name"`
|
|
OrgId string `json:"orgId"`
|
|
SsoEnabled bool `json:"ssoEnabled"`
|
|
SsoType SSOType `json:"ssoType"`
|
|
|
|
SamlConfig *SamlConfig `json:"samlConfig"`
|
|
GoogleAuthConfig *GoogleOAuthConfig `json:"googleAuthConfig"`
|
|
|
|
Org *basemodel.Organization
|
|
}
|
|
|
|
func (od *OrgDomain) String() string {
|
|
return fmt.Sprintf("[%s]%s-%s ", od.Name, od.Id.String(), od.SsoType)
|
|
}
|
|
|
|
// Valid is used a pipeline function to check if org domain
|
|
// loaded from db is valid
|
|
func (od *OrgDomain) Valid(err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if od.Id == uuid.Nil || od.OrgId == "" {
|
|
return fmt.Errorf("both id and orgId are required")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ValidNew cheks if the org domain is valid for insertion in db
|
|
func (od *OrgDomain) ValidNew() error {
|
|
|
|
if od.OrgId == "" {
|
|
return fmt.Errorf("orgId is required")
|
|
}
|
|
|
|
if od.Name == "" {
|
|
return fmt.Errorf("name is required")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// LoadConfig loads config params from json text
|
|
func (od *OrgDomain) LoadConfig(jsondata string) error {
|
|
d := *od
|
|
err := json.Unmarshal([]byte(jsondata), &d)
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to marshal json to OrgDomain{}")
|
|
}
|
|
*od = d
|
|
return nil
|
|
}
|
|
|
|
func (od *OrgDomain) GetSAMLEntityID() string {
|
|
if od.SamlConfig != nil {
|
|
return od.SamlConfig.SamlEntity
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (od *OrgDomain) GetSAMLIdpURL() string {
|
|
if od.SamlConfig != nil {
|
|
return od.SamlConfig.SamlIdp
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (od *OrgDomain) GetSAMLCert() string {
|
|
if od.SamlConfig != nil {
|
|
return od.SamlConfig.SamlCert
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// PrepareGoogleOAuthProvider creates GoogleProvider that is used in
|
|
// requesting OAuth and also used in processing response from google
|
|
func (od *OrgDomain) PrepareGoogleOAuthProvider(siteUrl *url.URL) (sso.OAuthCallbackProvider, error) {
|
|
if od.GoogleAuthConfig == nil {
|
|
return nil, fmt.Errorf("GOOGLE OAUTH is not setup correctly for this domain")
|
|
}
|
|
|
|
return od.GoogleAuthConfig.GetProvider(od.Name, siteUrl)
|
|
}
|
|
|
|
// PrepareSamlRequest creates a request accordingly gosaml2
|
|
func (od *OrgDomain) PrepareSamlRequest(siteUrl *url.URL) (*saml2.SAMLServiceProvider, error) {
|
|
|
|
// this is the url Idp will call after login completion
|
|
acs := fmt.Sprintf("%s://%s/%s",
|
|
siteUrl.Scheme,
|
|
siteUrl.Host,
|
|
"api/v1/complete/saml")
|
|
|
|
// this is the address of the calling url, useful to redirect user
|
|
sourceUrl := fmt.Sprintf("%s://%s%s",
|
|
siteUrl.Scheme,
|
|
siteUrl.Host,
|
|
siteUrl.Path)
|
|
|
|
// ideally this should be some unique ID for each installation
|
|
// but since we dont have UI to support it, we default it to
|
|
// host. this issuer is an identifier of service provider (signoz)
|
|
// on id provider (e.g. azure, okta). Azure requires this id to be configured
|
|
// in their system, while others seem to not care about it.
|
|
// currently we default it to host from window.location (received from browser)
|
|
issuer := siteUrl.Host
|
|
|
|
return saml.PrepareRequest(issuer, acs, sourceUrl, od.GetSAMLEntityID(), od.GetSAMLIdpURL(), od.GetSAMLCert())
|
|
}
|
|
|
|
func (od *OrgDomain) BuildSsoUrl(siteUrl *url.URL) (ssoUrl string, err error) {
|
|
|
|
fmtDomainId := strings.Replace(od.Id.String(), "-", ":", -1)
|
|
|
|
// build redirect url from window.location sent by frontend
|
|
redirectURL := fmt.Sprintf("%s://%s%s", siteUrl.Scheme, siteUrl.Host, siteUrl.Path)
|
|
|
|
// prepare state that gets relayed back when the auth provider
|
|
// calls back our url. here we pass the app url (where signoz runs)
|
|
// and the domain Id. The domain Id helps in identifying sso config
|
|
// when the call back occurs and the app url is useful in redirecting user
|
|
// back to the right path.
|
|
// why do we need to pass app url? the callback typically is handled by backend
|
|
// and sometimes backend might right at a different port or is unaware of frontend
|
|
// endpoint (unless SITE_URL param is set). hence, we receive this build sso request
|
|
// along with frontend window.location and use it to relay the information through
|
|
// auth provider to the backend (HandleCallback or HandleSSO method).
|
|
relayState := fmt.Sprintf("%s?domainId=%s", redirectURL, fmtDomainId)
|
|
|
|
switch od.SsoType {
|
|
case SAML:
|
|
|
|
sp, err := od.PrepareSamlRequest(siteUrl)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return sp.BuildAuthURL(relayState)
|
|
|
|
case GoogleAuth:
|
|
|
|
googleProvider, err := od.PrepareGoogleOAuthProvider(siteUrl)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return googleProvider.BuildAuthURL(relayState)
|
|
|
|
default:
|
|
zap.L().Error("found unsupported SSO config for the org domain", zap.String("orgDomain", od.Name))
|
|
return "", fmt.Errorf("unsupported SSO config for the domain")
|
|
}
|
|
|
|
}
|