Add assume org session on api

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
Bryan Frimin
2025-12-19 15:49:39 +01:00
parent 342d93911b
commit 3f0616530a
4 changed files with 931 additions and 35 deletions

View File

@@ -59,7 +59,7 @@ type Query {
} }
type Mutation { type Mutation {
signIn(input: SignInInput!): SignInPayload! @session(required: NONE) signIn(input: SignInInput!): SignInPayload! @session(required: OPTIONAL)
signUp(input: SignUpInput!): SignUpPayload! @session(required: NONE) signUp(input: SignUpInput!): SignUpPayload! @session(required: NONE)
signOut: SignOutPayload! @session(required: PRESENT) signOut: SignOutPayload! @session(required: PRESENT)
signUpFromInvitation( signUpFromInvitation(
@@ -75,6 +75,9 @@ type Mutation {
@session(required: PRESENT) @session(required: PRESENT)
changeEmail(input: ChangeEmailInput!): ChangeEmailPayload! changeEmail(input: ChangeEmailInput!): ChangeEmailPayload!
@session(required: PRESENT) @session(required: PRESENT)
assumeOrganizationSession(
input: AssumeOrganizationSessionInput!
): AssumeOrganizationSessionPayload! @session(required: PRESENT)
updateIdentityProfile( updateIdentityProfile(
input: UpdateIdentityProfileInput! input: UpdateIdentityProfileInput!
@@ -417,6 +420,12 @@ enum ProvisioningSource {
SAML SAML
} }
enum ReauthenticationReason {
SESSION_EXPIRED
SENSITIVE_ACTION
POLICY_REQUIREMENT
}
enum MembershipOrderField enum MembershipOrderField
@goModel(model: "go.probo.inc/probo/pkg/coredata.MembershipOrderField") { @goModel(model: "go.probo.inc/probo/pkg/coredata.MembershipOrderField") {
ROLE ROLE
@@ -579,6 +588,10 @@ input ChangeEmailInput {
password: String! password: String!
} }
input AssumeOrganizationSessionInput {
organizationId: ID!
}
input DeactivateAccountInput { input DeactivateAccountInput {
password: String! password: String!
} }
@@ -709,6 +722,7 @@ input DeleteSAMLConfigurationInput {
type SignInPayload { type SignInPayload {
identity: Identity identity: Identity
session: Session
} }
type SignUpPayload { type SignUpPayload {
@@ -743,6 +757,29 @@ type ChangeEmailPayload {
success: Boolean! success: Boolean!
} }
union AssumeOrganizationSessionResult =
| OrganizationSessionCreated
| PasswordRequired
| SAMLAuthenticationRequired
type OrganizationSessionCreated {
session: Session!
membership: Membership!
}
type PasswordRequired {
reason: ReauthenticationReason!
}
type SAMLAuthenticationRequired {
reason: ReauthenticationReason!
redirectUrl: String!
}
type AssumeOrganizationSessionPayload {
result: AssumeOrganizationSessionResult!
}
type DeactivateAccountPayload { type DeactivateAccountPayload {
success: Boolean! success: Boolean!
} }

File diff suppressed because it is too large Load Diff

View File

@@ -16,6 +16,10 @@ import (
"go.probo.inc/probo/pkg/page" "go.probo.inc/probo/pkg/page"
) )
type AssumeOrganizationSessionResult interface {
IsAssumeOrganizationSessionResult()
}
type Node interface { type Node interface {
IsNode() IsNode()
GetID() gid.GID GetID() gid.GID
@@ -42,6 +46,14 @@ type Application struct {
AvailableAccessLevels []AccessLevel `json:"availableAccessLevels"` AvailableAccessLevels []AccessLevel `json:"availableAccessLevels"`
} }
type AssumeOrganizationSessionInput struct {
OrganizationID gid.GID `json:"organizationId"`
}
type AssumeOrganizationSessionPayload struct {
Result AssumeOrganizationSessionResult `json:"result"`
}
type ChangeEmailInput struct { type ChangeEmailInput struct {
NewEmail mail.Addr `json:"newEmail"` NewEmail mail.Addr `json:"newEmail"`
Password string `json:"password"` Password string `json:"password"`
@@ -263,6 +275,13 @@ type Organization struct {
func (Organization) IsNode() {} func (Organization) IsNode() {}
func (this Organization) GetID() gid.GID { return this.ID } func (this Organization) GetID() gid.GID { return this.ID }
type OrganizationSessionCreated struct {
Session *Session `json:"session"`
Membership *Membership `json:"membership"`
}
func (OrganizationSessionCreated) IsAssumeOrganizationSessionResult() {}
type PageInfo struct { type PageInfo struct {
HasNextPage bool `json:"hasNextPage"` HasNextPage bool `json:"hasNextPage"`
HasPreviousPage bool `json:"hasPreviousPage"` HasPreviousPage bool `json:"hasPreviousPage"`
@@ -270,6 +289,12 @@ type PageInfo struct {
EndCursor *page.CursorKey `json:"endCursor,omitempty"` EndCursor *page.CursorKey `json:"endCursor,omitempty"`
} }
type PasswordRequired struct {
Reason ReauthenticationReason `json:"reason"`
}
func (PasswordRequired) IsAssumeOrganizationSessionResult() {}
type Permission struct { type Permission struct {
ID gid.GID `json:"id"` ID gid.GID `json:"id"`
CreatedAt time.Time `json:"createdAt"` CreatedAt time.Time `json:"createdAt"`
@@ -365,6 +390,13 @@ type SAMLAttributeMappingsInput struct {
Role *string `json:"role,omitempty"` Role *string `json:"role,omitempty"`
} }
type SAMLAuthenticationRequired struct {
Reason ReauthenticationReason `json:"reason"`
RedirectURL string `json:"redirectUrl"`
}
func (SAMLAuthenticationRequired) IsAssumeOrganizationSessionResult() {}
type SAMLConfiguration struct { type SAMLConfiguration struct {
ID gid.GID `json:"id"` ID gid.GID `json:"id"`
EmailDomain string `json:"emailDomain"` EmailDomain string `json:"emailDomain"`
@@ -442,6 +474,7 @@ type SignInInput struct {
type SignInPayload struct { type SignInPayload struct {
Identity *Identity `json:"identity,omitempty"` Identity *Identity `json:"identity,omitempty"`
Session *Session `json:"session,omitempty"`
} }
type SignOutPayload struct { type SignOutPayload struct {
@@ -820,6 +853,63 @@ func (e ProvisioningSource) MarshalJSON() ([]byte, error) {
return buf.Bytes(), nil return buf.Bytes(), nil
} }
type ReauthenticationReason string
const (
ReauthenticationReasonSessionExpired ReauthenticationReason = "SESSION_EXPIRED"
ReauthenticationReasonSensitiveAction ReauthenticationReason = "SENSITIVE_ACTION"
ReauthenticationReasonPolicyRequirement ReauthenticationReason = "POLICY_REQUIREMENT"
)
var AllReauthenticationReason = []ReauthenticationReason{
ReauthenticationReasonSessionExpired,
ReauthenticationReasonSensitiveAction,
ReauthenticationReasonPolicyRequirement,
}
func (e ReauthenticationReason) IsValid() bool {
switch e {
case ReauthenticationReasonSessionExpired, ReauthenticationReasonSensitiveAction, ReauthenticationReasonPolicyRequirement:
return true
}
return false
}
func (e ReauthenticationReason) String() string {
return string(e)
}
func (e *ReauthenticationReason) UnmarshalGQL(v any) error {
str, ok := v.(string)
if !ok {
return fmt.Errorf("enums must be strings")
}
*e = ReauthenticationReason(str)
if !e.IsValid() {
return fmt.Errorf("%s is not a valid ReauthenticationReason", str)
}
return nil
}
func (e ReauthenticationReason) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}
func (e *ReauthenticationReason) UnmarshalJSON(b []byte) error {
s, err := strconv.Unquote(string(b))
if err != nil {
return err
}
return e.UnmarshalGQL(s)
}
func (e ReauthenticationReason) MarshalJSON() ([]byte, error) {
var buf bytes.Buffer
e.MarshalGQL(&buf)
return buf.Bytes(), nil
}
type SessionRequirement string type SessionRequirement string
const ( const (

View File

@@ -215,13 +215,8 @@ func (r *mutationResolver) SignIn(ctx context.Context, input types.SignInInput)
) )
return &types.SignInPayload{ return &types.SignInPayload{
Identity: &types.Identity{ Identity: types.NewIdentity(user),
ID: user.ID, Session: types.NewSession(session),
Email: user.EmailAddress,
EmailVerified: user.EmailAddressVerified,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
},
}, nil }, nil
} }
@@ -465,6 +460,50 @@ func (r *mutationResolver) ChangeEmail(ctx context.Context, input types.ChangeEm
}, nil }, nil
} }
// AssumeOrganizationSession is the resolver for the assumeOrganizationSession field.
func (r *mutationResolver) AssumeOrganizationSession(ctx context.Context, input types.AssumeOrganizationSessionInput) (*types.AssumeOrganizationSessionPayload, error) {
rootSession := SessionFromContext(ctx)
childSession, membership, err := r.iam.SessionService.AssumeOrganizationSession(ctx, rootSession.ID, input.OrganizationID)
if err != nil {
var (
errMembershipNotFound *iam.ErrMembershipNotFound
errPasswordRequired *iam.ErrPasswordRequired
errSAMLAuthenticationRequired *iam.ErrSAMLAuthenticationRequired
)
switch {
case errors.As(err, &errMembershipNotFound):
return nil, gqlutils.NotFound(err)
case errors.As(err, &errPasswordRequired):
return &types.AssumeOrganizationSessionPayload{
Result: types.PasswordRequired{
Reason: types.ReauthenticationReason(errPasswordRequired.Reason),
},
}, nil
case errors.As(err, &errSAMLAuthenticationRequired):
return &types.AssumeOrganizationSessionPayload{
Result: types.SAMLAuthenticationRequired{
Reason: types.ReauthenticationReason(errSAMLAuthenticationRequired.Reason),
RedirectURL: errSAMLAuthenticationRequired.RedirectURL,
},
}, nil
default:
panic(fmt.Errorf("cannot assume organization session: %w", err))
}
}
return &types.AssumeOrganizationSessionPayload{
Result: types.OrganizationSessionCreated{
Session: types.NewSession(childSession),
Membership: types.NewMembership(membership),
},
}, nil
}
// UpdateIdentityProfile is the resolver for the updateIdentityProfile field. // UpdateIdentityProfile is the resolver for the updateIdentityProfile field.
func (r *mutationResolver) UpdateIdentityProfile(ctx context.Context, input types.UpdateIdentityProfileInput) (*types.UpdateIdentityProfilePayload, error) { func (r *mutationResolver) UpdateIdentityProfile(ctx context.Context, input types.UpdateIdentityProfileInput) (*types.UpdateIdentityProfilePayload, error) {
panic(fmt.Errorf("not implemented: UpdateIdentityProfile - updateIdentityProfile")) panic(fmt.Errorf("not implemented: UpdateIdentityProfile - updateIdentityProfile"))
@@ -1061,3 +1100,15 @@ type personalAPIKeyConnectionResolver struct{ *Resolver }
type queryResolver struct{ *Resolver } type queryResolver struct{ *Resolver }
type sAMLConfigurationConnectionResolver struct{ *Resolver } type sAMLConfigurationConnectionResolver struct{ *Resolver }
type sessionConnectionResolver struct{ *Resolver } type sessionConnectionResolver struct{ *Resolver }
// !!! WARNING !!!
// The code below was going to be deleted when updating resolvers. It has been copied here so you have
// one last chance to move it out of harms way if you want. There are two reasons this happens:
// - When renaming or deleting a resolver the old code will be put in here. You can safely delete
// it when you're done.
// - You have helper methods in this file. Move them out to keep these resolver files clean.
/*
func (r *mutationResolver) SignInWithSession(ctx context.Context, input types.SignInWithSessionInput) (*types.SignInWithSessionPayload, error) {
panic(fmt.Errorf("not implemented: SignInWithSession - signInWithSession"))
}
*/