@@ -59,6 +59,7 @@ type (
|
||||
Auth *auth.Service
|
||||
Authz *authz.Service
|
||||
Trust *trust.Service
|
||||
SAML *auth.SAMLService
|
||||
ConsoleAuth ConsoleAuthConfig
|
||||
TrustAuth TrustAuthConfig
|
||||
ConnectorRegistry *connector.ConnectorRegistry
|
||||
@@ -194,6 +195,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
s.cfg.ConnectorRegistry,
|
||||
s.cfg.SafeRedirect,
|
||||
s.cfg.CustomDomainCname,
|
||||
s.cfg.SAML,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package console_v1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/getprobo/probo/pkg/auth"
|
||||
"go.gearno.de/kit/httpserver"
|
||||
)
|
||||
|
||||
type (
|
||||
ForgetPasswordRequest struct {
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
ForgetPasswordResponse struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
)
|
||||
|
||||
func ForgetPasswordHandler(authSvc *auth.Service, authCfg AuthConfig) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req ForgetPasswordRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("cannot decode body: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
err := authSvc.ForgetPassword(r.Context(), req.Email)
|
||||
if err != nil {
|
||||
// For security reasons, we don't expose whether an email exists or not
|
||||
httpserver.RenderError(w, http.StatusInternalServerError, fmt.Errorf("cannot process request: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
httpserver.RenderJSON(w, http.StatusOK, ForgetPasswordResponse{
|
||||
Success: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package console_v1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"errors"
|
||||
|
||||
"github.com/getprobo/probo/pkg/auth"
|
||||
"go.gearno.de/kit/httpserver"
|
||||
)
|
||||
|
||||
type (
|
||||
ResetPasswordRequest struct {
|
||||
Token string `json:"token"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
ResetPasswordResponse struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
)
|
||||
|
||||
func ResetPasswordHandler(authSvc *auth.Service, authCfg AuthConfig) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req ResetPasswordRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("cannot decode body: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
err := authSvc.ResetPassword(r.Context(), req.Token, req.Password)
|
||||
if err != nil {
|
||||
var invalidPasswordErr *auth.ErrInvalidPassword
|
||||
var invalidTokenErr *auth.ErrInvalidTokenType
|
||||
|
||||
if errors.As(err, &invalidPasswordErr) {
|
||||
httpserver.RenderError(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
if errors.As(err, &invalidTokenErr) {
|
||||
httpserver.RenderError(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
httpserver.RenderError(w, http.StatusInternalServerError, fmt.Errorf("cannot reset password: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
httpserver.RenderJSON(w, http.StatusOK, ResetPasswordResponse{
|
||||
Success: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -60,11 +60,17 @@ type (
|
||||
proboSvc *probo.Service
|
||||
authSvc *auth.Service
|
||||
authzSvc *authz.Service
|
||||
samlSvc *auth.SAMLService
|
||||
authCfg AuthConfig
|
||||
customDomainCname string
|
||||
}
|
||||
|
||||
ctxKey struct{ name string }
|
||||
|
||||
userTenantAccess struct {
|
||||
tenantIDs []gid.TenantID
|
||||
authErrors map[gid.TenantID]error
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -92,6 +98,7 @@ func NewMux(
|
||||
connectorRegistry *connector.ConnectorRegistry,
|
||||
safeRedirect *saferedirect.SafeRedirect,
|
||||
customDomainCname string,
|
||||
samlSvc *auth.SAMLService,
|
||||
) *chi.Mux {
|
||||
r := chi.NewMux()
|
||||
|
||||
@@ -211,13 +218,6 @@ func NewMux(
|
||||
},
|
||||
)
|
||||
|
||||
r.Post("/auth/register", SignUpHandler(authSvc, authCfg))
|
||||
r.Post("/auth/login", SignInHandler(authSvc, authCfg))
|
||||
r.Delete("/auth/logout", SignOutHandler(authSvc, authCfg))
|
||||
r.Post("/auth/signup-from-invitation", SignupFromInvitationHandler(authSvc, authCfg))
|
||||
r.Post("/auth/forget-password", ForgetPasswordHandler(authSvc, authCfg))
|
||||
r.Post("/auth/reset-password", ResetPasswordHandler(authSvc, authCfg))
|
||||
|
||||
r.Get("/connectors/initiate", WithSession(authSvc, authzSvc, authCfg, func(w http.ResponseWriter, r *http.Request) {
|
||||
provider := r.URL.Query().Get("provider")
|
||||
if provider != "SLACK" {
|
||||
@@ -295,12 +295,12 @@ func NewMux(
|
||||
})
|
||||
|
||||
r.Get("/", playground.Handler("GraphQL", "/api/console/v1/query"))
|
||||
r.Post("/query", graphqlHandler(logger, proboSvc, authSvc, authzSvc, authCfg, customDomainCname))
|
||||
r.Post("/query", graphqlHandler(logger, proboSvc, authSvc, authzSvc, samlSvc, authCfg, customDomainCname))
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func graphqlHandler(logger *log.Logger, proboSvc *probo.Service, authSvc *auth.Service, authzSvc *authz.Service, authCfg AuthConfig, customDomainCname string) http.HandlerFunc {
|
||||
func graphqlHandler(logger *log.Logger, proboSvc *probo.Service, authSvc *auth.Service, authzSvc *authz.Service, samlSvc *auth.SAMLService, authCfg AuthConfig, customDomainCname string) http.HandlerFunc {
|
||||
var mb int64 = 1 << 20
|
||||
|
||||
es := schema.NewExecutableSchema(
|
||||
@@ -309,6 +309,7 @@ func graphqlHandler(logger *log.Logger, proboSvc *probo.Service, authSvc *auth.S
|
||||
proboSvc: proboSvc,
|
||||
authSvc: authSvc,
|
||||
authzSvc: authzSvc,
|
||||
samlSvc: samlSvc,
|
||||
authCfg: authCfg,
|
||||
customDomainCname: customDomainCname,
|
||||
},
|
||||
@@ -387,7 +388,10 @@ func WithSession(authSvc *auth.Service, authzSvc *authz.Service, authCfg AuthCon
|
||||
|
||||
ctx = context.WithValue(ctx, sessionContextKey, authResult.Session)
|
||||
ctx = context.WithValue(ctx, userContextKey, authResult.User)
|
||||
ctx = context.WithValue(ctx, userTenantContextKey, &authResult.TenantIDs)
|
||||
ctx = context.WithValue(ctx, userTenantContextKey, &userTenantAccess{
|
||||
tenantIDs: authResult.TenantIDs,
|
||||
authErrors: authResult.AuthErrors,
|
||||
})
|
||||
|
||||
next(w, r.WithContext(ctx))
|
||||
|
||||
@@ -425,13 +429,19 @@ func GetTenantAuthzService(ctx context.Context, authzSvc *authz.Service, tenantI
|
||||
}
|
||||
|
||||
func validateTenantAccess(ctx context.Context, tenantID gid.TenantID) {
|
||||
tenantIDs, _ := ctx.Value(userTenantContextKey).(*[]gid.TenantID)
|
||||
access, _ := ctx.Value(userTenantContextKey).(*userTenantAccess)
|
||||
|
||||
if tenantIDs == nil {
|
||||
if access == nil {
|
||||
panic(fmt.Errorf("tenant not found"))
|
||||
}
|
||||
|
||||
if !slices.Contains(*tenantIDs, tenantID) {
|
||||
panic(fmt.Errorf("tenant not found"))
|
||||
if !slices.Contains(access.tenantIDs, tenantID) {
|
||||
if access.authErrors != nil {
|
||||
if authErr := access.authErrors[tenantID]; authErr != nil {
|
||||
panic(authErr)
|
||||
}
|
||||
}
|
||||
|
||||
panic(fmt.Errorf("access denied to tenant"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,6 +162,34 @@ enum AuditState
|
||||
@goEnum(value: "github.com/getprobo/probo/pkg/coredata.AuditStateOutdated")
|
||||
}
|
||||
|
||||
enum SAMLEnforcementPolicy
|
||||
@goModel(
|
||||
model: "github.com/getprobo/probo/pkg/coredata.SAMLEnforcementPolicy"
|
||||
) {
|
||||
OFF
|
||||
@goEnum(
|
||||
value: "github.com/getprobo/probo/pkg/coredata.SAMLEnforcementPolicyOff"
|
||||
)
|
||||
OPTIONAL
|
||||
@goEnum(
|
||||
value: "github.com/getprobo/probo/pkg/coredata.SAMLEnforcementPolicyOptional"
|
||||
)
|
||||
REQUIRED
|
||||
@goEnum(
|
||||
value: "github.com/getprobo/probo/pkg/coredata.SAMLEnforcementPolicyRequired"
|
||||
)
|
||||
}
|
||||
|
||||
enum UserAuthMethod
|
||||
@goModel(model: "github.com/getprobo/probo/pkg/coredata.UserAuthMethod") {
|
||||
PASSWORD
|
||||
@goEnum(
|
||||
value: "github.com/getprobo/probo/pkg/coredata.UserAuthMethodPassword"
|
||||
)
|
||||
SAML
|
||||
@goEnum(value: "github.com/getprobo/probo/pkg/coredata.UserAuthMethodSAML")
|
||||
}
|
||||
|
||||
enum TrustCenterVisibility
|
||||
@goModel(
|
||||
model: "github.com/getprobo/probo/pkg/coredata.TrustCenterVisibility"
|
||||
@@ -1791,6 +1819,8 @@ type Organization implements Node {
|
||||
|
||||
customDomain: CustomDomain @goField(forceResolver: true)
|
||||
|
||||
samlConfigurations: [SAMLConfiguration!]! @goField(forceResolver: true)
|
||||
|
||||
createdAt: Datetime!
|
||||
updatedAt: Datetime!
|
||||
}
|
||||
@@ -1810,6 +1840,7 @@ type Membership implements Node {
|
||||
role: String!
|
||||
fullName: String!
|
||||
emailAddress: String!
|
||||
authMethod: UserAuthMethod! @goField(forceResolver: true)
|
||||
createdAt: Datetime!
|
||||
updatedAt: Datetime!
|
||||
}
|
||||
@@ -2693,7 +2724,6 @@ type VendorServiceEdge {
|
||||
node: VendorService!
|
||||
}
|
||||
|
||||
|
||||
type VendorRiskAssessmentConnection {
|
||||
edges: [VendorRiskAssessmentEdge!]!
|
||||
pageInfo: PageInfo!
|
||||
@@ -3174,6 +3204,28 @@ type Mutation {
|
||||
deleteCustomDomain(
|
||||
input: DeleteCustomDomainInput!
|
||||
): DeleteCustomDomainPayload!
|
||||
|
||||
# SAML Configuration mutations (OWNER/ADMIN only)
|
||||
# Step 1: Initiate domain verification (creates SAML config with unverified domain)
|
||||
initiateDomainVerification(
|
||||
input: InitiateDomainVerificationInput!
|
||||
): InitiateDomainVerificationPayload!
|
||||
|
||||
# Step 2: Verify domain ownership via DNS TXT record
|
||||
verifyDomain(input: VerifyDomainInput!): VerifyDomainPayload!
|
||||
|
||||
# Step 3: Configure SAML (only allowed after domain is verified)
|
||||
createSAMLConfiguration(
|
||||
input: CreateSAMLConfigurationInput!
|
||||
): CreateSAMLConfigurationPayload!
|
||||
updateSAMLConfiguration(
|
||||
input: UpdateSAMLConfigurationInput!
|
||||
): UpdateSAMLConfigurationPayload!
|
||||
deleteSAMLConfiguration(
|
||||
input: DeleteSAMLConfigurationInput!
|
||||
): DeleteSAMLConfigurationPayload!
|
||||
enableSAML(input: EnableSAMLInput!): EnableSAMLPayload!
|
||||
disableSAML(input: DisableSAMLInput!): DisableSAMLPayload!
|
||||
}
|
||||
|
||||
# Input Types
|
||||
@@ -4794,3 +4846,167 @@ type CreateCustomDomainPayload {
|
||||
type DeleteCustomDomainPayload {
|
||||
deletedCustomDomainId: ID!
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# SAML Configuration Types
|
||||
# ============================================
|
||||
|
||||
type SAMLConfiguration implements Node {
|
||||
id: ID!
|
||||
organization: Organization! @goField(forceResolver: true)
|
||||
emailDomain: String!
|
||||
enabled: Boolean!
|
||||
enforcementPolicy: SAMLEnforcementPolicy!
|
||||
|
||||
# Domain verification (required before SAML can be configured)
|
||||
domainVerified: Boolean!
|
||||
domainVerificationToken: String
|
||||
domainVerifiedAt: Datetime
|
||||
|
||||
# Service Provider metadata (read-only, auto-generated)
|
||||
spEntityId: String!
|
||||
spAcsUrl: String!
|
||||
spMetadataUrl: String! @goField(forceResolver: true)
|
||||
|
||||
# Identity Provider configuration
|
||||
idpEntityId: String!
|
||||
idpSsoUrl: String!
|
||||
idpCertificate: String!
|
||||
idpMetadataUrl: String
|
||||
|
||||
# Attribute mapping
|
||||
attributeEmail: String!
|
||||
attributeFirstname: String!
|
||||
attributeLastname: String!
|
||||
attributeRole: String!
|
||||
|
||||
# Default role for users when role attribute is missing or invalid
|
||||
defaultRole: String!
|
||||
|
||||
# Auto-signup
|
||||
autoSignupEnabled: Boolean!
|
||||
|
||||
# Test login URL for this configuration
|
||||
testLoginUrl: String! @goField(forceResolver: true)
|
||||
|
||||
createdAt: Datetime!
|
||||
updatedAt: Datetime!
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# SAML Configuration Inputs
|
||||
# ============================================
|
||||
|
||||
input CreateSAMLConfigurationInput {
|
||||
organizationId: ID!
|
||||
|
||||
# Email domain this config applies to
|
||||
emailDomain: String!
|
||||
|
||||
# Enforcement policy for this SAML configuration
|
||||
enforcementPolicy: SAMLEnforcementPolicy!
|
||||
|
||||
# SP configuration (optional - auto-generated if not provided)
|
||||
spCertificate: String
|
||||
spPrivateKey: String
|
||||
|
||||
# IdP configuration - Option 1: Provide metadata XML (recommended for Google Workspace)
|
||||
# This will automatically extract entityId, ssoUrl, and certificate from the metadata
|
||||
idpMetadataXml: String
|
||||
|
||||
# IdP configuration - Option 2: Provide individual fields manually
|
||||
# Required if idpMetadataXml is not provided
|
||||
idpEntityId: String
|
||||
idpSsoUrl: String
|
||||
idpCertificate: String
|
||||
idpMetadataUrl: String
|
||||
|
||||
# Attribute mapping (optional, defaults provided)
|
||||
attributeEmail: String
|
||||
attributeFirstname: String
|
||||
attributeLastname: String
|
||||
attributeRole: String
|
||||
|
||||
defaultRole: String
|
||||
autoSignupEnabled: Boolean
|
||||
}
|
||||
|
||||
input UpdateSAMLConfigurationInput {
|
||||
id: ID!
|
||||
|
||||
enabled: Boolean
|
||||
enforcementPolicy: SAMLEnforcementPolicy
|
||||
spCertificate: String
|
||||
spPrivateKey: String
|
||||
idpEntityId: String
|
||||
idpSsoUrl: String
|
||||
idpCertificate: String
|
||||
idpMetadataUrl: String
|
||||
attributeEmail: String
|
||||
attributeFirstname: String
|
||||
attributeLastname: String
|
||||
attributeRole: String
|
||||
defaultRole: String
|
||||
autoSignupEnabled: Boolean
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# Domain Verification Inputs
|
||||
# ============================================
|
||||
|
||||
input InitiateDomainVerificationInput {
|
||||
organizationId: ID!
|
||||
emailDomain: String!
|
||||
}
|
||||
|
||||
input VerifyDomainInput {
|
||||
id: ID!
|
||||
}
|
||||
|
||||
input DeleteSAMLConfigurationInput {
|
||||
id: ID!
|
||||
}
|
||||
|
||||
input EnableSAMLInput {
|
||||
id: ID!
|
||||
}
|
||||
|
||||
input DisableSAMLInput {
|
||||
id: ID!
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# SAML Configuration Payloads
|
||||
# ============================================
|
||||
|
||||
type InitiateDomainVerificationPayload {
|
||||
samlConfiguration: SAMLConfiguration!
|
||||
# The TXT record value that needs to be added to DNS
|
||||
# Format: probo-verification={token}
|
||||
dnsRecord: String!
|
||||
}
|
||||
|
||||
type VerifyDomainPayload {
|
||||
samlConfiguration: SAMLConfiguration!
|
||||
verified: Boolean!
|
||||
}
|
||||
|
||||
type CreateSAMLConfigurationPayload {
|
||||
samlConfiguration: SAMLConfiguration!
|
||||
}
|
||||
|
||||
type UpdateSAMLConfigurationPayload {
|
||||
samlConfiguration: SAMLConfiguration!
|
||||
}
|
||||
|
||||
type DeleteSAMLConfigurationPayload {
|
||||
deletedSAMLConfigurationId: ID!
|
||||
}
|
||||
|
||||
type EnableSAMLPayload {
|
||||
samlConfiguration: SAMLConfiguration!
|
||||
}
|
||||
|
||||
type DisableSAMLPayload {
|
||||
samlConfiguration: SAMLConfiguration!
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,92 +0,0 @@
|
||||
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package console_v1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/getprobo/probo/pkg/gid"
|
||||
"github.com/getprobo/probo/pkg/securecookie"
|
||||
"github.com/getprobo/probo/pkg/auth"
|
||||
"go.gearno.de/kit/httpserver"
|
||||
)
|
||||
|
||||
type (
|
||||
SignInRequest struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
SignInResponse struct {
|
||||
User UserResponse `json:"user"`
|
||||
}
|
||||
|
||||
UserResponse struct {
|
||||
ID gid.GID `json:"id"`
|
||||
Email string `json:"email"`
|
||||
FullName string `json:"fullName"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
)
|
||||
|
||||
func SignInHandler(authSvc *auth.Service, authCfg AuthConfig) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var req SignInRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("cannot decode body: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
session, user, err := authSvc.SignIn(r.Context(), req.Email, req.Password)
|
||||
if err != nil {
|
||||
var ErrInvalidCredentials *auth.ErrInvalidCredentials
|
||||
if errors.As(err, &ErrInvalidCredentials) {
|
||||
httpserver.RenderError(w, http.StatusUnauthorized, err)
|
||||
return
|
||||
}
|
||||
|
||||
panic(fmt.Errorf("cannot sign in: %w", err))
|
||||
}
|
||||
|
||||
securecookie.Set(
|
||||
w,
|
||||
securecookie.DefaultConfig(
|
||||
authCfg.CookieName,
|
||||
authCfg.CookieSecret,
|
||||
),
|
||||
session.ID.String(),
|
||||
)
|
||||
|
||||
httpserver.RenderJSON(
|
||||
w,
|
||||
http.StatusOK,
|
||||
SignInResponse{
|
||||
User: UserResponse{
|
||||
ID: user.ID,
|
||||
Email: user.EmailAddress,
|
||||
FullName: user.FullName,
|
||||
CreatedAt: user.CreatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package console_v1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/getprobo/probo/pkg/gid"
|
||||
"github.com/getprobo/probo/pkg/securecookie"
|
||||
"github.com/getprobo/probo/pkg/auth"
|
||||
"go.gearno.de/kit/httpserver"
|
||||
)
|
||||
|
||||
func SignOutHandler(authSvc *auth.Service, authCfg AuthConfig) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
sessionID, err := securecookie.Get(r, securecookie.DefaultConfig(
|
||||
authCfg.CookieName,
|
||||
authCfg.CookieSecret,
|
||||
))
|
||||
if err != nil {
|
||||
httpserver.RenderError(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
gid, err := gid.ParseGID(sessionID)
|
||||
if err != nil {
|
||||
httpserver.RenderError(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = authSvc.SignOut(r.Context(), gid)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot sign out: %w", err))
|
||||
}
|
||||
|
||||
securecookie.Clear(w, securecookie.DefaultConfig(
|
||||
authCfg.CookieName,
|
||||
authCfg.CookieSecret,
|
||||
))
|
||||
|
||||
w.Header().Set("Clear-Site-Data", "*")
|
||||
|
||||
httpserver.RenderJSON(w, http.StatusOK, map[string]bool{"success": true})
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package console_v1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/getprobo/probo/pkg/auth"
|
||||
"github.com/getprobo/probo/pkg/securecookie"
|
||||
"go.gearno.de/kit/httpserver"
|
||||
)
|
||||
|
||||
type (
|
||||
SignUpRequest struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
FullName string `json:"fullName"`
|
||||
}
|
||||
|
||||
SignUpResponse struct {
|
||||
User UserResponse `json:"user"`
|
||||
}
|
||||
)
|
||||
|
||||
func SignUpHandler(authSvc *auth.Service, authCfg AuthConfig) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req SignUpRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("cannot decode body: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
user, session, err := authSvc.SignUp(
|
||||
r.Context(),
|
||||
req.Email,
|
||||
req.Password,
|
||||
req.FullName,
|
||||
)
|
||||
if err != nil {
|
||||
var errUserAlreadyExists *auth.ErrUserAlreadyExists
|
||||
if errors.As(err, &errUserAlreadyExists) {
|
||||
httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("cannot register user: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
var errSignupDisabled *auth.ErrSignupDisabled
|
||||
if errors.As(err, &errSignupDisabled) {
|
||||
httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("cannot register user: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
panic(fmt.Errorf("cannot register user: %w", err))
|
||||
}
|
||||
|
||||
securecookie.Set(
|
||||
w,
|
||||
securecookie.DefaultConfig(
|
||||
authCfg.CookieName,
|
||||
authCfg.CookieSecret,
|
||||
),
|
||||
session.ID.String(),
|
||||
)
|
||||
|
||||
httpserver.RenderJSON(
|
||||
w,
|
||||
http.StatusOK,
|
||||
SignUpResponse{
|
||||
User: UserResponse{
|
||||
ID: user.ID,
|
||||
Email: user.EmailAddress,
|
||||
FullName: user.FullName,
|
||||
CreatedAt: user.CreatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package console_v1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/getprobo/probo/pkg/auth"
|
||||
"github.com/getprobo/probo/pkg/securecookie"
|
||||
"go.gearno.de/kit/httpserver"
|
||||
)
|
||||
|
||||
type (
|
||||
SignupFromInvitationRequest struct {
|
||||
Token string `json:"token"`
|
||||
Password string `json:"password"`
|
||||
FullName string `json:"fullName"`
|
||||
}
|
||||
|
||||
SignupFromInvitationResponse struct {
|
||||
}
|
||||
)
|
||||
|
||||
func SignupFromInvitationHandler(authSvc *auth.Service, authCfg AuthConfig) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req SignupFromInvitationRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("cannot decode body: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
user, session, err := authSvc.SignupFromInvitation(r.Context(), req.Token, req.Password, req.FullName)
|
||||
if err != nil {
|
||||
httpserver.RenderError(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
securecookie.Set(
|
||||
w,
|
||||
securecookie.DefaultConfig(
|
||||
authCfg.CookieName,
|
||||
authCfg.CookieSecret,
|
||||
),
|
||||
session.ID.String(),
|
||||
)
|
||||
|
||||
httpserver.RenderJSON(
|
||||
w,
|
||||
http.StatusOK,
|
||||
SignUpResponse{
|
||||
User: UserResponse{
|
||||
ID: user.ID,
|
||||
Email: user.EmailAddress,
|
||||
FullName: user.FullName,
|
||||
CreatedAt: user.CreatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
45
pkg/server/api/console/v1/types/saml_configuration.go
Normal file
45
pkg/server/api/console/v1/types/saml_configuration.go
Normal file
@@ -0,0 +1,45 @@
|
||||
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"github.com/getprobo/probo/pkg/coredata"
|
||||
)
|
||||
|
||||
func NewSAMLConfigurationWithURLs(c *coredata.SAMLConfiguration, spEntityID, spAcsURL string) *SAMLConfiguration {
|
||||
return &SAMLConfiguration{
|
||||
ID: c.ID,
|
||||
EmailDomain: c.EmailDomain,
|
||||
Enabled: c.Enabled,
|
||||
EnforcementPolicy: c.EnforcementPolicy,
|
||||
DomainVerified: c.DomainVerified,
|
||||
DomainVerificationToken: c.DomainVerificationToken,
|
||||
DomainVerifiedAt: c.DomainVerifiedAt,
|
||||
SpEntityID: spEntityID,
|
||||
SpAcsURL: spAcsURL,
|
||||
IdpEntityID: c.IdPEntityID,
|
||||
IdpSsoURL: c.IdPSsoURL,
|
||||
IdpCertificate: c.IdPCertificate,
|
||||
IdpMetadataURL: c.IdPMetadataURL,
|
||||
AttributeEmail: c.AttributeEmail,
|
||||
AttributeFirstname: c.AttributeFirstname,
|
||||
AttributeLastname: c.AttributeLastname,
|
||||
AttributeRole: c.AttributeRole,
|
||||
DefaultRole: c.DefaultRole,
|
||||
AutoSignupEnabled: c.AutoSignupEnabled,
|
||||
CreatedAt: c.CreatedAt,
|
||||
UpdatedAt: c.UpdatedAt,
|
||||
}
|
||||
}
|
||||
@@ -505,6 +505,29 @@ type CreateRiskPayload struct {
|
||||
RiskEdge *RiskEdge `json:"riskEdge"`
|
||||
}
|
||||
|
||||
type CreateSAMLConfigurationInput struct {
|
||||
OrganizationID gid.GID `json:"organizationId"`
|
||||
EmailDomain string `json:"emailDomain"`
|
||||
EnforcementPolicy coredata.SAMLEnforcementPolicy `json:"enforcementPolicy"`
|
||||
SpCertificate *string `json:"spCertificate,omitempty"`
|
||||
SpPrivateKey *string `json:"spPrivateKey,omitempty"`
|
||||
IdpMetadataXML *string `json:"idpMetadataXml,omitempty"`
|
||||
IdpEntityID *string `json:"idpEntityId,omitempty"`
|
||||
IdpSsoURL *string `json:"idpSsoUrl,omitempty"`
|
||||
IdpCertificate *string `json:"idpCertificate,omitempty"`
|
||||
IdpMetadataURL *string `json:"idpMetadataUrl,omitempty"`
|
||||
AttributeEmail *string `json:"attributeEmail,omitempty"`
|
||||
AttributeFirstname *string `json:"attributeFirstname,omitempty"`
|
||||
AttributeLastname *string `json:"attributeLastname,omitempty"`
|
||||
AttributeRole *string `json:"attributeRole,omitempty"`
|
||||
DefaultRole *string `json:"defaultRole,omitempty"`
|
||||
AutoSignupEnabled *bool `json:"autoSignupEnabled,omitempty"`
|
||||
}
|
||||
|
||||
type CreateSAMLConfigurationPayload struct {
|
||||
SamlConfiguration *SAMLConfiguration `json:"samlConfiguration"`
|
||||
}
|
||||
|
||||
type CreateSnapshotInput struct {
|
||||
OrganizationID gid.GID `json:"organizationId"`
|
||||
Name string `json:"name"`
|
||||
@@ -904,6 +927,14 @@ type DeleteRiskPayload struct {
|
||||
DeletedRiskID gid.GID `json:"deletedRiskId"`
|
||||
}
|
||||
|
||||
type DeleteSAMLConfigurationInput struct {
|
||||
ID gid.GID `json:"id"`
|
||||
}
|
||||
|
||||
type DeleteSAMLConfigurationPayload struct {
|
||||
DeletedSAMLConfigurationID gid.GID `json:"deletedSAMLConfigurationId"`
|
||||
}
|
||||
|
||||
type DeleteSnapshotInput struct {
|
||||
SnapshotID gid.GID `json:"snapshotId"`
|
||||
}
|
||||
@@ -1000,6 +1031,14 @@ type DeleteVendorServicePayload struct {
|
||||
DeletedVendorServiceID gid.GID `json:"deletedVendorServiceId"`
|
||||
}
|
||||
|
||||
type DisableSAMLInput struct {
|
||||
ID gid.GID `json:"id"`
|
||||
}
|
||||
|
||||
type DisableSAMLPayload struct {
|
||||
SamlConfiguration *SAMLConfiguration `json:"samlConfiguration"`
|
||||
}
|
||||
|
||||
type Document struct {
|
||||
ID gid.GID `json:"id"`
|
||||
Title string `json:"title"`
|
||||
@@ -1094,6 +1133,14 @@ type DocumentVersionSignatureOrder struct {
|
||||
Direction page.OrderDirection `json:"direction"`
|
||||
}
|
||||
|
||||
type EnableSAMLInput struct {
|
||||
ID gid.GID `json:"id"`
|
||||
}
|
||||
|
||||
type EnableSAMLPayload struct {
|
||||
SamlConfiguration *SAMLConfiguration `json:"samlConfiguration"`
|
||||
}
|
||||
|
||||
type Evidence struct {
|
||||
ID gid.GID `json:"id"`
|
||||
Size int `json:"size"`
|
||||
@@ -1216,6 +1263,16 @@ type ImportMeasurePayload struct {
|
||||
MeasureEdges []*MeasureEdge `json:"measureEdges"`
|
||||
}
|
||||
|
||||
type InitiateDomainVerificationInput struct {
|
||||
OrganizationID gid.GID `json:"organizationId"`
|
||||
EmailDomain string `json:"emailDomain"`
|
||||
}
|
||||
|
||||
type InitiateDomainVerificationPayload struct {
|
||||
SamlConfiguration *SAMLConfiguration `json:"samlConfiguration"`
|
||||
DNSRecord string `json:"dnsRecord"`
|
||||
}
|
||||
|
||||
type Invitation struct {
|
||||
ID gid.GID `json:"id"`
|
||||
Email string `json:"email"`
|
||||
@@ -1284,14 +1341,15 @@ type MeasureFilter struct {
|
||||
}
|
||||
|
||||
type Membership struct {
|
||||
ID gid.GID `json:"id"`
|
||||
UserID gid.GID `json:"userID"`
|
||||
OrganizationID gid.GID `json:"organizationID"`
|
||||
Role string `json:"role"`
|
||||
FullName string `json:"fullName"`
|
||||
EmailAddress string `json:"emailAddress"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
ID gid.GID `json:"id"`
|
||||
UserID gid.GID `json:"userID"`
|
||||
OrganizationID gid.GID `json:"organizationID"`
|
||||
Role string `json:"role"`
|
||||
FullName string `json:"fullName"`
|
||||
EmailAddress string `json:"emailAddress"`
|
||||
AuthMethod coredata.UserAuthMethod `json:"authMethod"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
func (Membership) IsNode() {}
|
||||
@@ -1396,6 +1454,7 @@ type Organization struct {
|
||||
TrustCenterFiles *TrustCenterFileConnection `json:"trustCenterFiles"`
|
||||
TrustCenter *TrustCenter `json:"trustCenter,omitempty"`
|
||||
CustomDomain *CustomDomain `json:"customDomain,omitempty"`
|
||||
SamlConfigurations []*SAMLConfiguration `json:"samlConfigurations"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
@@ -1580,6 +1639,36 @@ type RiskFilter struct {
|
||||
SnapshotID *gid.GID `json:"snapshotId,omitempty"`
|
||||
}
|
||||
|
||||
type SAMLConfiguration struct {
|
||||
ID gid.GID `json:"id"`
|
||||
Organization *Organization `json:"organization"`
|
||||
EmailDomain string `json:"emailDomain"`
|
||||
Enabled bool `json:"enabled"`
|
||||
EnforcementPolicy coredata.SAMLEnforcementPolicy `json:"enforcementPolicy"`
|
||||
DomainVerified bool `json:"domainVerified"`
|
||||
DomainVerificationToken *string `json:"domainVerificationToken,omitempty"`
|
||||
DomainVerifiedAt *time.Time `json:"domainVerifiedAt,omitempty"`
|
||||
SpEntityID string `json:"spEntityId"`
|
||||
SpAcsURL string `json:"spAcsUrl"`
|
||||
SpMetadataURL string `json:"spMetadataUrl"`
|
||||
IdpEntityID string `json:"idpEntityId"`
|
||||
IdpSsoURL string `json:"idpSsoUrl"`
|
||||
IdpCertificate string `json:"idpCertificate"`
|
||||
IdpMetadataURL *string `json:"idpMetadataUrl,omitempty"`
|
||||
AttributeEmail string `json:"attributeEmail"`
|
||||
AttributeFirstname string `json:"attributeFirstname"`
|
||||
AttributeLastname string `json:"attributeLastname"`
|
||||
AttributeRole string `json:"attributeRole"`
|
||||
DefaultRole string `json:"defaultRole"`
|
||||
AutoSignupEnabled bool `json:"autoSignupEnabled"`
|
||||
TestLoginURL string `json:"testLoginUrl"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
func (SAMLConfiguration) IsNode() {}
|
||||
func (this SAMLConfiguration) GetID() gid.GID { return this.ID }
|
||||
|
||||
type SendSigningNotificationsInput struct {
|
||||
OrganizationID gid.GID `json:"organizationId"`
|
||||
}
|
||||
@@ -1973,6 +2062,28 @@ type UpdateRiskPayload struct {
|
||||
Risk *Risk `json:"risk"`
|
||||
}
|
||||
|
||||
type UpdateSAMLConfigurationInput struct {
|
||||
ID gid.GID `json:"id"`
|
||||
Enabled *bool `json:"enabled,omitempty"`
|
||||
EnforcementPolicy *coredata.SAMLEnforcementPolicy `json:"enforcementPolicy,omitempty"`
|
||||
SpCertificate *string `json:"spCertificate,omitempty"`
|
||||
SpPrivateKey *string `json:"spPrivateKey,omitempty"`
|
||||
IdpEntityID *string `json:"idpEntityId,omitempty"`
|
||||
IdpSsoURL *string `json:"idpSsoUrl,omitempty"`
|
||||
IdpCertificate *string `json:"idpCertificate,omitempty"`
|
||||
IdpMetadataURL *string `json:"idpMetadataUrl,omitempty"`
|
||||
AttributeEmail *string `json:"attributeEmail,omitempty"`
|
||||
AttributeFirstname *string `json:"attributeFirstname,omitempty"`
|
||||
AttributeLastname *string `json:"attributeLastname,omitempty"`
|
||||
AttributeRole *string `json:"attributeRole,omitempty"`
|
||||
DefaultRole *string `json:"defaultRole,omitempty"`
|
||||
AutoSignupEnabled *bool `json:"autoSignupEnabled,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateSAMLConfigurationPayload struct {
|
||||
SamlConfiguration *SAMLConfiguration `json:"samlConfiguration"`
|
||||
}
|
||||
|
||||
type UpdateTaskInput struct {
|
||||
TaskID gid.GID `json:"taskId"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
@@ -2359,6 +2470,15 @@ type VendorServiceEdge struct {
|
||||
Node *VendorService `json:"node"`
|
||||
}
|
||||
|
||||
type VerifyDomainInput struct {
|
||||
ID gid.GID `json:"id"`
|
||||
}
|
||||
|
||||
type VerifyDomainPayload struct {
|
||||
SamlConfiguration *SAMLConfiguration `json:"samlConfiguration"`
|
||||
Verified bool `json:"verified"`
|
||||
}
|
||||
|
||||
type Viewer struct {
|
||||
ID gid.GID `json:"id"`
|
||||
User *User `json:"user"`
|
||||
|
||||
@@ -10,8 +10,10 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/getprobo/probo/pkg/auth"
|
||||
"github.com/getprobo/probo/pkg/authz"
|
||||
"github.com/getprobo/probo/pkg/coredata"
|
||||
"github.com/getprobo/probo/pkg/gid"
|
||||
@@ -1081,6 +1083,20 @@ func (r *measureConnectionResolver) TotalCount(ctx context.Context, obj *types.M
|
||||
panic(fmt.Errorf("unsupported resolver: %T", obj.Resolver))
|
||||
}
|
||||
|
||||
// AuthMethod is the resolver for the authMethod field.
|
||||
func (r *membershipResolver) AuthMethod(ctx context.Context, obj *types.Membership) (coredata.UserAuthMethod, error) {
|
||||
session := SessionFromContext(ctx)
|
||||
if session == nil {
|
||||
return coredata.UserAuthMethodPassword, nil
|
||||
}
|
||||
|
||||
authMethod, err := r.authSvc.GetUserAuthMethod(ctx, coredata.NewScope(obj.UserID.TenantID()), obj.UserID, obj.OrganizationID, session)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot get user auth method: %w", err)
|
||||
}
|
||||
return authMethod, nil
|
||||
}
|
||||
|
||||
// TotalCount is the resolver for the totalCount field.
|
||||
func (r *membershipConnectionResolver) TotalCount(ctx context.Context, obj *types.MembershipConnection) (int, error) {
|
||||
switch obj.Resolver.(type) {
|
||||
@@ -1098,6 +1114,8 @@ func (r *membershipConnectionResolver) TotalCount(ctx context.Context, obj *type
|
||||
|
||||
// CreateOrganization is the resolver for the createOrganization field.
|
||||
func (r *mutationResolver) CreateOrganization(ctx context.Context, input types.CreateOrganizationInput) (*types.CreateOrganizationPayload, error) {
|
||||
currentUser := UserFromContext(ctx)
|
||||
|
||||
prb := r.proboSvc.WithTenant(gid.NewTenantID())
|
||||
|
||||
organization, err := prb.Organizations.Create(
|
||||
@@ -1112,7 +1130,7 @@ func (r *mutationResolver) CreateOrganization(ctx context.Context, input types.C
|
||||
|
||||
err = r.authzSvc.AddUserToOrganization(
|
||||
ctx,
|
||||
UserFromContext(ctx).ID,
|
||||
currentUser.ID,
|
||||
organization.ID,
|
||||
string(authz.RoleMember),
|
||||
)
|
||||
@@ -1129,8 +1147,8 @@ func (r *mutationResolver) CreateOrganization(ctx context.Context, input types.C
|
||||
ctx,
|
||||
probo.CreatePeopleRequest{
|
||||
OrganizationID: organization.ID,
|
||||
FullName: UserFromContext(ctx).FullName,
|
||||
PrimaryEmailAddress: UserFromContext(ctx).EmailAddress,
|
||||
FullName: currentUser.FullName,
|
||||
PrimaryEmailAddress: currentUser.EmailAddress,
|
||||
AdditionalEmailAddresses: []string{},
|
||||
Kind: coredata.PeopleKindEmployee,
|
||||
},
|
||||
@@ -3537,6 +3555,260 @@ func (r *mutationResolver) DeleteCustomDomain(ctx context.Context, input types.D
|
||||
}, nil
|
||||
}
|
||||
|
||||
// InitiateDomainVerification is the resolver for the initiateDomainVerification field.
|
||||
func (r *mutationResolver) InitiateDomainVerification(ctx context.Context, input types.InitiateDomainVerificationInput) (*types.InitiateDomainVerificationPayload, error) {
|
||||
user := UserFromContext(ctx)
|
||||
if user == nil {
|
||||
return nil, fmt.Errorf("user not authenticated")
|
||||
}
|
||||
|
||||
organizationID := input.OrganizationID
|
||||
tenantID := organizationID.TenantID()
|
||||
|
||||
config, err := r.authSvc.InitiateDomainVerification(ctx, tenantID, organizationID, input.EmailDomain)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initiate domain verification: %w", err)
|
||||
}
|
||||
|
||||
dnsRecord := auth.GetDomainVerificationRecord(*config.DomainVerificationToken)
|
||||
|
||||
return &types.InitiateDomainVerificationPayload{
|
||||
SamlConfiguration: types.NewSAMLConfigurationWithURLs(
|
||||
config,
|
||||
r.samlSvc.GetEntityID(),
|
||||
r.samlSvc.GetAcsURL(),
|
||||
),
|
||||
DNSRecord: dnsRecord,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// VerifyDomain is the resolver for the verifyDomain field.
|
||||
func (r *mutationResolver) VerifyDomain(ctx context.Context, input types.VerifyDomainInput) (*types.VerifyDomainPayload, error) {
|
||||
user := UserFromContext(ctx)
|
||||
if user == nil {
|
||||
return nil, fmt.Errorf("user not authenticated")
|
||||
}
|
||||
|
||||
configID := input.ID
|
||||
tenantID := configID.TenantID()
|
||||
|
||||
config, verified, err := r.authSvc.VerifyDomain(ctx, tenantID, configID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to verify domain: %w", err)
|
||||
}
|
||||
|
||||
return &types.VerifyDomainPayload{
|
||||
SamlConfiguration: types.NewSAMLConfigurationWithURLs(
|
||||
config,
|
||||
r.samlSvc.GetEntityID(),
|
||||
r.samlSvc.GetAcsURL(),
|
||||
),
|
||||
Verified: verified,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateSAMLConfiguration is the resolver for the createSAMLConfiguration field.
|
||||
func (r *mutationResolver) CreateSAMLConfiguration(ctx context.Context, input types.CreateSAMLConfigurationInput) (*types.CreateSAMLConfigurationPayload, error) {
|
||||
user := UserFromContext(ctx)
|
||||
if user == nil {
|
||||
return nil, fmt.Errorf("user not authenticated")
|
||||
}
|
||||
|
||||
organizationID := input.OrganizationID
|
||||
tenantID := organizationID.TenantID()
|
||||
|
||||
var idpEntityID, idpSsoURL, idpCertificate string
|
||||
var idpMetadataURL *string
|
||||
|
||||
if input.IdpMetadataXML != nil && *input.IdpMetadataXML != "" {
|
||||
metadata, err := auth.ParseIdPMetadata(*input.IdpMetadataXML)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse IdP metadata XML: %w", err)
|
||||
}
|
||||
idpEntityID = metadata.EntityID
|
||||
idpSsoURL = metadata.SsoURL
|
||||
idpCertificate = metadata.Certificate
|
||||
idpMetadataURL = metadata.MetadataURL
|
||||
} else {
|
||||
if input.IdpEntityID == nil || *input.IdpEntityID == "" {
|
||||
return nil, fmt.Errorf("either idpMetadataXml or idpEntityId must be provided")
|
||||
}
|
||||
if input.IdpSsoURL == nil || *input.IdpSsoURL == "" {
|
||||
return nil, fmt.Errorf("either idpMetadataXml or idpSsoUrl must be provided")
|
||||
}
|
||||
if input.IdpCertificate == nil || *input.IdpCertificate == "" {
|
||||
return nil, fmt.Errorf("either idpMetadataXml or idpCertificate must be provided")
|
||||
}
|
||||
idpEntityID = *input.IdpEntityID
|
||||
idpSsoURL = *input.IdpSsoURL
|
||||
idpCertificate = *input.IdpCertificate
|
||||
idpMetadataURL = input.IdpMetadataURL
|
||||
}
|
||||
|
||||
attributeEmail := "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"
|
||||
if input.AttributeEmail != nil {
|
||||
attributeEmail = *input.AttributeEmail
|
||||
}
|
||||
|
||||
attributeFirstname := "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname"
|
||||
if input.AttributeFirstname != nil {
|
||||
attributeFirstname = *input.AttributeFirstname
|
||||
}
|
||||
|
||||
attributeLastname := "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname"
|
||||
if input.AttributeLastname != nil {
|
||||
attributeLastname = *input.AttributeLastname
|
||||
}
|
||||
|
||||
attributeRole := "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/role"
|
||||
if input.AttributeRole != nil {
|
||||
attributeRole = *input.AttributeRole
|
||||
}
|
||||
|
||||
defaultRole := "MEMBER"
|
||||
if input.DefaultRole != nil {
|
||||
defaultRole = *input.DefaultRole
|
||||
}
|
||||
|
||||
autoSignupEnabled := false
|
||||
if input.AutoSignupEnabled != nil {
|
||||
autoSignupEnabled = *input.AutoSignupEnabled
|
||||
}
|
||||
|
||||
config, err := r.authSvc.WithTenant(tenantID).CreateSAMLConfiguration(ctx, auth.CreateSAMLConfigurationRequest{
|
||||
OrganizationID: organizationID,
|
||||
EmailDomain: input.EmailDomain,
|
||||
EnforcementPolicy: input.EnforcementPolicy,
|
||||
IdPEntityID: idpEntityID,
|
||||
IdPSsoURL: idpSsoURL,
|
||||
IdPCertificate: idpCertificate,
|
||||
IdPMetadataURL: idpMetadataURL,
|
||||
AttributeEmail: attributeEmail,
|
||||
AttributeFirstname: attributeFirstname,
|
||||
AttributeLastname: attributeLastname,
|
||||
AttributeRole: attributeRole,
|
||||
DefaultRole: defaultRole,
|
||||
AutoSignupEnabled: autoSignupEnabled,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create SAML configuration: %w", err)
|
||||
}
|
||||
|
||||
return &types.CreateSAMLConfigurationPayload{
|
||||
SamlConfiguration: types.NewSAMLConfigurationWithURLs(
|
||||
config,
|
||||
r.samlSvc.GetEntityID(),
|
||||
r.samlSvc.GetAcsURL(),
|
||||
),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UpdateSAMLConfiguration is the resolver for the updateSAMLConfiguration field.
|
||||
func (r *mutationResolver) UpdateSAMLConfiguration(ctx context.Context, input types.UpdateSAMLConfigurationInput) (*types.UpdateSAMLConfigurationPayload, error) {
|
||||
user := UserFromContext(ctx)
|
||||
if user == nil {
|
||||
return nil, fmt.Errorf("user not authenticated")
|
||||
}
|
||||
|
||||
configID := input.ID
|
||||
tenantID := configID.TenantID()
|
||||
|
||||
updatedConfig, err := r.authSvc.WithTenant(tenantID).UpdateSAMLConfiguration(ctx, auth.UpdateSAMLConfigurationRequest{
|
||||
ID: configID,
|
||||
Enabled: input.Enabled,
|
||||
EnforcementPolicy: input.EnforcementPolicy,
|
||||
IdPEntityID: input.IdpEntityID,
|
||||
IdPSsoURL: input.IdpSsoURL,
|
||||
IdPCertificate: input.IdpCertificate,
|
||||
IdPMetadataURL: input.IdpMetadataURL,
|
||||
AttributeEmail: input.AttributeEmail,
|
||||
AttributeFirstname: input.AttributeFirstname,
|
||||
AttributeLastname: input.AttributeLastname,
|
||||
AttributeRole: input.AttributeRole,
|
||||
DefaultRole: input.DefaultRole,
|
||||
AutoSignupEnabled: input.AutoSignupEnabled,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update SAML configuration: %w", err)
|
||||
}
|
||||
|
||||
return &types.UpdateSAMLConfigurationPayload{
|
||||
SamlConfiguration: types.NewSAMLConfigurationWithURLs(
|
||||
updatedConfig,
|
||||
r.samlSvc.GetEntityID(),
|
||||
r.samlSvc.GetAcsURL(),
|
||||
),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DeleteSAMLConfiguration is the resolver for the deleteSAMLConfiguration field.
|
||||
func (r *mutationResolver) DeleteSAMLConfiguration(ctx context.Context, input types.DeleteSAMLConfigurationInput) (*types.DeleteSAMLConfigurationPayload, error) {
|
||||
user := UserFromContext(ctx)
|
||||
if user == nil {
|
||||
return nil, fmt.Errorf("user not authenticated")
|
||||
}
|
||||
|
||||
configID := input.ID
|
||||
tenantID := configID.TenantID()
|
||||
|
||||
err := r.authSvc.WithTenant(tenantID).DeleteSAMLConfiguration(ctx, configID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to delete SAML configuration: %w", err)
|
||||
}
|
||||
|
||||
return &types.DeleteSAMLConfigurationPayload{
|
||||
DeletedSAMLConfigurationID: configID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// EnableSaml is the resolver for the enableSAML field.
|
||||
func (r *mutationResolver) EnableSaml(ctx context.Context, input types.EnableSAMLInput) (*types.EnableSAMLPayload, error) {
|
||||
user := UserFromContext(ctx)
|
||||
if user == nil {
|
||||
return nil, fmt.Errorf("user not authenticated")
|
||||
}
|
||||
|
||||
configID := input.ID
|
||||
tenantID := configID.TenantID()
|
||||
|
||||
enabledConfig, err := r.authSvc.WithTenant(tenantID).EnableSAMLConfiguration(ctx, configID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to enable SAML: %w", err)
|
||||
}
|
||||
|
||||
return &types.EnableSAMLPayload{
|
||||
SamlConfiguration: types.NewSAMLConfigurationWithURLs(
|
||||
enabledConfig,
|
||||
r.samlSvc.GetEntityID(),
|
||||
r.samlSvc.GetAcsURL(),
|
||||
),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DisableSaml is the resolver for the disableSAML field.
|
||||
func (r *mutationResolver) DisableSaml(ctx context.Context, input types.DisableSAMLInput) (*types.DisableSAMLPayload, error) {
|
||||
user := UserFromContext(ctx)
|
||||
if user == nil {
|
||||
return nil, fmt.Errorf("user not authenticated")
|
||||
}
|
||||
|
||||
configID := input.ID
|
||||
tenantID := configID.TenantID()
|
||||
|
||||
disabledConfig, err := r.authSvc.WithTenant(tenantID).DisableSAMLConfiguration(ctx, configID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to disable SAML: %w", err)
|
||||
}
|
||||
|
||||
return &types.DisableSAMLPayload{
|
||||
SamlConfiguration: types.NewSAMLConfigurationWithURLs(
|
||||
disabledConfig,
|
||||
r.samlSvc.GetEntityID(),
|
||||
r.samlSvc.GetAcsURL(),
|
||||
),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Organization is the resolver for the organization field.
|
||||
func (r *nonconformityResolver) Organization(ctx context.Context, obj *types.Nonconformity) (*types.Organization, error) {
|
||||
prb := r.ProboService(ctx, obj.ID.TenantID())
|
||||
@@ -4282,6 +4554,27 @@ func (r *organizationResolver) CustomDomain(ctx context.Context, obj *types.Orga
|
||||
return types.NewCustomDomain(domain, r.customDomainCname), nil
|
||||
}
|
||||
|
||||
// SamlConfigurations is the resolver for the samlConfigurations field.
|
||||
func (r *organizationResolver) SamlConfigurations(ctx context.Context, obj *types.Organization) ([]*types.SAMLConfiguration, error) {
|
||||
tenantID := obj.ID.TenantID()
|
||||
|
||||
configs, err := r.authSvc.WithTenant(tenantID).GetSAMLConfigurationsByOrganizationID(ctx, obj.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load SAML configurations: %w", err)
|
||||
}
|
||||
|
||||
result := make([]*types.SAMLConfiguration, len(configs))
|
||||
for i, config := range configs {
|
||||
result[i] = types.NewSAMLConfigurationWithURLs(
|
||||
config,
|
||||
r.samlSvc.GetEntityID(),
|
||||
r.samlSvc.GetAcsURL(),
|
||||
)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// TotalCount is the resolver for the totalCount field.
|
||||
func (r *peopleConnectionResolver) TotalCount(ctx context.Context, obj *types.PeopleConnection) (int, error) {
|
||||
prb := r.ProboService(ctx, obj.ParentID.TenantID())
|
||||
@@ -4727,6 +5020,41 @@ func (r *riskConnectionResolver) TotalCount(ctx context.Context, obj *types.Risk
|
||||
panic(fmt.Errorf("unsupported resolver: %T", obj.Resolver))
|
||||
}
|
||||
|
||||
// Organization is the resolver for the organization field.
|
||||
func (r *sAMLConfigurationResolver) Organization(ctx context.Context, obj *types.SAMLConfiguration) (*types.Organization, error) {
|
||||
tenantID := obj.ID.TenantID()
|
||||
prb := r.ProboService(ctx, tenantID)
|
||||
|
||||
config, err := r.authSvc.WithTenant(tenantID).GetSAMLConfigurationByID(ctx, obj.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load SAML configuration: %w", err)
|
||||
}
|
||||
|
||||
org, err := prb.Organizations.Get(ctx, config.OrganizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load organization: %w", err)
|
||||
}
|
||||
|
||||
return types.NewOrganization(org), nil
|
||||
}
|
||||
|
||||
// SpMetadataURL is the resolver for the spMetadataUrl field.
|
||||
// Returns global Entity ID (same as spEntityId since metadata URL no longer needs config parameter)
|
||||
func (r *sAMLConfigurationResolver) SpMetadataURL(ctx context.Context, obj *types.SAMLConfiguration) (string, error) {
|
||||
return r.samlSvc.GetEntityID(), nil
|
||||
}
|
||||
|
||||
// TestLoginURL is the resolver for the testLoginUrl field.
|
||||
func (r *sAMLConfigurationResolver) TestLoginURL(ctx context.Context, obj *types.SAMLConfiguration) (string, error) {
|
||||
entityID := r.samlSvc.GetEntityID()
|
||||
parts := strings.Split(entityID, "/auth/saml/metadata")
|
||||
if len(parts) != 2 {
|
||||
return "", fmt.Errorf("invalid entity ID format")
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s/auth/saml/login/%s", parts[0], obj.ID), nil
|
||||
}
|
||||
|
||||
// Organization is the resolver for the organization field.
|
||||
func (r *snapshotResolver) Organization(ctx context.Context, obj *types.Snapshot) (*types.Organization, error) {
|
||||
prb := r.ProboService(ctx, obj.ID.TenantID())
|
||||
@@ -5520,6 +5848,8 @@ func (r *viewerResolver) Organizations(ctx context.Context, obj *types.Viewer, f
|
||||
panic(fmt.Errorf("failed to list organizations for user: %w", err))
|
||||
}
|
||||
|
||||
// Show all organizations the user is a member of
|
||||
// Authentication requirements will be enforced when switching to an organization
|
||||
page := page.NewPage(organizations, cursor)
|
||||
|
||||
return types.NewOrganizationConnection(page), nil
|
||||
@@ -5649,6 +5979,9 @@ func (r *Resolver) MeasureConnection() schema.MeasureConnectionResolver {
|
||||
return &measureConnectionResolver{r}
|
||||
}
|
||||
|
||||
// Membership returns schema.MembershipResolver implementation.
|
||||
func (r *Resolver) Membership() schema.MembershipResolver { return &membershipResolver{r} }
|
||||
|
||||
// MembershipConnection returns schema.MembershipConnectionResolver implementation.
|
||||
func (r *Resolver) MembershipConnection() schema.MembershipConnectionResolver {
|
||||
return &membershipConnectionResolver{r}
|
||||
@@ -5703,6 +6036,11 @@ func (r *Resolver) Risk() schema.RiskResolver { return &riskResolver{r} }
|
||||
// RiskConnection returns schema.RiskConnectionResolver implementation.
|
||||
func (r *Resolver) RiskConnection() schema.RiskConnectionResolver { return &riskConnectionResolver{r} }
|
||||
|
||||
// SAMLConfiguration returns schema.SAMLConfigurationResolver implementation.
|
||||
func (r *Resolver) SAMLConfiguration() schema.SAMLConfigurationResolver {
|
||||
return &sAMLConfigurationResolver{r}
|
||||
}
|
||||
|
||||
// Snapshot returns schema.SnapshotResolver implementation.
|
||||
func (r *Resolver) Snapshot() schema.SnapshotResolver { return &snapshotResolver{r} }
|
||||
|
||||
@@ -5818,6 +6156,7 @@ type invitationResolver struct{ *Resolver }
|
||||
type invitationConnectionResolver struct{ *Resolver }
|
||||
type measureResolver struct{ *Resolver }
|
||||
type measureConnectionResolver struct{ *Resolver }
|
||||
type membershipResolver struct{ *Resolver }
|
||||
type membershipConnectionResolver struct{ *Resolver }
|
||||
type mutationResolver struct{ *Resolver }
|
||||
type nonconformityResolver struct{ *Resolver }
|
||||
@@ -5832,6 +6171,7 @@ type queryResolver struct{ *Resolver }
|
||||
type reportResolver struct{ *Resolver }
|
||||
type riskResolver struct{ *Resolver }
|
||||
type riskConnectionResolver struct{ *Resolver }
|
||||
type sAMLConfigurationResolver struct{ *Resolver }
|
||||
type snapshotResolver struct{ *Resolver }
|
||||
type snapshotConnectionResolver struct{ *Resolver }
|
||||
type taskResolver struct{ *Resolver }
|
||||
|
||||
Reference in New Issue
Block a user