Flatten compliance portal package layout
Remove the root complianceportal package and the resolver facade that existed only to break an IAM import cycle. Admin policies, domain URL helpers, and actions live under management; visitor OAuth metadata, brand URLs, and public read paths live under visitor. Drop the duplicate trust API magic-link mutations now that Connect handles portal auth, and stop IAM from owning compliance page email branding. Signed-off-by: Bryan Frimin <bryan@probo.com>
This commit is contained in:
@@ -15,23 +15,23 @@
|
||||
package complianceportal
|
||||
|
||||
import (
|
||||
portal "go.probo.inc/probo/pkg/complianceportal"
|
||||
"go.probo.inc/probo/pkg/complianceportal/visitor"
|
||||
)
|
||||
|
||||
const (
|
||||
VisitorOAuthScope = portal.VisitorOAuthScope
|
||||
VisitorOAuthScope = visitor.VisitorOAuthScope
|
||||
GraphQLPath = "/graphql"
|
||||
CIMDMetadataPath = portal.CIMDMetadataPath
|
||||
BrandLogoPath = portal.BrandLogoPath
|
||||
BrandDarkLogoPath = portal.BrandDarkLogoPath
|
||||
CIMDMetadataPath = visitor.CIMDMetadataPath
|
||||
BrandLogoPath = visitor.BrandLogoPath
|
||||
BrandDarkLogoPath = visitor.BrandDarkLogoPath
|
||||
OAuthInitiatePath = "/initiate"
|
||||
OAuthCallbackPath = portal.OAuthCallbackPath
|
||||
OAuthCallbackPath = visitor.OAuthCallbackPath
|
||||
)
|
||||
|
||||
func CIMDClientIDURL(portalBaseURL string) (string, error) {
|
||||
return portal.CIMDClientIDURL(portalBaseURL)
|
||||
return visitor.CIMDClientIDURL(portalBaseURL)
|
||||
}
|
||||
|
||||
func OAuthCallbackURL(portalBaseURL string) (string, error) {
|
||||
return portal.OAuthCallbackURL(portalBaseURL)
|
||||
return visitor.OAuthCallbackURL(portalBaseURL)
|
||||
}
|
||||
|
||||
@@ -10,150 +10,14 @@ import (
|
||||
"errors"
|
||||
|
||||
"go.gearno.de/kit/log"
|
||||
"go.probo.inc/probo/pkg/baseurl"
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/iam"
|
||||
"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/complianceportal/v1/types"
|
||||
"go.probo.inc/probo/pkg/server/gqlutils"
|
||||
)
|
||||
|
||||
// SendMagicLink is the resolver for the sendMagicLink field.
|
||||
func (r *mutationResolver) SendMagicLink(ctx context.Context, input types.SendMagicLinkInput) (*types.SendMagicLinkPayload, error) {
|
||||
trustCenter := complianceportal.CompliancePageFromContext(ctx)
|
||||
|
||||
baseURL := complianceportal.CompliancePageBaseURLFromContext(ctx)
|
||||
|
||||
safeRedirect := saferedirect.New(saferedirect.StaticHosts(baseurl.MustParse(*baseURL).Host()))
|
||||
|
||||
if input.Continue != nil {
|
||||
_, ok := safeRedirect.Validate(ctx, *input.Continue)
|
||||
if !ok {
|
||||
return nil, gqlutils.Invalidf(ctx, "invalid continue URL")
|
||||
}
|
||||
}
|
||||
|
||||
req := &iam.SendMagicLinkRequest{
|
||||
Email: input.Email,
|
||||
CompliancePageID: &trustCenter.ID,
|
||||
OrganizationID: &trustCenter.OrganizationID,
|
||||
URLPath: "verify-magic-link",
|
||||
Continue: input.Continue,
|
||||
}
|
||||
|
||||
if err := r.iam.AuthService.SendMagicLink(ctx, req); err != nil {
|
||||
r.logger.ErrorCtx(ctx, "cannot send magic link", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// VerifyMagicLink is the resolver for the verifyMagicLink field.
|
||||
func (r *mutationResolver) VerifyMagicLink(ctx context.Context, input types.VerifyMagicLinkInput) (*types.VerifyMagicLinkPayload, error) {
|
||||
session := authn.SessionFromContext(ctx)
|
||||
identity := authn.IdentityFromContext(ctx)
|
||||
|
||||
email, err := r.iam.AuthService.GetMagicLinkEmail(ctx, input.Token)
|
||||
if err != nil {
|
||||
if _, ok := errors.AsType[*iam.ErrExpiredToken](err); ok {
|
||||
return nil, gqlutils.TokenExpired(ctx, err)
|
||||
}
|
||||
|
||||
if _, ok := errors.AsType[*iam.ErrInvalidToken](err); ok {
|
||||
return nil, gqlutils.Invalid(ctx, err)
|
||||
}
|
||||
|
||||
r.logger.ErrorCtx(ctx, "cannot get magic link email", log.Error(err))
|
||||
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
var continueURL *string
|
||||
|
||||
switch {
|
||||
case session == nil:
|
||||
var err error
|
||||
|
||||
identity, session, continueURL, err = r.iam.AuthService.OpenSessionWithMagicLink(ctx, input.Token)
|
||||
if err != nil {
|
||||
if _, ok := errors.AsType[*iam.ErrExpiredToken](err); ok {
|
||||
return nil, gqlutils.TokenExpired(ctx, err)
|
||||
}
|
||||
|
||||
if _, ok := errors.AsType[*iam.ErrTokenAlreadyUsed](err); ok {
|
||||
return nil, gqlutils.TokenAlreadyUsed(ctx, err)
|
||||
}
|
||||
|
||||
if _, ok := errors.AsType[*iam.ErrInvalidToken](err); ok {
|
||||
return nil, gqlutils.Invalid(ctx, err)
|
||||
}
|
||||
|
||||
r.logger.ErrorCtx(ctx, "cannot open session with magic link", log.Error(err))
|
||||
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
case identity.EmailAddress != email:
|
||||
if err := r.iam.SessionService.CloseSession(ctx, session.ID); err != nil {
|
||||
r.logger.ErrorCtx(ctx, "cannot close session", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
var err error
|
||||
|
||||
identity, session, continueURL, err = r.iam.AuthService.OpenSessionWithMagicLink(ctx, input.Token)
|
||||
if err != nil {
|
||||
if _, ok := errors.AsType[*iam.ErrExpiredToken](err); ok {
|
||||
return nil, gqlutils.TokenExpired(ctx, err)
|
||||
}
|
||||
|
||||
if _, ok := errors.AsType[*iam.ErrTokenAlreadyUsed](err); ok {
|
||||
return nil, gqlutils.TokenAlreadyUsed(ctx, err)
|
||||
}
|
||||
|
||||
if _, ok := errors.AsType[*iam.ErrInvalidToken](err); ok {
|
||||
return nil, gqlutils.Invalid(ctx, err)
|
||||
}
|
||||
|
||||
r.logger.ErrorCtx(ctx, "cannot open session with magic link", log.Error(err))
|
||||
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
r.logger.ErrorCtx(ctx, "cannot provision member", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
w := gqlutils.HTTPResponseWriterFromContext(ctx)
|
||||
r.sessionCookie.Set(w, session)
|
||||
|
||||
return &types.VerifyMagicLinkPayload{
|
||||
Continue: continueURL,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UpdateFullName is the resolver for the updateFullName field.
|
||||
func (r *mutationResolver) UpdateFullName(ctx context.Context, input types.UpdateFullNameInput) (*types.UpdateFullNamePayload, error) {
|
||||
identity := authn.IdentityFromContext(ctx)
|
||||
|
||||
@@ -132,7 +132,7 @@ func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error
|
||||
|
||||
trustCenterFile, err := trustService.GetPortalFile(ctx, scope, trustCenter.OrganizationID, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, visitor.ErrTrustCenterFileNotFound) || errors.Is(err, visitor.ErrTrustCenterFileNotVisible) {
|
||||
if errors.Is(err, visitor.ErrPortalFileNotFound) || errors.Is(err, visitor.ErrPortalFileNotVisible) {
|
||||
return nil, gqlutils.NotFoundf(ctx, "node %q not found", id)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,30 +1,9 @@
|
||||
extend type Mutation {
|
||||
sendMagicLink(input: SendMagicLinkInput!): SendMagicLinkPayload
|
||||
@authentication(required: OPTIONAL)
|
||||
verifyMagicLink(input: VerifyMagicLinkInput!): VerifyMagicLinkPayload
|
||||
@authentication(required: OPTIONAL)
|
||||
updateFullName(input: UpdateFullNameInput!): UpdateFullNamePayload
|
||||
@authentication(required: PRESENT) @sessionOnly
|
||||
signOut: SignOutPayload! @authentication(required: PRESENT) @sessionOnly
|
||||
}
|
||||
|
||||
input SendMagicLinkInput {
|
||||
email: EmailAddr!
|
||||
continue: String
|
||||
}
|
||||
|
||||
type SendMagicLinkPayload {
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
input VerifyMagicLinkInput {
|
||||
token: String!
|
||||
}
|
||||
|
||||
type VerifyMagicLinkPayload {
|
||||
continue: String
|
||||
}
|
||||
|
||||
input UpdateFullNameInput {
|
||||
fullName: String!
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ import (
|
||||
"go.gearno.de/kit/log"
|
||||
"go.gearno.de/x/ref"
|
||||
"go.probo.inc/probo/pkg/baseurl"
|
||||
page "go.probo.inc/probo/pkg/complianceportal"
|
||||
visitor "go.probo.inc/probo/pkg/complianceportal/visitor"
|
||||
"go.probo.inc/probo/pkg/esign"
|
||||
"go.probo.inc/probo/pkg/filemanager"
|
||||
@@ -148,7 +147,7 @@ func compliancePageHeadData() HeadDataFunc {
|
||||
}
|
||||
|
||||
if tc.LogoFileID != nil && compliancePageBaseURL != nil {
|
||||
faviconURL, err := page.BrandLogoURL(*compliancePageBaseURL)
|
||||
faviconURL, err := visitor.BrandLogoURL(*compliancePageBaseURL)
|
||||
if err == nil {
|
||||
headData.FaviconURL = faviconURL
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
"net/http"
|
||||
|
||||
"go.gearno.de/kit/httpserver"
|
||||
portal "go.probo.inc/probo/pkg/complianceportal"
|
||||
"go.probo.inc/probo/pkg/complianceportal/visitor"
|
||||
"go.probo.inc/probo/pkg/server/api/complianceportal"
|
||||
)
|
||||
|
||||
@@ -38,7 +38,7 @@ func (h *oauthClientMetadataHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
|
||||
return
|
||||
}
|
||||
|
||||
doc, err := portal.BuildClientMetadataDocument(compliancePage, *baseURL)
|
||||
doc, err := visitor.BuildClientMetadataDocument(compliancePage, *baseURL)
|
||||
if err != nil {
|
||||
httpserver.RenderError(w, http.StatusInternalServerError, errInternal)
|
||||
return
|
||||
|
||||
@@ -467,7 +467,7 @@ func (r *mutationResolver) ExportTrustCenterFile(ctx context.Context, input type
|
||||
|
||||
trustCenterFile, err := trustService.GetPortalFile(ctx, scope, trustCenter.OrganizationID, input.TrustCenterFileID)
|
||||
if err != nil {
|
||||
if errors.Is(err, visitor.ErrTrustCenterFileNotFound) || errors.Is(err, visitor.ErrTrustCenterFileNotVisible) {
|
||||
if errors.Is(err, visitor.ErrPortalFileNotFound) || errors.Is(err, visitor.ErrPortalFileNotVisible) {
|
||||
return nil, gqlutils.NotFoundf(ctx, "trust center file %q not found", input.TrustCenterFileID)
|
||||
}
|
||||
|
||||
@@ -620,7 +620,7 @@ func (r *mutationResolver) RequestTrustCenterFileAccess(ctx context.Context, inp
|
||||
|
||||
trustCenterFile, err := trustService.GetPortalFile(ctx, scope, trustCenter.OrganizationID, input.TrustCenterFileID)
|
||||
if err != nil {
|
||||
if errors.Is(err, visitor.ErrTrustCenterFileNotFound) || errors.Is(err, visitor.ErrTrustCenterFileNotVisible) {
|
||||
if errors.Is(err, visitor.ErrPortalFileNotFound) || errors.Is(err, visitor.ErrPortalFileNotVisible) {
|
||||
return nil, gqlutils.NotFoundf(ctx, "trust center file %q not found", input.TrustCenterFileID)
|
||||
}
|
||||
|
||||
@@ -836,7 +836,7 @@ func (r *trustCenterResolver) SubprocessorCategories(ctx context.Context, obj *t
|
||||
trustCenter := complianceportal.CompliancePageFromContext(ctx)
|
||||
scope := coredata.NewScopeFromObjectID(obj.ID)
|
||||
|
||||
categories, err := r.trust.ListDistinctTrustCenterCategoriesForOrganizationID(ctx, scope, trustCenter.OrganizationID)
|
||||
categories, err := r.trust.ListDistinctPortalCategoriesForOrganizationID(ctx, scope, trustCenter.OrganizationID)
|
||||
if err != nil {
|
||||
r.logger.ErrorCtx(ctx, "cannot list subprocessor categories", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
@@ -850,7 +850,7 @@ func (r *trustCenterResolver) SubprocessorCountries(ctx context.Context, obj *ty
|
||||
trustCenter := complianceportal.CompliancePageFromContext(ctx)
|
||||
scope := coredata.NewScopeFromObjectID(obj.ID)
|
||||
|
||||
countries, err := r.trust.ListDistinctTrustCenterCountriesForOrganizationID(ctx, scope, trustCenter.OrganizationID)
|
||||
countries, err := r.trust.ListDistinctPortalCountriesForOrganizationID(ctx, scope, trustCenter.OrganizationID)
|
||||
if err != nil {
|
||||
r.logger.ErrorCtx(ctx, "cannot list subprocessor countries", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
@@ -986,7 +986,7 @@ func (r *trustCenterFileResolver) IsUserAuthorized(ctx context.Context, obj *typ
|
||||
|
||||
trustCenterFile, err := trustService.GetPortalFile(ctx, scope, trustCenter.OrganizationID, obj.ID)
|
||||
if err != nil {
|
||||
if errors.Is(err, visitor.ErrTrustCenterFileNotFound) || errors.Is(err, visitor.ErrTrustCenterFileNotVisible) {
|
||||
if errors.Is(err, visitor.ErrPortalFileNotFound) || errors.Is(err, visitor.ErrPortalFileNotVisible) {
|
||||
return false, gqlutils.NotFoundf(ctx, "trust center file %q not found", obj.ID)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user