Add scim bridge with connector
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
@@ -347,6 +347,8 @@ type SCIMConfiguration implements Node {
|
||||
updatedAt: Datetime!
|
||||
organization: Organization @goField(forceResolver: true)
|
||||
|
||||
bridge: SCIMBridge @goField(forceResolver: true)
|
||||
|
||||
events(
|
||||
first: Int
|
||||
after: CursorKey
|
||||
@@ -360,6 +362,50 @@ type SCIMConfiguration implements Node {
|
||||
@session(required: PRESENT)
|
||||
}
|
||||
|
||||
type SCIMBridge implements Node {
|
||||
id: ID!
|
||||
state: SCIMBridgeState!
|
||||
scimConfiguration: SCIMConfiguration @goField(forceResolver: true)
|
||||
connector: Connector @goField(forceResolver: true)
|
||||
type: SCIMBridgeType!
|
||||
createdAt: Datetime!
|
||||
updatedAt: Datetime!
|
||||
|
||||
permission(action: String!): Boolean!
|
||||
@goField(forceResolver: true)
|
||||
@session(required: PRESENT)
|
||||
}
|
||||
|
||||
type Connector implements Node {
|
||||
id: ID!
|
||||
provider: ConnectorProvider!
|
||||
createdAt: Datetime!
|
||||
updatedAt: Datetime!
|
||||
|
||||
permission(action: String!): Boolean!
|
||||
@goField(forceResolver: true)
|
||||
@session(required: PRESENT)
|
||||
}
|
||||
|
||||
enum ConnectorProvider
|
||||
@goModel(model: "go.probo.inc/probo/pkg/coredata.ConnectorProvider") {
|
||||
SLACK @goEnum(value: "go.probo.inc/probo/pkg/coredata.ConnectorProviderSlack")
|
||||
GOOGLE_WORKSPACE
|
||||
@goEnum(value: "go.probo.inc/probo/pkg/coredata.ConnectorProviderGoogleWorkspace")
|
||||
}
|
||||
|
||||
enum SCIMBridgeType
|
||||
@goModel(model: "go.probo.inc/probo/pkg/coredata.SCIMBridgeType") {
|
||||
GOOGLE_WORKSPACE @goEnum(value: "go.probo.inc/probo/pkg/coredata.SCIMBridgeTypeGoogleWorkspace")
|
||||
}
|
||||
|
||||
enum SCIMBridgeState
|
||||
@goModel(model: "go.probo.inc/probo/pkg/coredata.SCIMBridgeState") {
|
||||
PENDING @goEnum(value: "go.probo.inc/probo/pkg/coredata.SCIMBridgeStatePending")
|
||||
ACTIVE @goEnum(value: "go.probo.inc/probo/pkg/coredata.SCIMBridgeStateActive")
|
||||
FAILED @goEnum(value: "go.probo.inc/probo/pkg/coredata.SCIMBridgeStateFailed")
|
||||
}
|
||||
|
||||
type SCIMEvent implements Node {
|
||||
id: ID!
|
||||
method: String!
|
||||
@@ -846,6 +892,7 @@ type DeleteSAMLConfigurationPayload {
|
||||
|
||||
input CreateSCIMConfigurationInput {
|
||||
organizationId: ID!
|
||||
connectorId: ID
|
||||
}
|
||||
|
||||
input DeleteSCIMConfigurationInput {
|
||||
@@ -860,6 +907,7 @@ input RegenerateSCIMTokenInput {
|
||||
|
||||
type CreateSCIMConfigurationPayload {
|
||||
scimConfiguration: SCIMConfiguration!
|
||||
scimBridge: SCIMBridge
|
||||
token: String!
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
38
pkg/server/api/connect/v1/types/bridge.go
Normal file
38
pkg/server/api/connect/v1/types/bridge.go
Normal file
@@ -0,0 +1,38 @@
|
||||
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package types
|
||||
|
||||
import "go.probo.inc/probo/pkg/coredata"
|
||||
|
||||
func NewSCIMBridge(bridge *coredata.SCIMBridge) *SCIMBridge {
|
||||
var connector *Connector
|
||||
if bridge.ConnectorID != nil {
|
||||
connector = &Connector{
|
||||
ID: *bridge.ConnectorID,
|
||||
}
|
||||
}
|
||||
|
||||
return &SCIMBridge{
|
||||
ID: bridge.ID,
|
||||
State: bridge.State,
|
||||
ScimConfiguration: &SCIMConfiguration{
|
||||
ID: bridge.ScimConfigurationID,
|
||||
},
|
||||
Connector: connector,
|
||||
Type: bridge.Type,
|
||||
CreatedAt: bridge.CreatedAt,
|
||||
UpdatedAt: bridge.UpdatedAt,
|
||||
}
|
||||
}
|
||||
26
pkg/server/api/connect/v1/types/connector.go
Normal file
26
pkg/server/api/connect/v1/types/connector.go
Normal file
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package types
|
||||
|
||||
import "go.probo.inc/probo/pkg/coredata"
|
||||
|
||||
func NewConnector(connector *coredata.Connector) *Connector {
|
||||
return &Connector{
|
||||
ID: connector.ID,
|
||||
Provider: connector.Provider,
|
||||
CreatedAt: connector.CreatedAt,
|
||||
UpdatedAt: connector.UpdatedAt,
|
||||
}
|
||||
}
|
||||
@@ -19,12 +19,18 @@ import (
|
||||
)
|
||||
|
||||
func NewSCIMConfiguration(scimConfiguration *coredata.SCIMConfiguration) *SCIMConfiguration {
|
||||
var bridge *SCIMBridge
|
||||
if scimConfiguration.BridgeID != nil {
|
||||
bridge = &SCIMBridge{
|
||||
ID: *scimConfiguration.BridgeID,
|
||||
}
|
||||
}
|
||||
|
||||
return &SCIMConfiguration{
|
||||
ID: scimConfiguration.ID,
|
||||
Organization: &Organization{
|
||||
ID: scimConfiguration.OrganizationID,
|
||||
},
|
||||
CreatedAt: scimConfiguration.CreatedAt,
|
||||
UpdatedAt: scimConfiguration.UpdatedAt,
|
||||
ID: scimConfiguration.ID,
|
||||
Organization: &Organization{ID: scimConfiguration.OrganizationID},
|
||||
Bridge: bridge,
|
||||
CreatedAt: scimConfiguration.CreatedAt,
|
||||
UpdatedAt: scimConfiguration.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,17 @@ type ChangePasswordPayload struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
type Connector struct {
|
||||
ID gid.GID `json:"id"`
|
||||
Provider coredata.ConnectorProvider `json:"provider"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
Permission bool `json:"permission"`
|
||||
}
|
||||
|
||||
func (Connector) IsNode() {}
|
||||
func (this Connector) GetID() gid.GID { return this.ID }
|
||||
|
||||
type CreateOrganizationInput struct {
|
||||
Name string `json:"name"`
|
||||
LogoFile *graphql.Upload `json:"logoFile,omitempty"`
|
||||
@@ -96,11 +107,13 @@ type CreateSAMLConfigurationPayload struct {
|
||||
}
|
||||
|
||||
type CreateSCIMConfigurationInput struct {
|
||||
OrganizationID gid.GID `json:"organizationId"`
|
||||
OrganizationID gid.GID `json:"organizationId"`
|
||||
ConnectorID *gid.GID `json:"connectorId,omitempty"`
|
||||
}
|
||||
|
||||
type CreateSCIMConfigurationPayload struct {
|
||||
ScimConfiguration *SCIMConfiguration `json:"scimConfiguration"`
|
||||
ScimBridge *SCIMBridge `json:"scimBridge,omitempty"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
@@ -396,12 +409,27 @@ type SAMLConfigurationEdge struct {
|
||||
Cursor page.CursorKey `json:"cursor"`
|
||||
}
|
||||
|
||||
type SCIMBridge struct {
|
||||
ID gid.GID `json:"id"`
|
||||
State coredata.SCIMBridgeState `json:"state"`
|
||||
ScimConfiguration *SCIMConfiguration `json:"scimConfiguration,omitempty"`
|
||||
Connector *Connector `json:"connector,omitempty"`
|
||||
Type coredata.SCIMBridgeType `json:"type"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
Permission bool `json:"permission"`
|
||||
}
|
||||
|
||||
func (SCIMBridge) IsNode() {}
|
||||
func (this SCIMBridge) GetID() gid.GID { return this.ID }
|
||||
|
||||
type SCIMConfiguration struct {
|
||||
ID gid.GID `json:"id"`
|
||||
EndpointURL string `json:"endpointUrl"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
Organization *Organization `json:"organization,omitempty"`
|
||||
Bridge *SCIMBridge `json:"bridge,omitempty"`
|
||||
Events *SCIMEventConnection `json:"events,omitempty"`
|
||||
Permission bool `json:"permission"`
|
||||
}
|
||||
|
||||
@@ -27,6 +27,11 @@ import (
|
||||
"go.probo.inc/probo/pkg/server/gqlutils/types/cursor"
|
||||
)
|
||||
|
||||
// Permission is the resolver for the permission field.
|
||||
func (r *connectorResolver) Permission(ctx context.Context, obj *types.Connector, action string) (bool, error) {
|
||||
return r.Resolver.Permission(ctx, obj, action)
|
||||
}
|
||||
|
||||
// Memberships is the resolver for the memberships field.
|
||||
func (r *identityResolver) Memberships(ctx context.Context, obj *types.Identity, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.MembershipOrderBy) (*types.MembershipConnection, error) {
|
||||
if err := r.authorize(ctx, obj.ID, iam.ActionMembershipList); err != nil {
|
||||
@@ -1119,10 +1124,24 @@ func (r *mutationResolver) CreateSCIMConfiguration(ctx context.Context, input ty
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
return &types.CreateSCIMConfigurationPayload{
|
||||
var bridge *types.SCIMBridge
|
||||
|
||||
if input.ConnectorID != nil {
|
||||
scimBridge, err := r.iam.OrganizationService.CreateSCIMBridge(ctx, input.OrganizationID, config.ID, *input.ConnectorID)
|
||||
if err != nil {
|
||||
r.logger.ErrorCtx(ctx, "cannot create scim bridge", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
bridge = types.NewSCIMBridge(scimBridge)
|
||||
}
|
||||
|
||||
payload := &types.CreateSCIMConfigurationPayload{
|
||||
ScimConfiguration: types.NewSCIMConfiguration(config),
|
||||
ScimBridge: bridge,
|
||||
Token: token,
|
||||
}, nil
|
||||
}
|
||||
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
// DeleteSCIMConfiguration is the resolver for the deleteSCIMConfiguration field.
|
||||
@@ -1578,6 +1597,63 @@ func (r *sAMLConfigurationConnectionResolver) TotalCount(ctx context.Context, ob
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
// ScimConfiguration is the resolver for the scimConfiguration field.
|
||||
func (r *sCIMBridgeResolver) ScimConfiguration(ctx context.Context, obj *types.SCIMBridge) (*types.SCIMConfiguration, error) {
|
||||
if err := r.authorize(ctx, obj.ScimConfiguration.ID, iam.ActionSCIMConfigurationGet); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if gqlutils.OnlyIDSelected(ctx) {
|
||||
return &types.SCIMConfiguration{
|
||||
ID: obj.ScimConfiguration.ID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
scimConfiguration, err := r.iam.GetSCIMConfiguration(ctx, obj.ScimConfiguration.ID)
|
||||
if err != nil {
|
||||
var errNoSCIMConfigurationFound *iam.ErrNoSCIMConfigurationFound
|
||||
if errors.As(err, &errNoSCIMConfigurationFound) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return types.NewSCIMConfiguration(scimConfiguration), nil
|
||||
}
|
||||
|
||||
// Connector is the resolver for the connector field.
|
||||
func (r *sCIMBridgeResolver) Connector(ctx context.Context, obj *types.SCIMBridge) (*types.Connector, error) {
|
||||
if obj.Connector == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Authorize based on the SCIM configuration (connector accessed via bridge is a sub-resource)
|
||||
if err := r.authorize(ctx, obj.ScimConfiguration.ID, iam.ActionSCIMConfigurationGet); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if gqlutils.OnlyIDSelected(ctx) {
|
||||
return &types.Connector{
|
||||
ID: obj.Connector.ID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Use metadata-only loading since we don't need the encrypted connection data
|
||||
connector, err := r.iam.OrganizationService.GetConnectorMetadataByID(ctx, obj.Connector.ID)
|
||||
if err != nil {
|
||||
r.logger.ErrorCtx(ctx, "cannot get connector", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
return types.NewConnector(connector), nil
|
||||
}
|
||||
|
||||
// Permission is the resolver for the permission field.
|
||||
func (r *sCIMBridgeResolver) Permission(ctx context.Context, obj *types.SCIMBridge, action string) (bool, error) {
|
||||
return r.Resolver.Permission(ctx, obj, action)
|
||||
}
|
||||
|
||||
// EndpointURL is the resolver for the endpointUrl field.
|
||||
func (r *sCIMConfigurationResolver) EndpointURL(ctx context.Context, obj *types.SCIMConfiguration) (string, error) {
|
||||
return r.baseURL.WithPath("/api/connect/v1/scim/2.0").MustString(), nil
|
||||
@@ -1609,6 +1685,31 @@ func (r *sCIMConfigurationResolver) Organization(ctx context.Context, obj *types
|
||||
return types.NewOrganization(organization), nil
|
||||
}
|
||||
|
||||
// Bridge is the resolver for the bridge field.
|
||||
func (r *sCIMConfigurationResolver) Bridge(ctx context.Context, obj *types.SCIMConfiguration) (*types.SCIMBridge, error) {
|
||||
|
||||
if obj.Bridge == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if err := r.authorize(ctx, obj.ID, iam.ActionSCIMConfigurationGet); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bridge, err := r.iam.OrganizationService.GetSCIMBridgeByID(ctx, obj.Bridge.ID)
|
||||
if err != nil {
|
||||
var errSCIMBridgeNotFound *iam.ErrSCIMBridgeNotFound
|
||||
if errors.As(err, &errSCIMBridgeNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
r.logger.ErrorCtx(ctx, "cannot get scim bridge", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
return types.NewSCIMBridge(bridge), nil
|
||||
}
|
||||
|
||||
// Events is the resolver for the events field.
|
||||
func (r *sCIMConfigurationResolver) Events(ctx context.Context, obj *types.SCIMConfiguration, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.SCIMEventOrderBy) (*types.SCIMEventConnection, error) {
|
||||
if err := r.authorize(ctx, obj.ID, iam.ActionSCIMEventList); err != nil {
|
||||
@@ -1734,6 +1835,9 @@ func (r *sessionConnectionResolver) TotalCount(ctx context.Context, obj *types.S
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
// Connector returns schema.ConnectorResolver implementation.
|
||||
func (r *Resolver) Connector() schema.ConnectorResolver { return &connectorResolver{r} }
|
||||
|
||||
// Identity returns schema.IdentityResolver implementation.
|
||||
func (r *Resolver) Identity() schema.IdentityResolver { return &identityResolver{r} }
|
||||
|
||||
@@ -1785,6 +1889,9 @@ func (r *Resolver) SAMLConfigurationConnection() schema.SAMLConfigurationConnect
|
||||
return &sAMLConfigurationConnectionResolver{r}
|
||||
}
|
||||
|
||||
// SCIMBridge returns schema.SCIMBridgeResolver implementation.
|
||||
func (r *Resolver) SCIMBridge() schema.SCIMBridgeResolver { return &sCIMBridgeResolver{r} }
|
||||
|
||||
// SCIMConfiguration returns schema.SCIMConfigurationResolver implementation.
|
||||
func (r *Resolver) SCIMConfiguration() schema.SCIMConfigurationResolver {
|
||||
return &sCIMConfigurationResolver{r}
|
||||
@@ -1806,6 +1913,7 @@ func (r *Resolver) SessionConnection() schema.SessionConnectionResolver {
|
||||
return &sessionConnectionResolver{r}
|
||||
}
|
||||
|
||||
type connectorResolver struct{ *Resolver }
|
||||
type identityResolver struct{ *Resolver }
|
||||
type invitationResolver struct{ *Resolver }
|
||||
type invitationConnectionResolver struct{ *Resolver }
|
||||
@@ -1819,6 +1927,7 @@ type personalAPIKeyConnectionResolver struct{ *Resolver }
|
||||
type queryResolver struct{ *Resolver }
|
||||
type sAMLConfigurationResolver struct{ *Resolver }
|
||||
type sAMLConfigurationConnectionResolver struct{ *Resolver }
|
||||
type sCIMBridgeResolver struct{ *Resolver }
|
||||
type sCIMConfigurationResolver struct{ *Resolver }
|
||||
type sCIMEventResolver struct{ *Resolver }
|
||||
type sCIMEventConnectionResolver struct{ *Resolver }
|
||||
|
||||
Reference in New Issue
Block a user