Move trust GraphQL API under complianceportal v1
Relocate the public trust center GraphQL surface, OAuth handlers, and SPA serving into the compliance portal API package and remove the legacy trust v1 server tree. Signed-off-by: Bryan Frimin <bryan@probo.com>
This commit is contained in:
35
pkg/server/api/complianceportal/oauth.go
Normal file
35
pkg/server/api/complianceportal/oauth.go
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.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 complianceportal
|
||||
|
||||
import (
|
||||
portal "go.probo.inc/probo/pkg/complianceportal"
|
||||
)
|
||||
|
||||
const (
|
||||
VisitorOAuthScope = portal.VisitorOAuthScope
|
||||
GraphQLPath = "/graphql"
|
||||
CIMDMetadataPath = portal.CIMDMetadataPath
|
||||
OAuthInitiatePath = "/initiate"
|
||||
OAuthCallbackPath = portal.OAuthCallbackPath
|
||||
)
|
||||
|
||||
func CIMDClientIDURL(portalBaseURL string) (string, error) {
|
||||
return portal.CIMDClientIDURL(portalBaseURL)
|
||||
}
|
||||
|
||||
func OAuthCallbackURL(portalBaseURL string) (string, error) {
|
||||
return portal.OAuthCallbackURL(portalBaseURL)
|
||||
}
|
||||
61
pkg/server/api/complianceportal/session.go
Normal file
61
pkg/server/api/complianceportal/session.go
Normal file
@@ -0,0 +1,61 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.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 complianceportal
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/securecookie"
|
||||
"go.probo.inc/probo/pkg/server/api/authn"
|
||||
)
|
||||
|
||||
func TrustedRequestHost(r *http.Request) (string, bool) {
|
||||
if r.TLS != nil && r.TLS.ServerName != "" {
|
||||
return coredata.NormalizeBoundHost(r.TLS.ServerName), true
|
||||
}
|
||||
|
||||
if r.Host == "" {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return coredata.NormalizeBoundHost(r.Host), true
|
||||
}
|
||||
|
||||
func NewSessionHostMiddleware(cookieConfig securecookie.Config) func(next http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
session := authn.SessionFromContext(ctx)
|
||||
if session == nil {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
host, ok := TrustedRequestHost(r)
|
||||
if ok && session.Data.MatchesBoundHost(host) {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
securecookie.Clear(w, cookieConfig)
|
||||
ctx = authn.ContextWithSession(ctx, nil)
|
||||
ctx = authn.ContextWithIdentity(ctx, nil)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
135
pkg/server/api/complianceportal/session_test.go
Normal file
135
pkg/server/api/complianceportal/session_test.go
Normal file
@@ -0,0 +1,135 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.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 complianceportal
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/securecookie"
|
||||
"go.probo.inc/probo/pkg/server/api/authn"
|
||||
)
|
||||
|
||||
func TestTrustedRequestHost_PrefersTLSServerName(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "https://evil.example.com/graphql", nil)
|
||||
req.Host = "evil.example.com"
|
||||
req.TLS = &tls.ConnectionState{ServerName: "portal.example.com"}
|
||||
|
||||
host, ok := TrustedRequestHost(req)
|
||||
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, "portal.example.com", host)
|
||||
}
|
||||
|
||||
func TestSessionHostMiddleware_RejectsMismatchedHost(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var authenticated bool
|
||||
|
||||
handler := NewSessionHostMiddleware(securecookie.Config{Name: "ssid"})(
|
||||
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
authenticated = authn.IdentityFromContext(r.Context()) != nil
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}),
|
||||
)
|
||||
|
||||
identity := &coredata.Identity{ID: gid.New(gid.NilTenant, coredata.IdentityEntityType)}
|
||||
session := &coredata.Session{
|
||||
ID: identity.ID,
|
||||
Data: coredata.SessionDataForHost("portal-a.example.com"),
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/graphql", nil)
|
||||
req.Host = "portal-b.example.com"
|
||||
req.TLS = &tls.ConnectionState{ServerName: "portal-b.example.com"}
|
||||
req = req.WithContext(authn.ContextWithIdentity(req.Context(), identity))
|
||||
req = req.WithContext(authn.ContextWithSession(req.Context(), session))
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
assert.False(t, authenticated)
|
||||
}
|
||||
|
||||
func TestSessionHostMiddleware_AllowsMatchingTLSHost(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var authenticated bool
|
||||
|
||||
handler := NewSessionHostMiddleware(securecookie.Config{Name: "ssid"})(
|
||||
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
authenticated = authn.IdentityFromContext(r.Context()) != nil
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}),
|
||||
)
|
||||
|
||||
identity := &coredata.Identity{ID: gid.New(gid.NilTenant, coredata.IdentityEntityType)}
|
||||
session := &coredata.Session{
|
||||
ID: identity.ID,
|
||||
Data: coredata.SessionDataForHost("portal.example.com"),
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/graphql", nil)
|
||||
req.Host = "evil.example.com"
|
||||
req.TLS = &tls.ConnectionState{ServerName: "portal.example.com"}
|
||||
req = req.WithContext(authn.ContextWithIdentity(req.Context(), identity))
|
||||
req = req.WithContext(authn.ContextWithSession(req.Context(), session))
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
assert.True(t, authenticated)
|
||||
}
|
||||
|
||||
func TestSessionHostMiddleware_RejectsSpoofedHostHeader(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var authenticated bool
|
||||
|
||||
handler := NewSessionHostMiddleware(securecookie.Config{Name: "ssid"})(
|
||||
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
authenticated = authn.IdentityFromContext(r.Context()) != nil
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}),
|
||||
)
|
||||
|
||||
identity := &coredata.Identity{ID: gid.New(gid.NilTenant, coredata.IdentityEntityType)}
|
||||
session := &coredata.Session{
|
||||
ID: identity.ID,
|
||||
Data: coredata.SessionDataForHost("portal.example.com"),
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/graphql", nil)
|
||||
req.Host = "portal.example.com"
|
||||
req.TLS = &tls.ConnectionState{ServerName: "other.example.com"}
|
||||
req = req.WithContext(authn.ContextWithIdentity(req.Context(), identity))
|
||||
req = req.WithContext(authn.ContextWithSession(req.Context(), session))
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
assert.False(t, authenticated)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package trust_v1
|
||||
package complianceportal_v1
|
||||
|
||||
// This file will be automatically regenerated based on the schema, any resolver
|
||||
// implementations
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
"go.probo.inc/probo/pkg/saferedirect"
|
||||
"go.probo.inc/probo/pkg/server/api/authn"
|
||||
"go.probo.inc/probo/pkg/server/api/complianceportal"
|
||||
"go.probo.inc/probo/pkg/server/api/trust/v1/types"
|
||||
"go.probo.inc/probo/pkg/server/api/complianceportal/v1/types"
|
||||
"go.probo.inc/probo/pkg/server/gqlutils"
|
||||
)
|
||||
|
||||
@@ -123,6 +123,22 @@ func (r *mutationResolver) VerifyMagicLink(ctx context.Context, input types.Veri
|
||||
}
|
||||
}
|
||||
|
||||
req := gqlutils.HTTPRequestFromContext(ctx)
|
||||
if req == nil {
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
host, ok := complianceportal.TrustedRequestHost(req)
|
||||
if !ok {
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
session.Data = coredata.SessionDataForHost(host)
|
||||
if err := r.iam.SessionService.UpdateSessionData(ctx, session.ID, session.Data); err != nil {
|
||||
r.logger.ErrorCtx(ctx, "cannot bind session to host", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
trustCenter := complianceportal.CompliancePageFromContext(ctx)
|
||||
|
||||
if _, err := r.trust.ProvisionPortalMember(ctx, trustCenter.ID, identity.ID); err != nil {
|
||||
@@ -1,4 +1,4 @@
|
||||
package trust_v1
|
||||
package complianceportal_v1
|
||||
|
||||
// This file will be automatically regenerated based on the schema, any resolver
|
||||
// implementations
|
||||
@@ -17,8 +17,8 @@ import (
|
||||
"go.probo.inc/probo/pkg/page"
|
||||
"go.probo.inc/probo/pkg/server/api/authn"
|
||||
"go.probo.inc/probo/pkg/server/api/complianceportal"
|
||||
"go.probo.inc/probo/pkg/server/api/trust/v1/schema"
|
||||
"go.probo.inc/probo/pkg/server/api/trust/v1/types"
|
||||
"go.probo.inc/probo/pkg/server/api/complianceportal/v1/schema"
|
||||
"go.probo.inc/probo/pkg/server/api/complianceportal/v1/types"
|
||||
"go.probo.inc/probo/pkg/server/gqlutils"
|
||||
)
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
package trust_v1
|
||||
package complianceportal_v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -27,7 +27,7 @@ import (
|
||||
"go.gearno.de/kit/log"
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/server/api/trust/v1/types"
|
||||
"go.probo.inc/probo/pkg/server/api/complianceportal/v1/types"
|
||||
"go.probo.inc/probo/pkg/server/gqlutils"
|
||||
)
|
||||
|
||||
@@ -14,7 +14,7 @@ model:
|
||||
resolver:
|
||||
layout: "follow-schema"
|
||||
dir: "."
|
||||
package: "trust_v1"
|
||||
package: "complianceportal_v1"
|
||||
filename_template: "{name}_resolvers.go"
|
||||
|
||||
autobind: []
|
||||
@@ -29,10 +29,7 @@ enum MailingListSubscriberStatus
|
||||
)
|
||||
}
|
||||
|
||||
type MailingListSubscriber implements Node
|
||||
@goModel(
|
||||
model: "go.probo.inc/probo/pkg/server/api/trust/v1/types.MailingListSubscriber"
|
||||
) {
|
||||
type MailingListSubscriber implements Node {
|
||||
id: ID!
|
||||
fullName: String!
|
||||
email: EmailAddr!
|
||||
@@ -172,26 +172,20 @@ type AuditEdge @nda {
|
||||
node: Audit!
|
||||
}
|
||||
|
||||
type ComplianceFramework implements Node
|
||||
@goModel(
|
||||
model: "go.probo.inc/probo/pkg/server/api/trust/v1/types.ComplianceFramework"
|
||||
) {
|
||||
type ComplianceFramework implements Node {
|
||||
id: ID!
|
||||
framework: Framework! @goField(forceResolver: true)
|
||||
}
|
||||
|
||||
type ComplianceFrameworkConnection
|
||||
@goModel(
|
||||
model: "go.probo.inc/probo/pkg/server/api/trust/v1/types.ComplianceFrameworkConnection"
|
||||
model: "go.probo.inc/probo/pkg/server/api/complianceportal/v1/types.ComplianceFrameworkConnection"
|
||||
) {
|
||||
edges: [ComplianceFrameworkEdge!]!
|
||||
pageInfo: PageInfo!
|
||||
}
|
||||
|
||||
type ComplianceFrameworkEdge
|
||||
@goModel(
|
||||
model: "go.probo.inc/probo/pkg/server/api/trust/v1/types.ComplianceFrameworkEdge"
|
||||
) {
|
||||
type ComplianceFrameworkEdge {
|
||||
cursor: CursorKey!
|
||||
node: ComplianceFramework!
|
||||
}
|
||||
@@ -285,7 +279,7 @@ input SubprocessorFilter {
|
||||
|
||||
type SubprocessorConnection
|
||||
@goModel(
|
||||
model: "go.probo.inc/probo/pkg/server/api/trust/v1/types.SubprocessorConnection"
|
||||
model: "go.probo.inc/probo/pkg/server/api/complianceportal/v1/types.SubprocessorConnection"
|
||||
) @nda {
|
||||
edges: [SubprocessorEdge!]!
|
||||
pageInfo: PageInfo!
|
||||
@@ -18,7 +18,7 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
package trust_v1
|
||||
package complianceportal_v1
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
@@ -33,7 +33,7 @@ import (
|
||||
"go.probo.inc/probo/pkg/resourcealias"
|
||||
"go.probo.inc/probo/pkg/securecookie"
|
||||
"go.probo.inc/probo/pkg/server/api/authn"
|
||||
"go.probo.inc/probo/pkg/server/api/trust/v1/schema"
|
||||
"go.probo.inc/probo/pkg/server/api/complianceportal/v1/schema"
|
||||
"go.probo.inc/probo/pkg/server/gqlutils"
|
||||
"go.probo.inc/probo/pkg/server/gqlutils/directives/authentication"
|
||||
"go.probo.inc/probo/pkg/server/gqlutils/directives/session"
|
||||
@@ -1,4 +1,4 @@
|
||||
package trust_v1
|
||||
package complianceportal_v1
|
||||
|
||||
// This file will be automatically regenerated based on the schema, any resolver
|
||||
// implementations
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"go.probo.inc/probo/pkg/mailman"
|
||||
"go.probo.inc/probo/pkg/server/api/authn"
|
||||
"go.probo.inc/probo/pkg/server/api/complianceportal"
|
||||
"go.probo.inc/probo/pkg/server/api/trust/v1/types"
|
||||
"go.probo.inc/probo/pkg/server/api/complianceportal/v1/types"
|
||||
"go.probo.inc/probo/pkg/server/gqlutils"
|
||||
"go.probo.inc/probo/pkg/validator"
|
||||
)
|
||||
163
pkg/server/api/complianceportal/v1/mux.go
Normal file
163
pkg/server/api/complianceportal/v1/mux.go
Normal file
@@ -0,0 +1,163 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.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 complianceportal_v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"go.gearno.de/kit/httpserver"
|
||||
"go.gearno.de/kit/log"
|
||||
"go.gearno.de/x/ref"
|
||||
"go.probo.inc/probo/pkg/baseurl"
|
||||
visitor "go.probo.inc/probo/pkg/complianceportal/visitor"
|
||||
"go.probo.inc/probo/pkg/esign"
|
||||
"go.probo.inc/probo/pkg/filemanager"
|
||||
"go.probo.inc/probo/pkg/iam"
|
||||
"go.probo.inc/probo/pkg/mailman"
|
||||
"go.probo.inc/probo/pkg/resourcealias"
|
||||
"go.probo.inc/probo/pkg/securecookie"
|
||||
"go.probo.inc/probo/pkg/server"
|
||||
"go.probo.inc/probo/pkg/server/api/authn"
|
||||
"go.probo.inc/probo/pkg/server/api/complianceportal"
|
||||
"go.probo.inc/probo/pkg/server/gqlutils"
|
||||
)
|
||||
|
||||
type MuxConfig struct {
|
||||
BaseURL *baseurl.BaseURL
|
||||
ExtraHeaderFields map[string]string
|
||||
Logger *log.Logger
|
||||
IAM *iam.Service
|
||||
Visitor *visitor.Service
|
||||
ResourceAlias *resourcealias.Service
|
||||
File *filemanager.Service
|
||||
ESign *esign.Service
|
||||
Mailman *mailman.Service
|
||||
Cookie securecookie.Config
|
||||
TokenSecret string
|
||||
GraphQLLimits gqlutils.Limits
|
||||
}
|
||||
|
||||
func NewMux(cfg MuxConfig) (http.Handler, error) {
|
||||
webServer, err := NewServer(compliancePageHeadData(cfg.BaseURL))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Use(complianceportal.NewSNIMiddleware(cfg.Visitor))
|
||||
r.Use(server.NewSecurityHeadersMiddleware(cfg.ExtraHeaderFields))
|
||||
|
||||
markdownHandler := complianceportal.NewHandler(cfg.Visitor)
|
||||
|
||||
r.Get("/llms.txt", markdownHandler.HandleLLMsTxt)
|
||||
r.Get("/robots.txt", markdownHandler.HandleRobotsTxt)
|
||||
r.Get("/sitemap.xml", markdownHandler.HandleSitemap)
|
||||
|
||||
allowedHost := func(ctx context.Context, host string) bool {
|
||||
_, err := cfg.Visitor.GetPortalByDomainName(ctx, host)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
oauthInitiateHandler := NewOAuthInitiateHandler(
|
||||
cfg.BaseURL,
|
||||
cfg.Visitor,
|
||||
allowedHost,
|
||||
cfg.Logger,
|
||||
)
|
||||
oauthCallbackHandler := NewOAuthCallbackHandler(
|
||||
cfg.IAM,
|
||||
cfg.Visitor,
|
||||
cfg.Cookie,
|
||||
allowedHost,
|
||||
cfg.Logger,
|
||||
)
|
||||
|
||||
graphqlHandler := NewGraphQLHandler(
|
||||
cfg.IAM,
|
||||
cfg.Visitor,
|
||||
cfg.ResourceAlias,
|
||||
cfg.File,
|
||||
cfg.ESign,
|
||||
cfg.Mailman,
|
||||
cfg.Logger,
|
||||
cfg.BaseURL,
|
||||
cfg.Cookie,
|
||||
cfg.TokenSecret,
|
||||
cfg.GraphQLLimits,
|
||||
)
|
||||
|
||||
r.Group(
|
||||
func(r chi.Router) {
|
||||
r.Use(complianceportal.NewCompliancePagePresenceMiddleware())
|
||||
|
||||
r.Method(http.MethodGet, complianceportal.CIMDMetadataPath, NewOAuthClientMetadataHandler())
|
||||
r.Method(http.MethodGet, complianceportal.OAuthInitiatePath, oauthInitiateHandler)
|
||||
r.Method(http.MethodGet, complianceportal.OAuthCallbackPath, oauthCallbackHandler)
|
||||
|
||||
r.Group(
|
||||
func(r chi.Router) {
|
||||
r.Use(authn.NewSessionMiddleware(cfg.IAM, cfg.Cookie))
|
||||
r.Use(complianceportal.NewSessionHostMiddleware(cfg.Cookie))
|
||||
r.Use(complianceportal.NewMemberProvisioningMiddleware(cfg.Visitor, cfg.Logger))
|
||||
r.Handle(complianceportal.GraphQLPath, graphqlHandler)
|
||||
},
|
||||
)
|
||||
|
||||
r.Handle("/*", webServer)
|
||||
r.NotFound(handleCustomDomain404)
|
||||
},
|
||||
)
|
||||
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func handleCustomDomain404(w http.ResponseWriter, r *http.Request) {
|
||||
httpserver.RenderError(w, http.StatusNotFound, errors.New("not found"))
|
||||
}
|
||||
|
||||
func compliancePageHeadData(baseURL *baseurl.BaseURL) HeadDataFunc {
|
||||
return func(r *http.Request) HeadData {
|
||||
tc := complianceportal.CompliancePageFromContext(r.Context())
|
||||
if tc == nil {
|
||||
return HeadData{Title: "Compliance Page"}
|
||||
}
|
||||
|
||||
compliancePageBaseURL := complianceportal.CompliancePageBaseURLFromContext(r.Context())
|
||||
|
||||
description := tc.Title + " Compliance Page"
|
||||
if tc.Description != nil && *tc.Description != "" {
|
||||
description = *tc.Description
|
||||
}
|
||||
|
||||
headData := HeadData{
|
||||
Title: tc.Title,
|
||||
Description: description,
|
||||
OGURL: ref.UnrefOrZero(compliancePageBaseURL),
|
||||
}
|
||||
|
||||
if tc.LogoFileID != nil {
|
||||
faviconURL, err := baseURL.WithPath("/api/files/v1/public/" + tc.LogoFileID.String()).String()
|
||||
if err == nil {
|
||||
headData.FaviconURL = faviconURL
|
||||
}
|
||||
}
|
||||
|
||||
return headData
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
package trust_v1
|
||||
package complianceportal_v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package trust_v1
|
||||
package complianceportal_v1
|
||||
|
||||
// This file will be automatically regenerated based on the schema, any resolver
|
||||
// implementations
|
||||
@@ -15,16 +15,17 @@ import (
|
||||
"go.probo.inc/probo/pkg/esign"
|
||||
"go.probo.inc/probo/pkg/server/api/authn"
|
||||
"go.probo.inc/probo/pkg/server/api/complianceportal"
|
||||
"go.probo.inc/probo/pkg/server/api/trust/v1/schema"
|
||||
"go.probo.inc/probo/pkg/server/api/trust/v1/types"
|
||||
"go.probo.inc/probo/pkg/server/api/complianceportal/v1/schema"
|
||||
"go.probo.inc/probo/pkg/server/api/complianceportal/v1/types"
|
||||
"go.probo.inc/probo/pkg/server/gqlutils"
|
||||
)
|
||||
|
||||
// AcceptElectronicSignature is the resolver for the acceptElectronicSignature field.
|
||||
func (r *mutationResolver) AcceptElectronicSignature(ctx context.Context, input types.AcceptElectronicSignatureInput) (*types.AcceptElectronicSignaturePayload, error) {
|
||||
var (
|
||||
identity = authn.IdentityFromContext(ctx)
|
||||
httpReq = gqlutils.HTTPRequestFromContext(ctx)
|
||||
identity = authn.IdentityFromContext(ctx)
|
||||
httpReq = gqlutils.HTTPRequestFromContext(ctx)
|
||||
trustCenter = complianceportal.CompliancePageFromContext(ctx)
|
||||
)
|
||||
|
||||
signerIP, _, _ := net.SplitHostPort(httpReq.RemoteAddr)
|
||||
@@ -32,8 +33,11 @@ func (r *mutationResolver) AcceptElectronicSignature(ctx context.Context, input
|
||||
signerIP = httpReq.RemoteAddr
|
||||
}
|
||||
|
||||
scope := coredata.NewScopeFromObjectID(trustCenter.ID)
|
||||
|
||||
signature, err := r.esign.AcceptSignature(
|
||||
ctx,
|
||||
scope,
|
||||
&esign.AcceptSignatureRequest{
|
||||
SignatureID: input.SignatureID,
|
||||
SignerFullName: identity.FullName,
|
||||
@@ -55,8 +59,9 @@ func (r *mutationResolver) AcceptElectronicSignature(ctx context.Context, input
|
||||
// RecordSigningEvent is the resolver for the recordSigningEvent field.
|
||||
func (r *mutationResolver) RecordSigningEvent(ctx context.Context, input types.RecordSigningEventInput) (*types.RecordSigningEventPayload, error) {
|
||||
var (
|
||||
identity = authn.IdentityFromContext(ctx)
|
||||
httpReq = gqlutils.HTTPRequestFromContext(ctx)
|
||||
identity = authn.IdentityFromContext(ctx)
|
||||
httpReq = gqlutils.HTTPRequestFromContext(ctx)
|
||||
trustCenter = complianceportal.CompliancePageFromContext(ctx)
|
||||
)
|
||||
|
||||
actorIP, _, _ := net.SplitHostPort(httpReq.RemoteAddr)
|
||||
@@ -64,8 +69,11 @@ func (r *mutationResolver) RecordSigningEvent(ctx context.Context, input types.R
|
||||
actorIP = httpReq.RemoteAddr
|
||||
}
|
||||
|
||||
scope := coredata.NewScopeFromObjectID(trustCenter.ID)
|
||||
|
||||
if err := r.esign.RecordEvent(
|
||||
ctx,
|
||||
scope,
|
||||
&esign.RecordEventRequest{
|
||||
SignatureID: input.SignatureID,
|
||||
EventType: input.EventType,
|
||||
@@ -132,7 +140,7 @@ func (r *nonDisclosureAgreementResolver) ViewerSignature(ctx context.Context, ob
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
sig, err := r.esign.GetSignatureByID(ctx, *access.ElectronicSignatureID)
|
||||
sig, err := r.esign.GetSignatureByID(ctx, scope, *access.ElectronicSignatureID)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
173
pkg/server/api/complianceportal/v1/oauth_callback_handler.go
Normal file
173
pkg/server/api/complianceportal/v1/oauth_callback_handler.go
Normal file
@@ -0,0 +1,173 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.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 complianceportal_v1
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"go.gearno.de/kit/httpserver"
|
||||
"go.gearno.de/kit/log"
|
||||
"go.probo.inc/probo/pkg/complianceportal/visitor"
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/iam"
|
||||
"go.probo.inc/probo/pkg/iam/oauth2"
|
||||
"go.probo.inc/probo/pkg/saferedirect"
|
||||
"go.probo.inc/probo/pkg/securecookie"
|
||||
"go.probo.inc/probo/pkg/server/api/authn"
|
||||
"go.probo.inc/probo/pkg/server/api/complianceportal"
|
||||
)
|
||||
|
||||
var (
|
||||
errNotFound = errors.New("not found")
|
||||
errInvalidContinueURL = errors.New("invalid continue URL")
|
||||
errInternal = errors.New("internal server error")
|
||||
errInvalidOAuthRequest = errors.New("invalid oauth request")
|
||||
)
|
||||
|
||||
type OAuthCallbackHandler struct {
|
||||
iam *iam.Service
|
||||
visitor *visitor.Service
|
||||
sessionCookie *authn.Cookie
|
||||
safeRedirect *saferedirect.SafeRedirect
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
func NewOAuthCallbackHandler(
|
||||
iamSvc *iam.Service,
|
||||
visitorSvc *visitor.Service,
|
||||
cookieConfig securecookie.Config,
|
||||
allowedHost saferedirect.AllowedHostFunc,
|
||||
logger *log.Logger,
|
||||
) *OAuthCallbackHandler {
|
||||
return &OAuthCallbackHandler{
|
||||
iam: iamSvc,
|
||||
visitor: visitorSvc,
|
||||
sessionCookie: authn.NewCookie(&cookieConfig),
|
||||
safeRedirect: saferedirect.New(allowedHost),
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *OAuthCallbackHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
if oauthErr := r.URL.Query().Get("error"); oauthErr != "" {
|
||||
h.logger.WarnCtx(
|
||||
ctx,
|
||||
"oauth callback returned error",
|
||||
log.String("error", oauthErr),
|
||||
log.String("error_description", r.URL.Query().Get("error_description")),
|
||||
)
|
||||
httpserver.RenderError(w, http.StatusBadRequest, errInvalidOAuthRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
code := r.URL.Query().Get("code")
|
||||
stateToken := r.URL.Query().Get("state")
|
||||
if code == "" || stateToken == "" {
|
||||
httpserver.RenderError(w, http.StatusBadRequest, errInvalidOAuthRequest)
|
||||
return
|
||||
}
|
||||
|
||||
state, err := h.visitor.ConsumeOAuthState(ctx, stateToken)
|
||||
if err != nil {
|
||||
h.logger.WarnCtx(ctx, "invalid oauth state", log.Error(err))
|
||||
httpserver.RenderError(w, http.StatusBadRequest, errInvalidOAuthRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
portal := complianceportal.CompliancePageFromContext(ctx)
|
||||
portalBaseURL := complianceportal.CompliancePageBaseURLFromContext(ctx)
|
||||
if portal == nil || portalBaseURL == nil {
|
||||
httpserver.RenderError(w, http.StatusNotFound, errNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
clientID, err := complianceportal.CIMDClientIDURL(*portalBaseURL)
|
||||
if err != nil {
|
||||
h.logger.ErrorCtx(ctx, "cannot build cimd client_id", log.Error(err))
|
||||
httpserver.RenderError(w, http.StatusInternalServerError, errInternal)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
redirectURI, err := complianceportal.OAuthCallbackURL(*portalBaseURL)
|
||||
if err != nil {
|
||||
h.logger.ErrorCtx(ctx, "cannot build oauth redirect_uri", log.Error(err))
|
||||
httpserver.RenderError(w, http.StatusInternalServerError, errInternal)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
tokenResult, err := h.iam.OAuth2ServerService.ExchangeAuthorizationCode(
|
||||
ctx,
|
||||
clientID,
|
||||
code,
|
||||
redirectURI,
|
||||
state.CodeVerifier,
|
||||
)
|
||||
if err != nil {
|
||||
h.logger.WarnCtx(ctx, "cannot exchange authorization code", log.Error(err))
|
||||
httpserver.RenderError(w, http.StatusBadRequest, errInvalidOAuthRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
identityID, err := oauth2.ParseIDTokenIdentity(tokenResult.IDToken, state.Nonce)
|
||||
if err != nil {
|
||||
h.logger.WarnCtx(ctx, "cannot validate id token", log.Error(err))
|
||||
httpserver.RenderError(w, http.StatusBadRequest, errInvalidOAuthRequest)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
host, ok := complianceportal.TrustedRequestHost(r)
|
||||
if !ok {
|
||||
httpserver.RenderError(w, http.StatusBadRequest, errInvalidOAuthRequest)
|
||||
return
|
||||
}
|
||||
|
||||
session, err := h.iam.AuthService.OpenRootSession(
|
||||
ctx,
|
||||
identityID,
|
||||
coredata.AuthMethodOIDC,
|
||||
coredata.SessionDataForHost(host),
|
||||
)
|
||||
if err != nil {
|
||||
h.logger.ErrorCtx(ctx, "cannot open session", log.Error(err))
|
||||
httpserver.RenderError(w, http.StatusInternalServerError, errInternal)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := h.visitor.ProvisionPortalMember(ctx, portal.ID, identityID); err != nil {
|
||||
h.logger.ErrorCtx(ctx, "cannot provision portal member", log.Error(err))
|
||||
httpserver.RenderError(w, http.StatusInternalServerError, errInternal)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
h.sessionCookie.Set(w, session)
|
||||
|
||||
continueURL := state.ContinueURL
|
||||
if continueURL == "" {
|
||||
continueURL = "/"
|
||||
}
|
||||
|
||||
h.safeRedirect.Redirect(w, r, continueURL, "/", http.StatusFound)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.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 complianceportal_v1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"go.gearno.de/kit/httpserver"
|
||||
portal "go.probo.inc/probo/pkg/complianceportal"
|
||||
"go.probo.inc/probo/pkg/server/api/complianceportal"
|
||||
)
|
||||
|
||||
type oauthClientMetadataHandler struct{}
|
||||
|
||||
func NewOAuthClientMetadataHandler() http.Handler {
|
||||
return &oauthClientMetadataHandler{}
|
||||
}
|
||||
|
||||
func (h *oauthClientMetadataHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
compliancePage := complianceportal.CompliancePageFromContext(r.Context())
|
||||
baseURL := complianceportal.CompliancePageBaseURLFromContext(r.Context())
|
||||
|
||||
if compliancePage == nil || baseURL == nil {
|
||||
httpserver.RenderError(w, http.StatusNotFound, errNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
doc, err := portal.BuildClientMetadataDocument(compliancePage, *baseURL)
|
||||
if err != nil {
|
||||
httpserver.RenderError(w, http.StatusInternalServerError, errInternal)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Cache-Control", "public, max-age=300")
|
||||
_ = json.NewEncoder(w).Encode(doc)
|
||||
}
|
||||
116
pkg/server/api/complianceportal/v1/oauth_initiate_handler.go
Normal file
116
pkg/server/api/complianceportal/v1/oauth_initiate_handler.go
Normal file
@@ -0,0 +1,116 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.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 complianceportal_v1
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"go.gearno.de/kit/httpclient"
|
||||
"go.gearno.de/kit/httpserver"
|
||||
"go.gearno.de/kit/log"
|
||||
"go.probo.inc/probo/pkg/baseurl"
|
||||
"go.probo.inc/probo/pkg/complianceportal/visitor"
|
||||
"go.probo.inc/probo/pkg/iam/oauth2"
|
||||
"go.probo.inc/probo/pkg/saferedirect"
|
||||
"go.probo.inc/probo/pkg/server/api/complianceportal"
|
||||
)
|
||||
|
||||
type OAuthInitiateHandler struct {
|
||||
proboBaseURL *baseurl.BaseURL
|
||||
visitor *visitor.Service
|
||||
safeRedirect *saferedirect.SafeRedirect
|
||||
httpClient *http.Client
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
func NewOAuthInitiateHandler(
|
||||
proboBaseURL *baseurl.BaseURL,
|
||||
visitorSvc *visitor.Service,
|
||||
allowedHost saferedirect.AllowedHostFunc,
|
||||
logger *log.Logger,
|
||||
) *OAuthInitiateHandler {
|
||||
return &OAuthInitiateHandler{
|
||||
proboBaseURL: proboBaseURL,
|
||||
visitor: visitorSvc,
|
||||
safeRedirect: saferedirect.New(allowedHost),
|
||||
httpClient: httpclient.DefaultClient(
|
||||
httpclient.WithLogger(logger),
|
||||
httpclient.WithSSRFProtection(),
|
||||
),
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *OAuthInitiateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
portalBaseURL := complianceportal.CompliancePageBaseURLFromContext(ctx)
|
||||
if portalBaseURL == nil {
|
||||
httpserver.RenderError(w, http.StatusNotFound, errNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
continueURL := r.URL.Query().Get("continue")
|
||||
if continueURL == "" {
|
||||
continueURL = "/overview"
|
||||
}
|
||||
|
||||
safeContinue, ok := h.safeRedirect.Validate(ctx, continueURL)
|
||||
if !ok {
|
||||
httpserver.RenderError(w, http.StatusBadRequest, errInvalidContinueURL)
|
||||
return
|
||||
}
|
||||
|
||||
clientID, err := complianceportal.CIMDClientIDURL(*portalBaseURL)
|
||||
if err != nil {
|
||||
h.logger.ErrorCtx(ctx, "cannot build cimd client_id", log.Error(err))
|
||||
httpserver.RenderError(w, http.StatusInternalServerError, errInternal)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
redirectURI, err := complianceportal.OAuthCallbackURL(*portalBaseURL)
|
||||
if err != nil {
|
||||
h.logger.ErrorCtx(ctx, "cannot build oauth redirect_uri", log.Error(err))
|
||||
httpserver.RenderError(w, http.StatusInternalServerError, errInternal)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
metadata, err := oauth2.FetchServerMetadata(ctx, h.httpClient, h.proboBaseURL.String())
|
||||
if err != nil {
|
||||
h.logger.ErrorCtx(ctx, "cannot fetch discovery metadata", log.Error(err))
|
||||
httpserver.RenderError(w, http.StatusInternalServerError, errInternal)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
authorizeURL, err := h.visitor.InitiateOAuthAuthorizeURL(
|
||||
ctx,
|
||||
metadata.AuthorizationEndpoint.String(),
|
||||
clientID,
|
||||
redirectURI,
|
||||
[]string{complianceportal.VisitorOAuthScope},
|
||||
safeContinue,
|
||||
)
|
||||
if err != nil {
|
||||
h.logger.ErrorCtx(ctx, "cannot initiate oauth authorize", log.Error(err))
|
||||
httpserver.RenderError(w, http.StatusInternalServerError, errInternal)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, authorizeURL, http.StatusFound)
|
||||
}
|
||||
56
pkg/server/api/complianceportal/v1/resolver.go
Normal file
56
pkg/server/api/complianceportal/v1/resolver.go
Normal file
@@ -0,0 +1,56 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.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.
|
||||
|
||||
//go:generate go run github.com/99designs/gqlgen generate
|
||||
|
||||
package complianceportal_v1
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"go.gearno.de/kit/log"
|
||||
"go.probo.inc/probo/pkg/baseurl"
|
||||
trust "go.probo.inc/probo/pkg/complianceportal/visitor"
|
||||
"go.probo.inc/probo/pkg/esign"
|
||||
"go.probo.inc/probo/pkg/filemanager"
|
||||
"go.probo.inc/probo/pkg/iam"
|
||||
"go.probo.inc/probo/pkg/mailman"
|
||||
"go.probo.inc/probo/pkg/resourcealias"
|
||||
"go.probo.inc/probo/pkg/server/api/authn"
|
||||
)
|
||||
|
||||
type (
|
||||
TrustAuthConfig struct {
|
||||
CookieName string
|
||||
CookieDomain string
|
||||
CookieDuration time.Duration
|
||||
TokenDuration time.Duration
|
||||
ReportURLDuration time.Duration
|
||||
Scope string
|
||||
TokenType string
|
||||
CookieSecure bool
|
||||
}
|
||||
|
||||
Resolver struct {
|
||||
trust *trust.Service
|
||||
resourceAlias *resourcealias.Service
|
||||
fileManager *filemanager.Service
|
||||
esign *esign.Service
|
||||
mailman *mailman.Service
|
||||
logger *log.Logger
|
||||
iam *iam.Service
|
||||
sessionCookie *authn.Cookie
|
||||
baseURL *baseurl.BaseURL
|
||||
}
|
||||
)
|
||||
@@ -18,7 +18,7 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
package trust_v1
|
||||
package complianceportal_v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -1,4 +1,4 @@
|
||||
package trust_v1
|
||||
package complianceportal_v1
|
||||
|
||||
// This file will be automatically regenerated based on the schema, any resolver
|
||||
// implementations
|
||||
@@ -9,12 +9,12 @@ import (
|
||||
"context"
|
||||
|
||||
"go.gearno.de/kit/log"
|
||||
trust "go.probo.inc/probo/pkg/complianceportal/visitor"
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/server/api/authn"
|
||||
"go.probo.inc/probo/pkg/server/api/compliancepage"
|
||||
"go.probo.inc/probo/pkg/server/api/trust/v1/types"
|
||||
"go.probo.inc/probo/pkg/server/api/complianceportal"
|
||||
"go.probo.inc/probo/pkg/server/api/complianceportal/v1/types"
|
||||
"go.probo.inc/probo/pkg/server/gqlutils"
|
||||
"go.probo.inc/probo/pkg/trust"
|
||||
)
|
||||
|
||||
// CreateRightsRequest is the resolver for the createRightsRequest field.
|
||||
@@ -26,10 +26,10 @@ func (r *mutationResolver) CreateRightsRequest(ctx context.Context, input types.
|
||||
return nil, gqlutils.Unauthenticatedf(ctx, "a verified email is required to submit a request")
|
||||
}
|
||||
|
||||
compliancePage := compliancepage.CompliancePageFromContext(ctx)
|
||||
compliancePage := complianceportal.CompliancePageFromContext(ctx)
|
||||
scope := coredata.NewScopeFromObjectID(compliancePage.OrganizationID)
|
||||
|
||||
rightsRequest, err := r.trust.RightsRequests.Create(
|
||||
rightsRequest, err := r.trust.CreateRightsRequest(
|
||||
ctx,
|
||||
scope,
|
||||
&trust.CreateRightsRequest{
|
||||
@@ -18,8 +18,7 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
// Package trust provides functionality for serving the trust center SPA frontend.
|
||||
package trust
|
||||
package complianceportal_v1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -1,4 +1,4 @@
|
||||
package trust_v1
|
||||
package complianceportal_v1
|
||||
|
||||
// This file will be automatically regenerated based on the schema, any resolver
|
||||
// implementations
|
||||
@@ -18,8 +18,8 @@ import (
|
||||
"go.probo.inc/probo/pkg/page"
|
||||
"go.probo.inc/probo/pkg/server/api/authn"
|
||||
"go.probo.inc/probo/pkg/server/api/complianceportal"
|
||||
"go.probo.inc/probo/pkg/server/api/trust/v1/schema"
|
||||
"go.probo.inc/probo/pkg/server/api/trust/v1/types"
|
||||
"go.probo.inc/probo/pkg/server/api/complianceportal/v1/schema"
|
||||
"go.probo.inc/probo/pkg/server/api/complianceportal/v1/types"
|
||||
"go.probo.inc/probo/pkg/server/gqlutils"
|
||||
)
|
||||
|
||||
@@ -165,7 +165,13 @@ func (r *complianceFrameworkResolver) Framework(ctx context.Context, obj *types.
|
||||
scope := coredata.NewScopeFromObjectID(obj.ID)
|
||||
trustService := r.trust
|
||||
|
||||
framework, err := trustService.GetFramework(ctx, scope, obj.FrameworkID)
|
||||
complianceFramework, err := trustService.GetComplianceFramework(ctx, scope, obj.ID)
|
||||
if err != nil {
|
||||
r.logger.ErrorCtx(ctx, "cannot load compliance framework", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
framework, err := trustService.GetFramework(ctx, scope, complianceFramework.FrameworkID)
|
||||
if err != nil {
|
||||
r.logger.ErrorCtx(ctx, "cannot load framework", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
@@ -827,9 +833,10 @@ func (r *trustCenterResolver) Subprocessors(ctx context.Context, obj *types.Trus
|
||||
|
||||
// SubprocessorCategories is the resolver for the subprocessorCategories field.
|
||||
func (r *trustCenterResolver) SubprocessorCategories(ctx context.Context, obj *types.TrustCenter) ([]coredata.ThirdPartyCategory, error) {
|
||||
trustCenter := complianceportal.CompliancePageFromContext(ctx)
|
||||
scope := coredata.NewScopeFromObjectID(obj.ID)
|
||||
|
||||
categories, err := r.trust.ListDistinctTrustCenterCategoriesForOrganizationID(ctx, scope, obj.Organization.ID)
|
||||
categories, err := r.trust.ListDistinctTrustCenterCategoriesForOrganizationID(ctx, scope, trustCenter.OrganizationID)
|
||||
if err != nil {
|
||||
r.logger.ErrorCtx(ctx, "cannot list subprocessor categories", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
@@ -840,9 +847,10 @@ func (r *trustCenterResolver) SubprocessorCategories(ctx context.Context, obj *t
|
||||
|
||||
// SubprocessorCountries is the resolver for the subprocessorCountries field.
|
||||
func (r *trustCenterResolver) SubprocessorCountries(ctx context.Context, obj *types.TrustCenter) ([]coredata.CountryCode, error) {
|
||||
trustCenter := complianceportal.CompliancePageFromContext(ctx)
|
||||
scope := coredata.NewScopeFromObjectID(obj.ID)
|
||||
|
||||
countries, err := r.trust.ListDistinctTrustCenterCountriesForOrganizationID(ctx, scope, obj.Organization.ID)
|
||||
countries, err := r.trust.ListDistinctTrustCenterCountriesForOrganizationID(ctx, scope, trustCenter.OrganizationID)
|
||||
if err != nil {
|
||||
r.logger.ErrorCtx(ctx, "cannot list subprocessor countries", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
@@ -22,33 +22,17 @@ package types
|
||||
|
||||
import (
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/page"
|
||||
)
|
||||
|
||||
type ComplianceFramework struct {
|
||||
ID gid.GID `json:"id"`
|
||||
Framework *Framework `json:"framework"`
|
||||
FrameworkID gid.GID `json:"-"`
|
||||
}
|
||||
|
||||
func (ComplianceFramework) IsNode() {}
|
||||
func (cf ComplianceFramework) GetID() gid.GID { return cf.ID }
|
||||
|
||||
type ComplianceFrameworkConnection struct {
|
||||
Edges []*ComplianceFrameworkEdge `json:"edges"`
|
||||
PageInfo *PageInfo `json:"pageInfo"`
|
||||
}
|
||||
|
||||
type ComplianceFrameworkEdge struct {
|
||||
Cursor page.CursorKey `json:"cursor"`
|
||||
Node *ComplianceFramework `json:"node"`
|
||||
}
|
||||
|
||||
func NewComplianceFramework(cf *coredata.ComplianceFramework) *ComplianceFramework {
|
||||
return &ComplianceFramework{
|
||||
ID: cf.ID,
|
||||
FrameworkID: cf.FrameworkID,
|
||||
ID: cf.ID,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,25 +21,9 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/mail"
|
||||
)
|
||||
|
||||
type MailingListSubscriber struct {
|
||||
ID gid.GID `json:"id"`
|
||||
FullName string `json:"fullName"`
|
||||
Email mail.Addr `json:"email"`
|
||||
Status coredata.MailingListSubscriberStatus `json:"status"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
func (MailingListSubscriber) IsNode() {}
|
||||
func (m MailingListSubscriber) GetID() gid.GID { return m.ID }
|
||||
|
||||
func NewMailingListSubscriber(s *coredata.MailingListSubscriber) *MailingListSubscriber {
|
||||
return &MailingListSubscriber{
|
||||
ID: s.ID,
|
||||
@@ -1,141 +0,0 @@
|
||||
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
//go:generate go tool github.com/99designs/gqlgen generate
|
||||
|
||||
// Copyright (c) 2025 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
package trust_v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"go.gearno.de/kit/log"
|
||||
"go.probo.inc/probo/pkg/baseurl"
|
||||
trust "go.probo.inc/probo/pkg/complianceportal/visitor"
|
||||
"go.probo.inc/probo/pkg/esign"
|
||||
"go.probo.inc/probo/pkg/filemanager"
|
||||
"go.probo.inc/probo/pkg/iam"
|
||||
"go.probo.inc/probo/pkg/mailman"
|
||||
"go.probo.inc/probo/pkg/resourcealias"
|
||||
"go.probo.inc/probo/pkg/securecookie"
|
||||
"go.probo.inc/probo/pkg/server/api/authn"
|
||||
"go.probo.inc/probo/pkg/server/api/complianceportal"
|
||||
"go.probo.inc/probo/pkg/server/gqlutils"
|
||||
)
|
||||
|
||||
type (
|
||||
TrustAuthConfig struct {
|
||||
CookieName string
|
||||
CookieDomain string
|
||||
CookieDuration time.Duration
|
||||
TokenDuration time.Duration
|
||||
ReportURLDuration time.Duration
|
||||
Scope string
|
||||
TokenType string
|
||||
CookieSecure bool
|
||||
}
|
||||
|
||||
Resolver struct {
|
||||
trust *trust.Service
|
||||
resourceAlias *resourcealias.Service
|
||||
fileManager *filemanager.Service
|
||||
esign *esign.Service
|
||||
mailman *mailman.Service
|
||||
logger *log.Logger
|
||||
iam *iam.Service
|
||||
sessionCookie *authn.Cookie
|
||||
baseURL *baseurl.BaseURL
|
||||
}
|
||||
)
|
||||
|
||||
func NewMux(
|
||||
logger *log.Logger,
|
||||
iamSvc *iam.Service,
|
||||
trustSvc *trust.Service,
|
||||
resourceAliasSvc *resourcealias.Service,
|
||||
fileManagerSvc *filemanager.Service,
|
||||
esignSvc *esign.Service,
|
||||
mailmanSvc *mailman.Service,
|
||||
cookieConfig securecookie.Config,
|
||||
tokenSecret string,
|
||||
baseURL *baseurl.BaseURL,
|
||||
graphqlLimits gqlutils.Limits,
|
||||
) *chi.Mux {
|
||||
r := chi.NewMux()
|
||||
|
||||
r.Use(complianceportal.NewCompliancePagePresenceMiddleware())
|
||||
|
||||
sessionTransferHandler := NewSessionTransferHandler(
|
||||
iamSvc,
|
||||
cookieConfig,
|
||||
func(ctx context.Context, host string) bool {
|
||||
_, err := trustSvc.GetPortalByDomainName(ctx, host)
|
||||
return err == nil
|
||||
},
|
||||
logger,
|
||||
)
|
||||
r.Method(http.MethodGet, "/session-transfer", sessionTransferHandler)
|
||||
|
||||
graphqlHandler := NewGraphQLHandler(
|
||||
iamSvc,
|
||||
trustSvc,
|
||||
resourceAliasSvc,
|
||||
fileManagerSvc,
|
||||
esignSvc,
|
||||
mailmanSvc,
|
||||
logger,
|
||||
baseURL,
|
||||
cookieConfig,
|
||||
tokenSecret,
|
||||
graphqlLimits,
|
||||
)
|
||||
|
||||
r.Group(
|
||||
func(r chi.Router) {
|
||||
r.Use(authn.NewSessionMiddleware(iamSvc, cookieConfig))
|
||||
r.Use(complianceportal.NewMemberProvisioningMiddleware(trustSvc, logger))
|
||||
r.Handle("/graphql", graphqlHandler)
|
||||
},
|
||||
)
|
||||
|
||||
return r
|
||||
}
|
||||
Reference in New Issue
Block a user