diff --git a/pkg/server/api/connect/v1/graphql/base.graphql b/pkg/server/api/connect/v1/graphql/base.graphql index 4f27b4294..d1bac308a 100644 --- a/pkg/server/api/connect/v1/graphql/base.graphql +++ b/pkg/server/api/connect/v1/graphql/base.graphql @@ -34,4 +34,9 @@ type Query { @session(required: OPTIONAL) } +type OIDCProviderInfo { + name: String! + loginURL: String! +} + type Mutation diff --git a/pkg/server/api/connect/v1/graphql/invitation.graphql b/pkg/server/api/connect/v1/graphql/invitation.graphql new file mode 100644 index 000000000..df0862389 --- /dev/null +++ b/pkg/server/api/connect/v1/graphql/invitation.graphql @@ -0,0 +1,64 @@ +type Invitation implements Node { + id: ID! + expiresAt: Datetime! + acceptedAt: Datetime + createdAt: Datetime! + status: InvitationStatus! + + permission(action: String!): Boolean! + @goField(forceResolver: true) + @session(required: PRESENT) +} + +enum InvitationStatus + @goModel(model: "go.probo.inc/probo/pkg/coredata.InvitationStatus") { + PENDING + @goEnum(value: "go.probo.inc/probo/pkg/coredata.InvitationStatusPending") + ACCEPTED + @goEnum(value: "go.probo.inc/probo/pkg/coredata.InvitationStatusAccepted") + EXPIRED + @goEnum(value: "go.probo.inc/probo/pkg/coredata.InvitationStatusExpired") +} + +enum InvitationOrderField + @goModel(model: "go.probo.inc/probo/pkg/coredata.InvitationOrderField") { + CREATED_AT + @goEnum( + value: "go.probo.inc/probo/pkg/coredata.InvitationOrderFieldCreatedAt" + ) +} + +input InvitationOrder + @goModel( + model: "go.probo.inc/probo/pkg/server/api/connect/v1/types.InvitationOrderBy" + ) { + direction: OrderDirection! + field: InvitationOrderField! +} + +type InvitationConnection + @goModel( + model: "go.probo.inc/probo/pkg/server/api/connect/v1/types.InvitationConnection" + ) { + edges: [InvitationEdge!]! + pageInfo: PageInfo! +} + +type InvitationEdge { + node: Invitation! + cursor: CursorKey! +} + +extend type Mutation { + inviteUser(input: InviteUserInput!): InviteUserPayload + @session(required: PRESENT) +} + +input InviteUserInput { + organizationId: ID! + profileId: ID! +} + +type InviteUserPayload { + invitationEdge: InvitationEdge! +} diff --git a/pkg/server/api/connect/v1/graphql/membership.graphql b/pkg/server/api/connect/v1/graphql/membership.graphql new file mode 100644 index 000000000..4310df3c6 --- /dev/null +++ b/pkg/server/api/connect/v1/graphql/membership.graphql @@ -0,0 +1,36 @@ +enum MembershipRole + @goModel(model: "go.probo.inc/probo/pkg/coredata.MembershipRole") { + OWNER @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleOwner") + ADMIN @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleAdmin") + EMPLOYEE + @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleEmployee") + VIEWER @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleViewer") + AUDITOR + @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleAuditor") +} + +type Membership implements Node { + id: ID! + createdAt: Datetime! + role: MembershipRole! + + lastSession: Session @goField(forceResolver: true) + + permission(action: String!): Boolean! + @goField(forceResolver: true) + @session(required: PRESENT) +} + +extend type Mutation { + updateMembership(input: UpdateMembershipInput!): UpdateMembershipPayload! +} + +input UpdateMembershipInput { + organizationId: ID! + membershipId: ID! + role: MembershipRole! +} + +type UpdateMembershipPayload { + membership: Membership! +} diff --git a/pkg/server/api/connect/v1/graphql/oidc.graphql b/pkg/server/api/connect/v1/graphql/oidc.graphql deleted file mode 100644 index 016b85698..000000000 --- a/pkg/server/api/connect/v1/graphql/oidc.graphql +++ /dev/null @@ -1,4 +0,0 @@ -type OIDCProviderInfo { - name: String! - loginURL: String! -} diff --git a/pkg/server/api/connect/v1/graphql/profile.graphql b/pkg/server/api/connect/v1/graphql/profile.graphql index 5444a2a03..ca65f8de5 100644 --- a/pkg/server/api/connect/v1/graphql/profile.graphql +++ b/pkg/server/api/connect/v1/graphql/profile.graphql @@ -43,80 +43,6 @@ enum ProfileSource SCIM @goEnum(value: "go.probo.inc/probo/pkg/coredata.ProfileSourceSCIM") } -enum MembershipRole - @goModel(model: "go.probo.inc/probo/pkg/coredata.MembershipRole") { - OWNER @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleOwner") - ADMIN @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleAdmin") - EMPLOYEE - @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleEmployee") - VIEWER @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleViewer") - AUDITOR - @goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipRoleAuditor") -} - -type Membership implements Node { - id: ID! - createdAt: Datetime! - role: MembershipRole! - - lastSession: Session @goField(forceResolver: true) - - permission(action: String!): Boolean! - @goField(forceResolver: true) - @session(required: PRESENT) -} - -type Invitation implements Node { - id: ID! - expiresAt: Datetime! - acceptedAt: Datetime - createdAt: Datetime! - status: InvitationStatus! - - permission(action: String!): Boolean! - @goField(forceResolver: true) - @session(required: PRESENT) -} - -enum InvitationStatus - @goModel(model: "go.probo.inc/probo/pkg/coredata.InvitationStatus") { - PENDING - @goEnum(value: "go.probo.inc/probo/pkg/coredata.InvitationStatusPending") - ACCEPTED - @goEnum(value: "go.probo.inc/probo/pkg/coredata.InvitationStatusAccepted") - EXPIRED - @goEnum(value: "go.probo.inc/probo/pkg/coredata.InvitationStatusExpired") -} - -enum InvitationOrderField - @goModel(model: "go.probo.inc/probo/pkg/coredata.InvitationOrderField") { - CREATED_AT - @goEnum( - value: "go.probo.inc/probo/pkg/coredata.InvitationOrderFieldCreatedAt" - ) -} - -input InvitationOrder - @goModel( - model: "go.probo.inc/probo/pkg/server/api/connect/v1/types.InvitationOrderBy" - ) { - direction: OrderDirection! - field: InvitationOrderField! -} - -type InvitationConnection - @goModel( - model: "go.probo.inc/probo/pkg/server/api/connect/v1/types.InvitationConnection" - ) { - edges: [InvitationEdge!]! - pageInfo: PageInfo! -} - -type InvitationEdge { - node: Invitation! - cursor: CursorKey! -} - enum ProfileOrderField @goModel(model: "go.probo.inc/probo/pkg/coredata.MembershipProfileOrderField") { FULL_NAME @@ -168,11 +94,8 @@ type ProfileEdge { extend type Mutation { createUser(input: CreateUserInput!): CreateUserPayload @session(required: PRESENT) - inviteUser(input: InviteUserInput!): InviteUserPayload - @session(required: PRESENT) deactivateUser(input: DeactivateUserInput!): DeactivateUserPayload updateUser(input: UpdateUserInput!): UpdateUserPayload! - updateMembership(input: UpdateMembershipInput!): UpdateMembershipPayload! removeUser(input: RemoveUserInput!): RemoveUserPayload @session(required: PRESENT) } @@ -189,11 +112,6 @@ input CreateUserInput { contractEndDate: Datetime @goField(omittable: true) } -input InviteUserInput { - organizationId: ID! - profileId: ID! -} - input ActivateUserInput { organizationId: ID! profileId: ID! @@ -214,12 +132,6 @@ input UpdateUserInput { contractEndDate: Datetime @goField(omittable: true) } -input UpdateMembershipInput { - organizationId: ID! - membershipId: ID! - role: MembershipRole! -} - input RemoveUserInput { organizationId: ID! profileId: ID! @@ -229,10 +141,6 @@ type CreateUserPayload { profileEdge: ProfileEdge! } -type InviteUserPayload { - invitationEdge: InvitationEdge! -} - type DeactivateUserPayload { success: Boolean! } @@ -241,10 +149,6 @@ type UpdateUserPayload { profile: Profile! } -type UpdateMembershipPayload { - membership: Membership! -} - type RemoveUserPayload { deletedProfileId: ID! } diff --git a/pkg/server/api/connect/v1/invitation_resolvers.go b/pkg/server/api/connect/v1/invitation_resolvers.go new file mode 100644 index 000000000..92f4b609d --- /dev/null +++ b/pkg/server/api/connect/v1/invitation_resolvers.go @@ -0,0 +1,62 @@ +package connect_v1 + +// This file will be automatically regenerated based on the schema, any resolver +// implementations +// will be copied through when generating and any unknown code will be moved to the end. +// Code generated by github.com/99designs/gqlgen version v0.17.87 + +import ( + "context" + "errors" + + "go.gearno.de/kit/log" + "go.probo.inc/probo/pkg/coredata" + "go.probo.inc/probo/pkg/iam" + "go.probo.inc/probo/pkg/server/api/connect/v1/schema" + "go.probo.inc/probo/pkg/server/api/connect/v1/types" + "go.probo.inc/probo/pkg/server/gqlutils" +) + +// Permission is the resolver for the permission field. +func (r *invitationResolver) Permission(ctx context.Context, obj *types.Invitation, action string) (bool, error) { + return r.Resolver.Permission(ctx, obj, action) +} + +// InviteUser is the resolver for the inviteUser field. +func (r *mutationResolver) InviteUser(ctx context.Context, input types.InviteUserInput) (*types.InviteUserPayload, error) { + if err := r.authorize(ctx, input.ProfileID, iam.ActionInvitationCreate); err != nil { + return nil, err + } + + invitation, err := r.iam.OrganizationService.InviteUser( + ctx, + &iam.CreateInvitationRequest{ + OrganizationID: input.OrganizationID, + ProfileID: input.ProfileID, + }, + ) + if err != nil { + var errOrganizationNotFound *iam.ErrOrganizationNotFound + var errUserAlreadyExists *iam.ErrUserAlreadyExists + + if errors.As(err, &errOrganizationNotFound) { + return nil, gqlutils.NotFound(ctx, err) + } + + if errors.As(err, &errUserAlreadyExists) { + return nil, gqlutils.Conflict(ctx, err) + } + + r.logger.ErrorCtx(ctx, "cannot invite user", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } + + return &types.InviteUserPayload{ + InvitationEdge: types.NewInvitationEdge(invitation, coredata.InvitationOrderFieldCreatedAt), + }, nil +} + +// Invitation returns schema.InvitationResolver implementation. +func (r *Resolver) Invitation() schema.InvitationResolver { return &invitationResolver{r} } + +type invitationResolver struct{ *Resolver } diff --git a/pkg/server/api/connect/v1/membership_resolvers.go b/pkg/server/api/connect/v1/membership_resolvers.go new file mode 100644 index 000000000..2fa5f2933 --- /dev/null +++ b/pkg/server/api/connect/v1/membership_resolvers.go @@ -0,0 +1,78 @@ +package connect_v1 + +// This file will be automatically regenerated based on the schema, any resolver +// implementations +// will be copied through when generating and any unknown code will be moved to the end. +// Code generated by github.com/99designs/gqlgen version v0.17.87 + +import ( + "context" + "errors" + + "go.gearno.de/kit/log" + "go.probo.inc/probo/pkg/coredata" + "go.probo.inc/probo/pkg/iam" + "go.probo.inc/probo/pkg/server/api/authn" + "go.probo.inc/probo/pkg/server/api/authz" + "go.probo.inc/probo/pkg/server/api/connect/v1/schema" + "go.probo.inc/probo/pkg/server/api/connect/v1/types" + "go.probo.inc/probo/pkg/server/gqlutils" +) + +// LastSession is the resolver for the lastSession field. +func (r *membershipResolver) LastSession(ctx context.Context, obj *types.Membership) (*types.Session, error) { + if err := r.authorize(ctx, obj.ID, iam.ActionMembershipGet, authz.WithSkipAssumptionCheck()); err != nil { + return nil, err + } + + session := authn.SessionFromContext(ctx) + if session == nil { + return nil, nil + } + + childSession, err := r.iam.SessionService.GetActiveSessionForMembership(ctx, session.ID, obj.ID) + if err != nil { + var errSessionNotFound *iam.ErrSessionNotFound + if errors.As(err, &errSessionNotFound) { + return nil, nil + } + + r.logger.ErrorCtx(ctx, "cannot get active session for membership", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } + + return types.NewSession(childSession), nil +} + +// Permission is the resolver for the permission field. +func (r *membershipResolver) Permission(ctx context.Context, obj *types.Membership, action string) (bool, error) { + return r.Resolver.Permission(ctx, obj, action) +} + +// UpdateMembership is the resolver for the updateMembership field. +func (r *mutationResolver) UpdateMembership(ctx context.Context, input types.UpdateMembershipInput) (*types.UpdateMembershipPayload, error) { + if err := r.authorize(ctx, input.MembershipID, iam.ActionMembershipUpdate); err != nil { + return nil, err + } + + if input.Role == coredata.MembershipRoleOwner { + if err := r.authorize(ctx, input.MembershipID, iam.ActionMembershipRoleSetOwner); err != nil { + return nil, err + } + } + + membership, err := r.iam.OrganizationService.UpdateMempership(ctx, input.OrganizationID, input.MembershipID, input.Role) + if err != nil { + r.logger.ErrorCtx(ctx, "cannot update membership", log.Error(err)) + return nil, gqlutils.Internal(ctx) + } + + return &types.UpdateMembershipPayload{ + Membership: types.NewMembership(membership), + }, nil +} + +// Membership returns schema.MembershipResolver implementation. +func (r *Resolver) Membership() schema.MembershipResolver { return &membershipResolver{r} } + +type membershipResolver struct{ *Resolver } diff --git a/pkg/server/api/connect/v1/profile_resolvers.go b/pkg/server/api/connect/v1/profile_resolvers.go index 235d65ac7..f79d543c4 100644 --- a/pkg/server/api/connect/v1/profile_resolvers.go +++ b/pkg/server/api/connect/v1/profile_resolvers.go @@ -13,7 +13,6 @@ import ( "go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/iam" "go.probo.inc/probo/pkg/page" - "go.probo.inc/probo/pkg/server/api/authn" "go.probo.inc/probo/pkg/server/api/authz" "go.probo.inc/probo/pkg/server/api/connect/v1/schema" "go.probo.inc/probo/pkg/server/api/connect/v1/types" @@ -21,41 +20,6 @@ import ( "go.probo.inc/probo/pkg/server/gqlutils/types/cursor" ) -// Permission is the resolver for the permission field. -func (r *invitationResolver) Permission(ctx context.Context, obj *types.Invitation, action string) (bool, error) { - return r.Resolver.Permission(ctx, obj, action) -} - -// LastSession is the resolver for the lastSession field. -func (r *membershipResolver) LastSession(ctx context.Context, obj *types.Membership) (*types.Session, error) { - if err := r.authorize(ctx, obj.ID, iam.ActionMembershipGet, authz.WithSkipAssumptionCheck()); err != nil { - return nil, err - } - - session := authn.SessionFromContext(ctx) - if session == nil { - return nil, nil - } - - childSession, err := r.iam.SessionService.GetActiveSessionForMembership(ctx, session.ID, obj.ID) - if err != nil { - var errSessionNotFound *iam.ErrSessionNotFound - if errors.As(err, &errSessionNotFound) { - return nil, nil - } - - r.logger.ErrorCtx(ctx, "cannot get active session for membership", log.Error(err)) - return nil, gqlutils.Internal(ctx) - } - - return types.NewSession(childSession), nil -} - -// Permission is the resolver for the permission field. -func (r *membershipResolver) Permission(ctx context.Context, obj *types.Membership, action string) (bool, error) { - return r.Resolver.Permission(ctx, obj, action) -} - // CreateUser is the resolver for the createUser field. func (r *mutationResolver) CreateUser(ctx context.Context, input types.CreateUserInput) (*types.CreateUserPayload, error) { if err := r.authorize(ctx, input.OrganizationID, iam.ActionMembershipProfileCreate); err != nil { @@ -91,40 +55,6 @@ func (r *mutationResolver) CreateUser(ctx context.Context, input types.CreateUse }, nil } -// InviteUser is the resolver for the inviteUser field. -func (r *mutationResolver) InviteUser(ctx context.Context, input types.InviteUserInput) (*types.InviteUserPayload, error) { - if err := r.authorize(ctx, input.ProfileID, iam.ActionInvitationCreate); err != nil { - return nil, err - } - - invitation, err := r.iam.OrganizationService.InviteUser( - ctx, - &iam.CreateInvitationRequest{ - OrganizationID: input.OrganizationID, - ProfileID: input.ProfileID, - }, - ) - if err != nil { - var errOrganizationNotFound *iam.ErrOrganizationNotFound - var errUserAlreadyExists *iam.ErrUserAlreadyExists - - if errors.As(err, &errOrganizationNotFound) { - return nil, gqlutils.NotFound(ctx, err) - } - - if errors.As(err, &errUserAlreadyExists) { - return nil, gqlutils.Conflict(ctx, err) - } - - r.logger.ErrorCtx(ctx, "cannot invite user", log.Error(err)) - return nil, gqlutils.Internal(ctx) - } - - return &types.InviteUserPayload{ - InvitationEdge: types.NewInvitationEdge(invitation, coredata.InvitationOrderFieldCreatedAt), - }, nil -} - // DeactivateUser is the resolver for the deactivateUser field. func (r *mutationResolver) DeactivateUser(ctx context.Context, input types.DeactivateUserInput) (*types.DeactivateUserPayload, error) { if err := r.authorize(ctx, input.ProfileID, iam.ActionMembershipProfileDeactivate); err != nil { @@ -175,29 +105,6 @@ func (r *mutationResolver) UpdateUser(ctx context.Context, input types.UpdateUse }, nil } -// UpdateMembership is the resolver for the updateMembership field. -func (r *mutationResolver) UpdateMembership(ctx context.Context, input types.UpdateMembershipInput) (*types.UpdateMembershipPayload, error) { - if err := r.authorize(ctx, input.MembershipID, iam.ActionMembershipUpdate); err != nil { - return nil, err - } - - if input.Role == coredata.MembershipRoleOwner { - if err := r.authorize(ctx, input.MembershipID, iam.ActionMembershipRoleSetOwner); err != nil { - return nil, err - } - } - - membership, err := r.iam.OrganizationService.UpdateMempership(ctx, input.OrganizationID, input.MembershipID, input.Role) - if err != nil { - r.logger.ErrorCtx(ctx, "cannot update membership", log.Error(err)) - return nil, gqlutils.Internal(ctx) - } - - return &types.UpdateMembershipPayload{ - Membership: types.NewMembership(membership), - }, nil -} - // RemoveUser is the resolver for the removeUser field. func (r *mutationResolver) RemoveUser(ctx context.Context, input types.RemoveUserInput) (*types.RemoveUserPayload, error) { if err := r.authorize(ctx, input.ProfileID, iam.ActionMembershipProfileDelete); err != nil { @@ -339,12 +246,6 @@ func (r *profileConnectionResolver) TotalCount(ctx context.Context, obj *types.P return nil, gqlutils.Internal(ctx) } -// Invitation returns schema.InvitationResolver implementation. -func (r *Resolver) Invitation() schema.InvitationResolver { return &invitationResolver{r} } - -// Membership returns schema.MembershipResolver implementation. -func (r *Resolver) Membership() schema.MembershipResolver { return &membershipResolver{r} } - // Profile returns schema.ProfileResolver implementation. func (r *Resolver) Profile() schema.ProfileResolver { return &profileResolver{r} } @@ -353,7 +254,5 @@ func (r *Resolver) ProfileConnection() schema.ProfileConnectionResolver { return &profileConnectionResolver{r} } -type invitationResolver struct{ *Resolver } -type membershipResolver struct{ *Resolver } type profileResolver struct{ *Resolver } type profileConnectionResolver struct{ *Resolver } diff --git a/pkg/server/api/trust/v1/graphql/base.graphql b/pkg/server/api/trust/v1/graphql/base.graphql index d7cdc2e91..3b36a9ae1 100644 --- a/pkg/server/api/trust/v1/graphql/base.graphql +++ b/pkg/server/api/trust/v1/graphql/base.graphql @@ -30,4 +30,9 @@ type Query { @session(required: OPTIONAL) } +type OIDCProviderInfo { + name: String! + loginURL: String! +} + type Mutation diff --git a/pkg/server/api/trust/v1/graphql/oidc.graphql b/pkg/server/api/trust/v1/graphql/oidc.graphql deleted file mode 100644 index 016b85698..000000000 --- a/pkg/server/api/trust/v1/graphql/oidc.graphql +++ /dev/null @@ -1,4 +0,0 @@ -type OIDCProviderInfo { - name: String! - loginURL: String! -}