Rewrite identity and access management
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
@@ -21,14 +21,14 @@ import (
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/cors"
|
||||
"github.com/vektah/gqlparser/v2/ast"
|
||||
"go.gearno.de/kit/httpserver"
|
||||
"go.gearno.de/kit/log"
|
||||
"go.probo.inc/probo/pkg/auth"
|
||||
"go.probo.inc/probo/pkg/authz"
|
||||
"go.probo.inc/probo/pkg/baseurl"
|
||||
"go.probo.inc/probo/pkg/connector"
|
||||
"go.probo.inc/probo/pkg/iam"
|
||||
"go.probo.inc/probo/pkg/probo"
|
||||
"go.probo.inc/probo/pkg/saferedirect"
|
||||
"go.probo.inc/probo/pkg/securecookie"
|
||||
connect_v1 "go.probo.inc/probo/pkg/server/api/connect/v1"
|
||||
console_v1 "go.probo.inc/probo/pkg/server/api/console/v1"
|
||||
mcp_v1 "go.probo.inc/probo/pkg/server/api/mcp/v1"
|
||||
slack_v1 "go.probo.inc/probo/pkg/server/api/slack/v1"
|
||||
@@ -38,39 +38,16 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
ConsoleAuthConfig struct {
|
||||
CookieName string
|
||||
CookieDomain string
|
||||
SessionDuration time.Duration
|
||||
CookieSecret string
|
||||
CookieSecure bool
|
||||
}
|
||||
|
||||
TrustAuthConfig struct {
|
||||
CookieName string
|
||||
CookieDomain string
|
||||
CookieDuration time.Duration
|
||||
TokenDuration time.Duration
|
||||
ReportURLDuration time.Duration
|
||||
TokenSecret string
|
||||
Scope string
|
||||
TokenType string
|
||||
CookieSecure bool
|
||||
}
|
||||
|
||||
Config struct {
|
||||
BaseURL *baseurl.BaseURL
|
||||
AllowedOrigins []string
|
||||
Probo *probo.Service
|
||||
Auth *auth.Service
|
||||
Authz *authz.Service
|
||||
IAM *iam.Service
|
||||
Trust *trust.Service
|
||||
Slack *slack.Service
|
||||
SAML *auth.SAMLService
|
||||
ConsoleAuth ConsoleAuthConfig
|
||||
TrustAuth TrustAuthConfig
|
||||
MCPConfig MCPConfig
|
||||
Cookie securecookie.Config
|
||||
TokenSecret string
|
||||
ConnectorRegistry *connector.ConnectorRegistry
|
||||
SafeRedirect *saferedirect.SafeRedirect
|
||||
CustomDomainCname string
|
||||
Logger *log.Logger
|
||||
}
|
||||
@@ -82,26 +59,21 @@ type (
|
||||
}
|
||||
|
||||
Server struct {
|
||||
cfg Config
|
||||
trustAPIHandler http.Handler
|
||||
consoleAPIHandler http.Handler
|
||||
mcpAPIHandler http.Handler
|
||||
slackAPIHandler http.Handler
|
||||
cfg Config
|
||||
compliancePageHandler http.Handler
|
||||
consoleHandler http.Handler
|
||||
mcpHandler http.Handler
|
||||
slackHandler http.Handler
|
||||
connectHandler http.Handler
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
ErrMissingProboService = errors.New("server configuration requires a valid probo.Service instance")
|
||||
ErrMissingAuthService = errors.New("server configuration requires a valid auth.Service instance")
|
||||
ErrMissingAuthzService = errors.New("server configuration requires a valid authz.Service instance")
|
||||
ErrMissingIAMService = errors.New("server configuration requires a valid iam.Service instance")
|
||||
ErrMissingSlackService = errors.New("server configuration requires a valid slack.Service instance")
|
||||
)
|
||||
|
||||
// GetConsoleSchema returns the GraphQL schema for the console API
|
||||
func GetConsoleSchema() *ast.Schema {
|
||||
return console_v1.GetSchema()
|
||||
}
|
||||
|
||||
func methodNotAllowed(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
|
||||
@@ -131,91 +103,53 @@ func NewServer(cfg Config) (*Server, error) {
|
||||
return nil, ErrMissingProboService
|
||||
}
|
||||
|
||||
if cfg.Auth == nil {
|
||||
return nil, ErrMissingAuthService
|
||||
}
|
||||
|
||||
if cfg.Authz == nil {
|
||||
return nil, ErrMissingAuthzService
|
||||
if cfg.IAM == nil {
|
||||
return nil, ErrMissingIAMService
|
||||
}
|
||||
|
||||
if cfg.Slack == nil {
|
||||
return nil, ErrMissingSlackService
|
||||
}
|
||||
|
||||
trustAPIHandler := trust_v1.NewMux(
|
||||
cfg.Logger.Named("trust.v1"),
|
||||
cfg.Auth,
|
||||
cfg.Authz,
|
||||
cfg.Trust,
|
||||
console_v1.AuthConfig{
|
||||
CookieName: cfg.ConsoleAuth.CookieName,
|
||||
CookieDomain: cfg.ConsoleAuth.CookieDomain,
|
||||
SessionDuration: cfg.ConsoleAuth.SessionDuration,
|
||||
CookieSecret: cfg.ConsoleAuth.CookieSecret,
|
||||
CookieSecure: cfg.ConsoleAuth.CookieSecure,
|
||||
},
|
||||
trust_v1.TrustAuthConfig{
|
||||
CookieName: cfg.TrustAuth.CookieName,
|
||||
CookieDomain: cfg.TrustAuth.CookieDomain,
|
||||
CookieDuration: cfg.TrustAuth.CookieDuration,
|
||||
TokenDuration: cfg.TrustAuth.TokenDuration,
|
||||
ReportURLDuration: cfg.TrustAuth.ReportURLDuration,
|
||||
TokenSecret: cfg.TrustAuth.TokenSecret,
|
||||
Scope: cfg.TrustAuth.Scope,
|
||||
TokenType: cfg.TrustAuth.TokenType,
|
||||
CookieSecure: cfg.TrustAuth.CookieSecure,
|
||||
},
|
||||
cfg.Slack,
|
||||
)
|
||||
|
||||
consoleAPIHandler := console_v1.NewMux(
|
||||
cfg.Logger.Named("console.v1"),
|
||||
cfg.Probo,
|
||||
cfg.Auth,
|
||||
cfg.Authz,
|
||||
console_v1.AuthConfig{
|
||||
CookieName: cfg.ConsoleAuth.CookieName,
|
||||
CookieDomain: cfg.ConsoleAuth.CookieDomain,
|
||||
SessionDuration: cfg.ConsoleAuth.SessionDuration,
|
||||
CookieSecret: cfg.ConsoleAuth.CookieSecret,
|
||||
CookieSecure: cfg.ConsoleAuth.CookieSecure,
|
||||
},
|
||||
cfg.ConnectorRegistry,
|
||||
cfg.SafeRedirect,
|
||||
cfg.CustomDomainCname,
|
||||
cfg.SAML,
|
||||
)
|
||||
|
||||
mcpAPIHandler := mcp_v1.NewMux(
|
||||
cfg.Logger.Named("mcp.v1"),
|
||||
cfg.Probo,
|
||||
cfg.Auth,
|
||||
cfg.Authz,
|
||||
mcp_v1.Config{
|
||||
Version: cfg.MCPConfig.Version,
|
||||
RequestTimeout: cfg.MCPConfig.RequestTimeout,
|
||||
MaxRequestSize: cfg.MCPConfig.MaxRequestSize,
|
||||
},
|
||||
)
|
||||
|
||||
slackAPIHandler := slack_v1.NewMux(
|
||||
cfg.Logger.Named("slack.v1"),
|
||||
cfg.Slack,
|
||||
cfg.Trust,
|
||||
)
|
||||
|
||||
return &Server{
|
||||
cfg: cfg,
|
||||
trustAPIHandler: trustAPIHandler,
|
||||
consoleAPIHandler: consoleAPIHandler,
|
||||
mcpAPIHandler: mcpAPIHandler,
|
||||
slackAPIHandler: slackAPIHandler,
|
||||
cfg: cfg,
|
||||
compliancePageHandler: trust_v1.NewMux(
|
||||
cfg.Logger.Named("trust.v1"),
|
||||
cfg.IAM,
|
||||
cfg.Trust,
|
||||
cfg.Cookie,
|
||||
),
|
||||
consoleHandler: console_v1.NewMux(
|
||||
cfg.Logger.Named("console.v1"),
|
||||
cfg.Probo,
|
||||
cfg.IAM,
|
||||
cfg.Cookie,
|
||||
cfg.TokenSecret,
|
||||
cfg.ConnectorRegistry,
|
||||
cfg.BaseURL,
|
||||
cfg.CustomDomainCname,
|
||||
),
|
||||
mcpHandler: mcp_v1.NewMux(
|
||||
cfg.Logger.Named("mcp.v1"),
|
||||
cfg.Probo,
|
||||
cfg.IAM,
|
||||
),
|
||||
slackHandler: slack_v1.NewMux(
|
||||
cfg.Logger.Named("slack.v1"),
|
||||
cfg.Slack,
|
||||
cfg.Trust,
|
||||
),
|
||||
connectHandler: connect_v1.NewMux(
|
||||
cfg.Logger.Named("connect.v1"),
|
||||
cfg.IAM,
|
||||
cfg.Cookie,
|
||||
cfg.BaseURL,
|
||||
),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) TrustAPIHandler() http.Handler {
|
||||
return s.trustAPIHandler
|
||||
func (s *Server) CompliancePageHandler() http.Handler {
|
||||
return s.compliancePageHandler
|
||||
}
|
||||
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -223,7 +157,7 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
AllowedOrigins: s.cfg.AllowedOrigins,
|
||||
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "HEAD"},
|
||||
AllowedHeaders: []string{"content-type", "traceparent", "authorization"},
|
||||
ExposedHeaders: []string{"x-Request-id"},
|
||||
ExposedHeaders: []string{"x-request-id"},
|
||||
AllowCredentials: true,
|
||||
MaxAge: 600, // 10 minutes (chrome >= 76 maximum value c.f. https://source.chromium.org/chromium/chromium/src/+/main:services/network/public/cpp/cors/preflight_result.cc;drc=52002151773d8cd9ffc5f557cd7cc880fddcae3e;l=36)
|
||||
OptionsPassthrough: false,
|
||||
@@ -243,10 +177,11 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
router.Use(cors.Handler(corsOpts))
|
||||
|
||||
router.Mount("/console/v1", s.consoleAPIHandler)
|
||||
router.Mount("/trust/v1", s.trustAPIHandler)
|
||||
router.Mount("/mcp/v1", s.mcpAPIHandler)
|
||||
router.Mount("/slack/v1", s.slackAPIHandler)
|
||||
router.Mount("/console/v1", s.consoleHandler)
|
||||
router.Mount("/connect/v1", s.connectHandler)
|
||||
router.Mount("/trust/v1", s.compliancePageHandler)
|
||||
router.Mount("/mcp/v1", s.mcpHandler)
|
||||
router.Mount("/slack/v1", s.slackHandler)
|
||||
|
||||
router.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
94
pkg/server/api/connect/v1/api_key_middleware.go
Normal file
94
pkg/server/api/connect/v1/api_key_middleware.go
Normal file
@@ -0,0 +1,94 @@
|
||||
// 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 connect_v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"go.gearno.de/kit/httpserver"
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/iam"
|
||||
"go.probo.inc/probo/pkg/securetoken"
|
||||
)
|
||||
|
||||
var (
|
||||
apiKeyContextKey = &ctxKey{name: "api_key"}
|
||||
)
|
||||
|
||||
func APIKeyFromContext(ctx context.Context) *coredata.UserAPIKey {
|
||||
apiKey, _ := ctx.Value(apiKeyContextKey).(*coredata.UserAPIKey)
|
||||
return apiKey
|
||||
}
|
||||
|
||||
func NewAPIKeyMiddleware(svc *iam.Service) 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 := SessionFromContext(ctx)
|
||||
if session != nil {
|
||||
httpserver.RenderError(w, http.StatusBadRequest, errors.New("api key authentication cannot be used with session authentication"))
|
||||
return
|
||||
}
|
||||
|
||||
tokenValue, err := securetoken.Get(r, "")
|
||||
if err != nil {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
keyID, err := gid.ParseGID(tokenValue)
|
||||
if err != nil {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
apiKey, err := svc.APIKeyService.GetAPIKey(ctx, keyID)
|
||||
if err != nil {
|
||||
var errUserAPIKeyNotFound *iam.ErrUserAPIKeyNotFound
|
||||
var errUserAPIKeyExpired *iam.ErrUserAPIKeyExpired
|
||||
|
||||
if errors.As(err, &errUserAPIKeyNotFound) || errors.As(err, &errUserAPIKeyExpired) {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
panic(fmt.Errorf("cannot get user API key: %w", err))
|
||||
}
|
||||
|
||||
user, err := svc.AccountService.GetIdentity(ctx, apiKey.UserID)
|
||||
if err != nil {
|
||||
var errUserNotFound *iam.ErrUserNotFound
|
||||
if errors.As(err, &errUserNotFound) {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
panic(fmt.Errorf("cannot get user: %w", err))
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, apiKeyContextKey, apiKey)
|
||||
ctx = context.WithValue(ctx, identityContextKey, user)
|
||||
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
19
pkg/server/api/connect/v1/ctxkey.go
Normal file
19
pkg/server/api/connect/v1/ctxkey.go
Normal file
@@ -0,0 +1,19 @@
|
||||
// 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 connect_v1
|
||||
|
||||
type (
|
||||
ctxKey struct{ name string }
|
||||
)
|
||||
36
pkg/server/api/connect/v1/gqlgen.yaml
Normal file
36
pkg/server/api/connect/v1/gqlgen.yaml
Normal file
@@ -0,0 +1,36 @@
|
||||
schema: ["schema.graphql"]
|
||||
|
||||
exec:
|
||||
filename: "schema/schema.go"
|
||||
package: "schema"
|
||||
|
||||
model:
|
||||
filename: "types/types.go"
|
||||
package: "types"
|
||||
|
||||
resolver:
|
||||
layout: "follow-schema"
|
||||
dir: "."
|
||||
package: "connect_v1"
|
||||
filename_template: "v1_resolver.go"
|
||||
|
||||
autobind: []
|
||||
call_argument_directives_with_null: true
|
||||
|
||||
directives:
|
||||
mustBeAuthorized:
|
||||
skip_runtime: false
|
||||
|
||||
models:
|
||||
ID:
|
||||
model:
|
||||
- "go.probo.inc/probo/pkg/server/gqlutils/types/gid.GIDScalar"
|
||||
Datetime:
|
||||
model:
|
||||
- "github.com/99designs/gqlgen/graphql.Time"
|
||||
CursorKey:
|
||||
model:
|
||||
- "go.probo.inc/probo/pkg/server/gqlutils/types/cursor.CursorKeyScalar"
|
||||
EmailAddr:
|
||||
model:
|
||||
- "go.probo.inc/probo/pkg/server/gqlutils/types/mail.AddrScalar"
|
||||
102
pkg/server/api/connect/v1/graphql_handler.go
Normal file
102
pkg/server/api/connect/v1/graphql_handler.go
Normal file
@@ -0,0 +1,102 @@
|
||||
// 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 connect_v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
"github.com/vektah/gqlparser/v2/gqlerror"
|
||||
"go.gearno.de/kit/log"
|
||||
"go.probo.inc/probo/pkg/iam"
|
||||
"go.probo.inc/probo/pkg/securecookie"
|
||||
"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"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrForbidden = &gqlerror.Error{
|
||||
Message: "You are not authorized to access this resource",
|
||||
Extensions: map[string]any{
|
||||
"code": "FORBIDDEN",
|
||||
},
|
||||
}
|
||||
|
||||
ErrUnauthorized = &gqlerror.Error{
|
||||
Message: "You are not authorized to access this resource",
|
||||
Extensions: map[string]any{
|
||||
"code": "UNAUTHORIZED",
|
||||
},
|
||||
}
|
||||
|
||||
ErrAlreadyAuthenticated = &gqlerror.Error{
|
||||
Message: "authentication not allowed for this resource/action",
|
||||
Extensions: map[string]any{
|
||||
"code": "ALREADY_AUTHENTICATED",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func SessionDirective(ctx context.Context, obj any, next graphql.Resolver, required types.SessionRequirement) (any, error) {
|
||||
session := SessionFromContext(ctx)
|
||||
|
||||
switch required {
|
||||
case types.SessionRequirementOptional:
|
||||
case types.SessionRequirementPresent:
|
||||
if session == nil {
|
||||
return nil, ErrUnauthorized
|
||||
}
|
||||
case types.SessionRequirementNone:
|
||||
if session != nil {
|
||||
return nil, ErrAlreadyAuthenticated
|
||||
}
|
||||
}
|
||||
|
||||
return next(ctx)
|
||||
}
|
||||
|
||||
func IsViewerDirective(ctx context.Context, obj any, next graphql.Resolver) (any, error) {
|
||||
identity := UserFromContext(ctx)
|
||||
resolvedIdentity, ok := obj.(*types.Identity)
|
||||
if !ok {
|
||||
panic(fmt.Errorf("@isViewer called on non-identity object: %T", obj))
|
||||
}
|
||||
|
||||
if identity.ID != resolvedIdentity.ID {
|
||||
return nil, ErrForbidden
|
||||
}
|
||||
|
||||
return next(ctx)
|
||||
}
|
||||
|
||||
func NewGraphQLHandler(svc *iam.Service, logger *log.Logger, cookieConfig securecookie.Config) http.Handler {
|
||||
config := schema.Config{
|
||||
Resolvers: &Resolver{
|
||||
iam: svc,
|
||||
cookieConfig: cookieConfig,
|
||||
},
|
||||
Directives: schema.DirectiveRoot{
|
||||
Session: SessionDirective,
|
||||
IsViewer: IsViewerDirective,
|
||||
},
|
||||
}
|
||||
|
||||
es := schema.NewExecutableSchema(config)
|
||||
gqlh := gqlutils.NewHandler(es, logger)
|
||||
return gqlh
|
||||
}
|
||||
51
pkg/server/api/connect/v1/httpctx.go
Normal file
51
pkg/server/api/connect/v1/httpctx.go
Normal file
@@ -0,0 +1,51 @@
|
||||
// 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 connect_v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var (
|
||||
httpResponseWriterKey = &ctxKey{name: "http_response_writer"}
|
||||
httpRequestKey = &ctxKey{name: "http_request"}
|
||||
)
|
||||
|
||||
func HTTPContextMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := WithHTTPContext(r.Context(), w, r)
|
||||
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func WithHTTPContext(ctx context.Context, w http.ResponseWriter, r *http.Request) context.Context {
|
||||
|
||||
ctx = context.WithValue(ctx, httpResponseWriterKey, w)
|
||||
ctx = context.WithValue(ctx, httpRequestKey, r)
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
func HTTPResponseWriterFromContext(ctx context.Context) http.ResponseWriter {
|
||||
return ctx.Value(httpResponseWriterKey).(http.ResponseWriter)
|
||||
}
|
||||
|
||||
func HTTPRequestFromContext(ctx context.Context) *http.Request {
|
||||
return ctx.Value(httpRequestKey).(*http.Request)
|
||||
}
|
||||
66
pkg/server/api/connect/v1/resolver.go
Normal file
66
pkg/server/api/connect/v1/resolver.go
Normal file
@@ -0,0 +1,66 @@
|
||||
//go:generate go run github.com/99designs/gqlgen generate
|
||||
|
||||
// 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 connect_v1
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"go.gearno.de/kit/log"
|
||||
"go.probo.inc/probo/pkg/baseurl"
|
||||
"go.probo.inc/probo/pkg/iam"
|
||||
"go.probo.inc/probo/pkg/securecookie"
|
||||
)
|
||||
|
||||
type (
|
||||
Resolver struct {
|
||||
iam *iam.Service
|
||||
cookieConfig securecookie.Config
|
||||
}
|
||||
)
|
||||
|
||||
func (r *Resolver) sessionCookieConfig(maxAge time.Duration) securecookie.Config {
|
||||
return securecookie.Config{
|
||||
Name: r.cookieConfig.Name,
|
||||
Secret: r.cookieConfig.Secret,
|
||||
Secure: r.cookieConfig.Secure,
|
||||
HTTPOnly: r.cookieConfig.HTTPOnly,
|
||||
SameSite: r.cookieConfig.SameSite,
|
||||
Path: r.cookieConfig.Path,
|
||||
Domain: r.cookieConfig.Domain,
|
||||
MaxAge: int(maxAge.Seconds()),
|
||||
}
|
||||
}
|
||||
|
||||
func NewMux(logger *log.Logger, svc *iam.Service, cookieConfig securecookie.Config, baseURL *baseurl.BaseURL) *chi.Mux {
|
||||
r := chi.NewMux()
|
||||
|
||||
r.Use(HTTPContextMiddleware)
|
||||
|
||||
sessionMiddleware := NewSessionMiddleware(svc, cookieConfig)
|
||||
graphqlHandler := NewGraphQLHandler(svc, logger, cookieConfig)
|
||||
samlHandler := NewSAMLHandler(svc, cookieConfig, baseURL)
|
||||
|
||||
router := r.With(sessionMiddleware)
|
||||
|
||||
router.Handle("/graphql", graphqlHandler)
|
||||
router.Get("/saml/2.0/metadata", samlHandler.MetadataHandler)
|
||||
router.Post("/saml/2.0/consume", samlHandler.ConsumeHandler)
|
||||
router.Get("/saml/2.0/{samlConfigID}", samlHandler.LoginHandler)
|
||||
|
||||
return r
|
||||
}
|
||||
96
pkg/server/api/connect/v1/saml_handler.go
Normal file
96
pkg/server/api/connect/v1/saml_handler.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package connect_v1
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"go.gearno.de/kit/httpserver"
|
||||
"go.probo.inc/probo/pkg/baseurl"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/iam"
|
||||
"go.probo.inc/probo/pkg/securecookie"
|
||||
)
|
||||
|
||||
type SAMLHandler struct {
|
||||
iam *iam.Service
|
||||
cookieConfig securecookie.Config
|
||||
baseURL *baseurl.BaseURL
|
||||
}
|
||||
|
||||
func NewSAMLHandler(iam *iam.Service, cookieConfig securecookie.Config, baseURL *baseurl.BaseURL) *SAMLHandler {
|
||||
return &SAMLHandler{iam: iam, cookieConfig: cookieConfig, baseURL: baseURL}
|
||||
}
|
||||
|
||||
func (h *SAMLHandler) MetadataHandler(w http.ResponseWriter, r *http.Request) {
|
||||
metadataXML, err := h.iam.SAMLService.GenerateSpMetadata()
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot generate metadata: %w", err))
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/samlmetadata+xml")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(metadataXML)
|
||||
}
|
||||
|
||||
func (h *SAMLHandler) ConsumeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
httpserver.RenderError(w, http.StatusBadRequest, errors.New("cannot parse form"))
|
||||
return
|
||||
}
|
||||
|
||||
samlResponse := r.FormValue("SAMLResponse")
|
||||
relayState := r.FormValue("RelayState")
|
||||
|
||||
configID, err := gid.ParseGID(relayState)
|
||||
if err != nil {
|
||||
httpserver.RenderError(w, http.StatusBadRequest, errors.New("invalid relay state"))
|
||||
return
|
||||
}
|
||||
|
||||
user, membership, err := h.iam.SAMLService.HandleAssertion(ctx, samlResponse, configID)
|
||||
if err != nil {
|
||||
httpserver.RenderError(w, http.StatusUnauthorized, err)
|
||||
return
|
||||
}
|
||||
|
||||
session := SessionFromContext(ctx)
|
||||
if session == nil {
|
||||
h.iam.AuthService.OpenSessionWithoutPassword(ctx, user.ID, membership.OrganizationID)
|
||||
}
|
||||
|
||||
// TODO open or update the organization session
|
||||
|
||||
securecookie.Set(w, h.cookieConfig, session.ID.String())
|
||||
|
||||
redirectURL := h.baseURL.WithPath("/organizations/" + membership.OrganizationID.String()).MustString()
|
||||
|
||||
http.Redirect(w, r, redirectURL, http.StatusFound)
|
||||
}
|
||||
|
||||
func (h *SAMLHandler) LoginHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
samlConfigIDParam := chi.URLParam(r, "samlConfigID")
|
||||
if samlConfigIDParam == "" {
|
||||
httpserver.RenderError(w, http.StatusBadRequest, errors.New("missing SAML config ID"))
|
||||
return
|
||||
}
|
||||
|
||||
samlConfigID, err := gid.ParseGID(samlConfigIDParam)
|
||||
if err != nil {
|
||||
httpserver.RenderError(w, http.StatusBadRequest, errors.New("invalid SAML config ID"))
|
||||
return
|
||||
}
|
||||
|
||||
url, err := h.iam.SAMLService.InitiateLogin(ctx, samlConfigID)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot initiate SAML login: %w", err))
|
||||
}
|
||||
|
||||
http.Redirect(w, r, url.String(), http.StatusFound)
|
||||
}
|
||||
769
pkg/server/api/connect/v1/schema.graphql
Normal file
769
pkg/server/api/connect/v1/schema.graphql
Normal file
@@ -0,0 +1,769 @@
|
||||
directive @goField(
|
||||
forceResolver: Boolean
|
||||
name: String
|
||||
omittable: Boolean
|
||||
) on INPUT_FIELD_DEFINITION | FIELD_DEFINITION
|
||||
|
||||
directive @goModel(
|
||||
model: String
|
||||
models: [String!]
|
||||
) on OBJECT | INPUT_OBJECT | SCALAR | ENUM | INTERFACE | UNION
|
||||
|
||||
directive @goEnum(value: String) on ENUM_VALUE
|
||||
|
||||
directive @session(required: SessionRequirement!) on FIELD_DEFINITION
|
||||
|
||||
directive @isViewer on FIELD_DEFINITION
|
||||
|
||||
scalar CursorKey
|
||||
scalar Datetime
|
||||
scalar Upload
|
||||
scalar EmailAddr
|
||||
|
||||
enum SessionRequirement {
|
||||
PRESENT
|
||||
NONE
|
||||
OPTIONAL
|
||||
}
|
||||
|
||||
enum OrderDirection
|
||||
@goModel(model: "go.probo.inc/probo/pkg/page.OrderDirection") {
|
||||
ASC @goEnum(value: "go.probo.inc/probo/pkg/page.OrderDirectionAsc")
|
||||
DESC @goEnum(value: "go.probo.inc/probo/pkg/page.OrderDirectionDesc")
|
||||
}
|
||||
|
||||
enum SessionOrderField
|
||||
@goModel(model: "go.probo.inc/probo/pkg/coredata.SessionOrderField") {
|
||||
CREATED_AT
|
||||
@goEnum(value: "go.probo.inc/probo/pkg/coredata.SessionOrderFieldCreatedAt")
|
||||
EXPIRED_AT
|
||||
@goEnum(value: "go.probo.inc/probo/pkg/coredata.SessionOrderFieldExpiredAt")
|
||||
UPDATED_AT
|
||||
@goEnum(value: "go.probo.inc/probo/pkg/coredata.SessionOrderFieldUpdatedAt")
|
||||
}
|
||||
|
||||
input SessionOrder {
|
||||
direction: OrderDirection!
|
||||
field: SessionOrderField!
|
||||
}
|
||||
|
||||
interface Node {
|
||||
id: ID!
|
||||
}
|
||||
|
||||
type Query {
|
||||
node(id: ID!): Node @session(required: PRESENT)
|
||||
viewer: Identity @session(required: PRESENT)
|
||||
checkSSOAvailability(email: String!): SSOAvailability!
|
||||
@session(required: NONE)
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
signIn(input: SignInInput!): SignInPayload! @session(required: NONE)
|
||||
signUp(input: SignUpInput!): SignUpPayload! @session(required: NONE)
|
||||
signOut: SignOutPayload! @session(required: PRESENT)
|
||||
signUpFromInvitation(
|
||||
input: SignUpFromInvitationInput!
|
||||
): SignUpFromInvitationPayload! @session(required: NONE)
|
||||
forgotPassword(input: ForgotPasswordInput!): ForgotPasswordPayload!
|
||||
@session(required: NONE)
|
||||
resetPassword(input: ResetPasswordInput!): ResetPasswordPayload!
|
||||
@session(required: NONE)
|
||||
verifyEmail(input: VerifyEmailInput!): VerifyEmailPayload!
|
||||
@session(required: OPTIONAL)
|
||||
changePassword(input: ChangePasswordInput!): ChangePasswordPayload!
|
||||
@session(required: PRESENT)
|
||||
changeEmail(input: ChangeEmailInput!): ChangeEmailPayload!
|
||||
@session(required: PRESENT)
|
||||
|
||||
updateIdentityProfile(
|
||||
input: UpdateIdentityProfileInput!
|
||||
): UpdateIdentityProfilePayload! @session(required: PRESENT)
|
||||
|
||||
revokeSession(input: RevokeSessionInput!): RevokeSessionPayload!
|
||||
@session(required: PRESENT)
|
||||
revokeAllSessions: RevokeAllSessionsPayload! @session(required: PRESENT)
|
||||
|
||||
createPersonalAPIKey(
|
||||
input: CreatePersonalAPIKeyInput!
|
||||
): CreatePersonalAPIKeyPayload! @session(required: PRESENT)
|
||||
updatePersonalAPIKey(
|
||||
input: UpdatePersonalAPIKeyInput!
|
||||
): UpdatePersonalAPIKeyPayload! @session(required: PRESENT)
|
||||
revokePersonalAPIKey(
|
||||
input: RevokePersonalAPIKeyInput!
|
||||
): RevokePersonalAPIKeyPayload! @session(required: PRESENT)
|
||||
|
||||
createOrganization(
|
||||
input: CreateOrganizationInput!
|
||||
): CreateOrganizationPayload! @session(required: PRESENT)
|
||||
updateOrganization(
|
||||
input: UpdateOrganizationInput!
|
||||
): UpdateOrganizationPayload! @session(required: PRESENT)
|
||||
deleteOrganization(
|
||||
input: DeleteOrganizationInput!
|
||||
): DeleteOrganizationPayload! @session(required: PRESENT)
|
||||
|
||||
inviteMember(input: InviteMemberInput!): InviteMemberPayload!
|
||||
@session(required: PRESENT)
|
||||
deleteInvitation(input: DeleteInvitationInput!): DeleteInvitationPayload!
|
||||
@session(required: PRESENT)
|
||||
removeMember(input: RemoveMemberInput!): RemoveMemberPayload!
|
||||
@session(required: PRESENT)
|
||||
|
||||
acceptInvitation(input: AcceptInvitationInput!): AcceptInvitationPayload!
|
||||
@session(required: PRESENT)
|
||||
|
||||
createSAMLConfiguration(
|
||||
input: CreateSAMLConfigurationInput!
|
||||
): CreateSAMLConfigurationPayload! @session(required: PRESENT)
|
||||
updateSAMLConfiguration(
|
||||
input: UpdateSAMLConfigurationInput!
|
||||
): UpdateSAMLConfigurationPayload! @session(required: PRESENT)
|
||||
deleteSAMLConfiguration(
|
||||
input: DeleteSAMLConfigurationInput!
|
||||
): DeleteSAMLConfigurationPayload! @session(required: PRESENT)
|
||||
}
|
||||
|
||||
type Identity implements Node {
|
||||
id: ID!
|
||||
email: EmailAddr!
|
||||
emailVerified: Boolean!
|
||||
createdAt: Datetime!
|
||||
updatedAt: Datetime!
|
||||
|
||||
memberships(
|
||||
first: Int
|
||||
after: CursorKey
|
||||
last: Int
|
||||
before: CursorKey
|
||||
): MembershipConnection! @goField(forceResolver: true) @isViewer
|
||||
|
||||
pendingInvitations(
|
||||
first: Int
|
||||
after: CursorKey
|
||||
last: Int
|
||||
before: CursorKey
|
||||
): InvitationConnection! @goField(forceResolver: true) @isViewer
|
||||
|
||||
sessions(
|
||||
first: Int
|
||||
after: CursorKey
|
||||
last: Int
|
||||
before: CursorKey
|
||||
orderBy: SessionOrder
|
||||
): SessionConnection! @goField(forceResolver: true) @isViewer
|
||||
|
||||
personalAPIKeys(
|
||||
first: Int
|
||||
after: CursorKey
|
||||
last: Int
|
||||
before: CursorKey
|
||||
): PersonalAPIKeyConnection! @goField(forceResolver: true) @isViewer
|
||||
|
||||
profileFor(organizationId: ID!): IdentityProfile @isViewer
|
||||
}
|
||||
|
||||
type IdentityProfile implements Node {
|
||||
id: ID!
|
||||
displayName: String!
|
||||
firstName: String
|
||||
lastName: String
|
||||
jobTitle: String
|
||||
department: String
|
||||
phoneNumber: String
|
||||
avatarUrl: String
|
||||
manager: IdentityProfile
|
||||
timezone: String
|
||||
locale: String
|
||||
customAttributes: [CustomAttribute!]!
|
||||
provisionedBy: ProvisioningSource!
|
||||
externalId: String
|
||||
identity: Identity!
|
||||
organization: Organization!
|
||||
createdAt: Datetime!
|
||||
updatedAt: Datetime!
|
||||
}
|
||||
|
||||
type CustomAttribute {
|
||||
key: String!
|
||||
value: String!
|
||||
}
|
||||
|
||||
type Organization implements Node {
|
||||
id: ID!
|
||||
name: String!
|
||||
logoUrl: String @goField(forceResolver: true)
|
||||
horizontalLogoUrl: String @goField(forceResolver: true)
|
||||
createdAt: Datetime!
|
||||
updatedAt: Datetime!
|
||||
|
||||
members(
|
||||
first: Int
|
||||
after: CursorKey
|
||||
last: Int
|
||||
before: CursorKey
|
||||
): MembershipConnection! @goField(forceResolver: true)
|
||||
|
||||
invitations(
|
||||
first: Int
|
||||
after: CursorKey
|
||||
last: Int
|
||||
before: CursorKey
|
||||
status: InvitationStatus
|
||||
): InvitationConnection! @goField(forceResolver: true)
|
||||
|
||||
samlConfigurations(
|
||||
first: Int
|
||||
after: CursorKey
|
||||
last: Int
|
||||
before: CursorKey
|
||||
): SAMLConfigurationConnection! @goField(forceResolver: true)
|
||||
|
||||
availableApplications: [Application!]!
|
||||
}
|
||||
|
||||
type Membership implements Node {
|
||||
id: ID!
|
||||
createdAt: Datetime!
|
||||
profile: IdentityProfile!
|
||||
identity: Identity! @goField(forceResolver: true)
|
||||
organization: Organization! @goField(forceResolver: true)
|
||||
permissions: [Permission!]!
|
||||
provisionedBy: ProvisioningSource!
|
||||
active: Boolean!
|
||||
lastSyncedAt: Datetime
|
||||
}
|
||||
|
||||
type Invitation implements Node {
|
||||
id: ID!
|
||||
email: EmailAddr!
|
||||
expiresAt: Datetime!
|
||||
acceptedAt: Datetime
|
||||
createdAt: Datetime!
|
||||
status: InvitationStatus!
|
||||
}
|
||||
|
||||
type InvitationProfile {
|
||||
displayName: String!
|
||||
firstName: String
|
||||
lastName: String
|
||||
jobTitle: String
|
||||
department: String
|
||||
}
|
||||
|
||||
type Session implements Node {
|
||||
id: ID!
|
||||
ipAddress: String!
|
||||
userAgent: String!
|
||||
updatedAt: Datetime!
|
||||
createdAt: Datetime!
|
||||
expiresAt: Datetime!
|
||||
}
|
||||
|
||||
type PersonalAPIKey implements Node {
|
||||
id: ID!
|
||||
name: String!
|
||||
lastUsedAt: Datetime
|
||||
expiresAt: Datetime!
|
||||
createdAt: Datetime!
|
||||
scopes: [TokenScope!]!
|
||||
organizations: [Organization!]!
|
||||
}
|
||||
|
||||
type Permission implements Node {
|
||||
id: ID!
|
||||
createdAt: Datetime!
|
||||
application: Application!
|
||||
accessLevel: AccessLevel!
|
||||
organization: Organization!
|
||||
principalType: PrincipalType!
|
||||
principalId: ID!
|
||||
}
|
||||
|
||||
type PermissionGrant {
|
||||
application: Application!
|
||||
accessLevel: AccessLevel!
|
||||
}
|
||||
|
||||
type Application {
|
||||
id: ApplicationId!
|
||||
name: String!
|
||||
description: String!
|
||||
availableAccessLevels: [AccessLevel!]!
|
||||
}
|
||||
|
||||
type SessionPolicy {
|
||||
maxSessionDurationHours: Int!
|
||||
idleTimeoutMinutes: Int!
|
||||
maxConcurrentSessions: Int
|
||||
requireReauthForSensitiveActions: Boolean!
|
||||
}
|
||||
|
||||
type SAMLConfiguration implements Node {
|
||||
id: ID!
|
||||
emailDomain: String!
|
||||
enabled: Boolean!
|
||||
enforcementPolicy: SAMLEnforcementPolicy!
|
||||
domainVerified: Boolean!
|
||||
domainVerifiedAt: Datetime
|
||||
domainVerificationToken: String
|
||||
idpEntityId: String!
|
||||
idpSsoUrl: String!
|
||||
idpCertificate: String!
|
||||
autoSignupEnabled: Boolean!
|
||||
createdAt: Datetime!
|
||||
updatedAt: Datetime!
|
||||
spMetadataUrl: String!
|
||||
testLoginUrl: String!
|
||||
attributeMappings: SAMLAttributeMappings!
|
||||
defaultPermissions: [PermissionGrant!]!
|
||||
}
|
||||
|
||||
type SAMLAttributeMappings {
|
||||
email: String!
|
||||
firstName: String!
|
||||
lastName: String!
|
||||
role: String!
|
||||
}
|
||||
|
||||
type SSOAvailability {
|
||||
available: Boolean!
|
||||
samlConfigId: ID
|
||||
organizationId: ID
|
||||
}
|
||||
|
||||
enum ApplicationId {
|
||||
CONSOLE
|
||||
COMPLIANCE
|
||||
RISK
|
||||
VENDOR
|
||||
DOCUMENTS
|
||||
TRUST_CENTER
|
||||
SETTINGS
|
||||
API
|
||||
}
|
||||
|
||||
enum AccessLevel {
|
||||
READ
|
||||
WRITE
|
||||
ADMIN
|
||||
}
|
||||
|
||||
enum PrincipalType {
|
||||
IDENTITY
|
||||
SERVICE_ACCOUNT
|
||||
}
|
||||
|
||||
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 SAMLEnforcementPolicy
|
||||
@goModel(model: "go.probo.inc/probo/pkg/coredata.SAMLEnforcementPolicy") {
|
||||
OFF @goEnum(value: "go.probo.inc/probo/pkg/coredata.SAMLEnforcementPolicyOff")
|
||||
OPTIONAL
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.SAMLEnforcementPolicyOptional"
|
||||
)
|
||||
REQUIRED
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.SAMLEnforcementPolicyRequired"
|
||||
)
|
||||
}
|
||||
|
||||
enum AuthMethod {
|
||||
PASSWORD
|
||||
SAML
|
||||
RECOVERY_CODE
|
||||
}
|
||||
|
||||
enum TokenScope {
|
||||
READ_ORGANIZATION
|
||||
WRITE_ORGANIZATION
|
||||
READ_COMPLIANCE
|
||||
WRITE_COMPLIANCE
|
||||
READ_RISK
|
||||
WRITE_RISK
|
||||
READ_VENDOR
|
||||
WRITE_VENDOR
|
||||
READ_DOCUMENTS
|
||||
WRITE_DOCUMENTS
|
||||
READ_TRUST_CENTER
|
||||
WRITE_TRUST_CENTER
|
||||
ADMIN
|
||||
}
|
||||
|
||||
enum ProvisioningSource {
|
||||
MANUAL
|
||||
INVITATION
|
||||
SAML
|
||||
}
|
||||
|
||||
type MembershipConnection
|
||||
@goModel(
|
||||
model: "go.probo.inc/probo/pkg/server/api/connect/v1/types.MembershipConnection"
|
||||
) {
|
||||
edges: [MembershipEdge!]!
|
||||
pageInfo: PageInfo!
|
||||
totalCount: Int! @goField(forceResolver: true)
|
||||
}
|
||||
|
||||
type MembershipEdge {
|
||||
node: Membership!
|
||||
cursor: CursorKey!
|
||||
}
|
||||
|
||||
type InvitationConnection
|
||||
@goModel(
|
||||
model: "go.probo.inc/probo/pkg/server/api/connect/v1/types.InvitationConnection"
|
||||
) {
|
||||
edges: [InvitationEdge!]!
|
||||
pageInfo: PageInfo!
|
||||
totalCount: Int! @goField(forceResolver: true)
|
||||
}
|
||||
|
||||
type InvitationEdge {
|
||||
node: Invitation!
|
||||
cursor: CursorKey!
|
||||
}
|
||||
|
||||
type SessionConnection
|
||||
@goModel(
|
||||
model: "go.probo.inc/probo/pkg/server/api/connect/v1/types.SessionConnection"
|
||||
) {
|
||||
edges: [SessionEdge!]!
|
||||
pageInfo: PageInfo!
|
||||
totalCount: Int! @goField(forceResolver: true)
|
||||
}
|
||||
|
||||
type SessionEdge {
|
||||
node: Session!
|
||||
cursor: CursorKey!
|
||||
}
|
||||
|
||||
type PersonalAPIKeyConnection
|
||||
@goModel(
|
||||
model: "go.probo.inc/probo/pkg/server/api/connect/v1/types.PersonalAPIKeyConnection"
|
||||
) {
|
||||
edges: [PersonalAPIKeyEdge!]!
|
||||
pageInfo: PageInfo!
|
||||
totalCount: Int! @goField(forceResolver: true)
|
||||
}
|
||||
|
||||
type PersonalAPIKeyEdge {
|
||||
node: PersonalAPIKey!
|
||||
cursor: CursorKey!
|
||||
}
|
||||
|
||||
type SAMLConfigurationConnection
|
||||
@goModel(
|
||||
model: "go.probo.inc/probo/pkg/server/api/connect/v1/types.SAMLConfigurationConnection"
|
||||
) {
|
||||
edges: [SAMLConfigurationEdge!]!
|
||||
pageInfo: PageInfo!
|
||||
totalCount: Int! @goField(forceResolver: true)
|
||||
}
|
||||
|
||||
type SAMLConfigurationEdge {
|
||||
node: SAMLConfiguration!
|
||||
cursor: CursorKey!
|
||||
}
|
||||
|
||||
type PageInfo {
|
||||
hasNextPage: Boolean!
|
||||
hasPreviousPage: Boolean!
|
||||
startCursor: CursorKey
|
||||
endCursor: CursorKey
|
||||
}
|
||||
|
||||
input SignInInput {
|
||||
email: EmailAddr!
|
||||
password: String!
|
||||
}
|
||||
|
||||
input SignUpInput {
|
||||
email: EmailAddr!
|
||||
password: String!
|
||||
fullName: String!
|
||||
}
|
||||
|
||||
input SignUpFromInvitationInput {
|
||||
token: String!
|
||||
password: String!
|
||||
}
|
||||
|
||||
input ForgotPasswordInput {
|
||||
email: EmailAddr!
|
||||
}
|
||||
|
||||
input ResetPasswordInput {
|
||||
token: String!
|
||||
password: String!
|
||||
}
|
||||
|
||||
input VerifyEmailInput {
|
||||
token: String!
|
||||
}
|
||||
|
||||
input ChangePasswordInput {
|
||||
currentPassword: String!
|
||||
newPassword: String!
|
||||
}
|
||||
|
||||
input ChangeEmailInput {
|
||||
newEmail: EmailAddr!
|
||||
password: String!
|
||||
}
|
||||
|
||||
input DeactivateAccountInput {
|
||||
password: String!
|
||||
}
|
||||
|
||||
input DeleteAccountInput {
|
||||
password: String!
|
||||
confirmation: String!
|
||||
}
|
||||
|
||||
input UpdateIdentityProfileInput {
|
||||
membershipId: ID!
|
||||
displayName: String
|
||||
firstName: String
|
||||
lastName: String
|
||||
jobTitle: String
|
||||
department: String
|
||||
phoneNumber: String
|
||||
timezone: String
|
||||
locale: String
|
||||
}
|
||||
|
||||
input RevokeSessionInput {
|
||||
sessionId: ID!
|
||||
}
|
||||
|
||||
input CreatePersonalAPIKeyInput {
|
||||
name: String!
|
||||
expiresAt: Datetime!
|
||||
organizationIds: [ID!]!
|
||||
}
|
||||
|
||||
input UpdatePersonalAPIKeyInput {
|
||||
tokenId: ID!
|
||||
name: String
|
||||
description: String
|
||||
}
|
||||
|
||||
input RevokePersonalAPIKeyInput {
|
||||
tokenId: ID!
|
||||
}
|
||||
|
||||
input CreateOrganizationInput {
|
||||
name: String!
|
||||
logoFile: Upload
|
||||
horizontalLogoFile: Upload
|
||||
}
|
||||
|
||||
input UpdateOrganizationInput {
|
||||
organizationId: ID!
|
||||
name: String
|
||||
logoFile: Upload @goField(omittable: true)
|
||||
horizontalLogoFile: Upload @goField(omittable: true)
|
||||
}
|
||||
|
||||
input DeleteOrganizationInput {
|
||||
organizationId: ID!
|
||||
}
|
||||
|
||||
input SessionPolicyInput {
|
||||
maxSessionDurationHours: Int
|
||||
idleTimeoutMinutes: Int
|
||||
maxConcurrentSessions: Int
|
||||
requireReauthForSensitiveActions: Boolean
|
||||
}
|
||||
|
||||
input AddIPAllowlistEntryInput {
|
||||
organizationId: ID!
|
||||
cidr: String!
|
||||
description: String
|
||||
}
|
||||
|
||||
input RemoveIPAllowlistEntryInput {
|
||||
entryId: ID!
|
||||
}
|
||||
|
||||
input InviteMemberInput {
|
||||
organizationId: ID!
|
||||
email: EmailAddr!
|
||||
fullName: String!
|
||||
}
|
||||
|
||||
input RemoveMemberInput {
|
||||
organizationId: ID!
|
||||
membershipId: ID!
|
||||
}
|
||||
|
||||
input InvitationProfileInput {
|
||||
displayName: String!
|
||||
firstName: String
|
||||
lastName: String
|
||||
jobTitle: String
|
||||
department: String
|
||||
}
|
||||
|
||||
input AcceptInvitationInput {
|
||||
invitationId: ID!
|
||||
}
|
||||
|
||||
input DeleteInvitationInput {
|
||||
organizationId: ID!
|
||||
invitationId: ID!
|
||||
}
|
||||
|
||||
input CreateSAMLConfigurationInput {
|
||||
organizationId: ID!
|
||||
emailDomain: String!
|
||||
idpEntityId: String!
|
||||
idpSsoUrl: String!
|
||||
idpCertificate: String!
|
||||
autoSignupEnabled: Boolean!
|
||||
attributeMappings: SAMLAttributeMappingsInput
|
||||
}
|
||||
|
||||
input SAMLAttributeMappingsInput {
|
||||
email: String
|
||||
firstName: String
|
||||
lastName: String
|
||||
role: String
|
||||
}
|
||||
|
||||
input UpdateSAMLConfigurationInput {
|
||||
organizationId: ID!
|
||||
samlConfigurationId: ID!
|
||||
idpEntityId: String
|
||||
idpSsoUrl: String
|
||||
idpCertificate: String
|
||||
autoSignupEnabled: Boolean
|
||||
enforcementPolicy: SAMLEnforcementPolicy
|
||||
attributeMappings: SAMLAttributeMappingsInput
|
||||
}
|
||||
|
||||
input DeleteSAMLConfigurationInput {
|
||||
organizationId: ID!
|
||||
samlConfigurationId: ID!
|
||||
}
|
||||
|
||||
type SignInPayload {
|
||||
identity: Identity
|
||||
}
|
||||
|
||||
type SignUpPayload {
|
||||
identity: Identity
|
||||
}
|
||||
|
||||
type SignOutPayload {
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
type SignUpFromInvitationPayload {
|
||||
identity: Identity
|
||||
}
|
||||
|
||||
type ForgotPasswordPayload {
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
type ResetPasswordPayload {
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
type VerifyEmailPayload {
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
type ChangePasswordPayload {
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
type ChangeEmailPayload {
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
type DeactivateAccountPayload {
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
type DeleteAccountPayload {
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
type UpdateIdentityProfilePayload {
|
||||
profile: IdentityProfile
|
||||
}
|
||||
|
||||
type RevokeSessionPayload {
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
type RevokeAllSessionsPayload {
|
||||
revokedCount: Int!
|
||||
}
|
||||
|
||||
type CreatePersonalAPIKeyPayload {
|
||||
personalAPIKeyEdge: PersonalAPIKeyEdge!
|
||||
token: String!
|
||||
}
|
||||
|
||||
type UpdatePersonalAPIKeyPayload {
|
||||
personalAPIKey: PersonalAPIKey
|
||||
}
|
||||
|
||||
type RevokePersonalAPIKeyPayload {
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
type CreateOrganizationPayload {
|
||||
organization: Organization
|
||||
membershipEdge: MembershipEdge!
|
||||
}
|
||||
|
||||
type UpdateOrganizationPayload {
|
||||
organization: Organization
|
||||
}
|
||||
|
||||
type DeleteOrganizationPayload {
|
||||
deletedOrganizationId: ID!
|
||||
}
|
||||
|
||||
type InviteMemberPayload {
|
||||
invitationEdge: InvitationEdge!
|
||||
}
|
||||
|
||||
type RemoveMemberPayload {
|
||||
deletedMembershipId: ID!
|
||||
}
|
||||
|
||||
type AcceptInvitationPayload {
|
||||
membershipEdge: MembershipEdge!
|
||||
}
|
||||
|
||||
type DeleteInvitationPayload {
|
||||
deletedInvitationId: ID!
|
||||
}
|
||||
|
||||
type CreateSAMLConfigurationPayload {
|
||||
samlConfigurationEdge: SAMLConfigurationEdge!
|
||||
}
|
||||
|
||||
type UpdateSAMLConfigurationPayload {
|
||||
samlConfiguration: SAMLConfiguration
|
||||
}
|
||||
|
||||
type DeleteSAMLConfigurationPayload {
|
||||
deletedSamlConfigurationId: ID!
|
||||
}
|
||||
20067
pkg/server/api/connect/v1/schema/schema.go
Normal file
20067
pkg/server/api/connect/v1/schema/schema.go
Normal file
File diff suppressed because it is too large
Load Diff
123
pkg/server/api/connect/v1/session_middleware.go
Normal file
123
pkg/server/api/connect/v1/session_middleware.go
Normal file
@@ -0,0 +1,123 @@
|
||||
// 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 connect_v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"go.gearno.de/kit/httpserver"
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/iam"
|
||||
"go.probo.inc/probo/pkg/securecookie"
|
||||
)
|
||||
|
||||
var (
|
||||
identityContextKey = &ctxKey{name: "identity"}
|
||||
sessionContextKey = &ctxKey{name: "session"}
|
||||
)
|
||||
|
||||
func SessionFromContext(ctx context.Context) *coredata.Session {
|
||||
session, _ := ctx.Value(sessionContextKey).(*coredata.Session)
|
||||
return session
|
||||
}
|
||||
|
||||
func UserFromContext(ctx context.Context) *coredata.User {
|
||||
user, _ := ctx.Value(identityContextKey).(*coredata.User)
|
||||
return user
|
||||
}
|
||||
|
||||
func NewSessionMiddleware(svc *iam.Service, 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()
|
||||
|
||||
apiKey := APIKeyFromContext(ctx)
|
||||
if apiKey != nil {
|
||||
httpserver.RenderError(w, http.StatusBadRequest, errors.New("session authentication cannot be used with API key authentication"))
|
||||
return
|
||||
}
|
||||
|
||||
cookieValue, err := securecookie.Get(r, cookieConfig)
|
||||
if err != nil {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
sessionID, err := gid.ParseGID(cookieValue)
|
||||
if err != nil {
|
||||
securecookie.Clear(w, cookieConfig)
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
session, err := svc.SessionService.GetSession(ctx, sessionID)
|
||||
if err != nil {
|
||||
var errSessionNotFound *iam.ErrSessionNotFound
|
||||
var errSessionExpired *iam.ErrSessionExpired
|
||||
|
||||
if errors.As(err, &errSessionNotFound) || errors.As(err, &errSessionExpired) {
|
||||
securecookie.Clear(w, cookieConfig)
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
panic(fmt.Errorf("cannot get session: %w", err))
|
||||
}
|
||||
|
||||
user, err := svc.AccountService.GetIdentity(ctx, session.UserID)
|
||||
if err != nil {
|
||||
var errUserNotFound *iam.ErrUserNotFound
|
||||
if errors.As(err, &errUserNotFound) {
|
||||
securecookie.Clear(w, cookieConfig)
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
panic(fmt.Errorf("cannot get user: %w", err))
|
||||
}
|
||||
|
||||
userAgent := r.UserAgent()
|
||||
// TODO: will work well when no layer 7 proxy is in front of the server
|
||||
var ipAddress net.IP
|
||||
if host, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
|
||||
ipAddress = net.ParseIP(host)
|
||||
} else {
|
||||
ipAddress = net.ParseIP(r.RemoteAddr)
|
||||
}
|
||||
|
||||
err = svc.SessionService.UpdateSessionInfo(ctx, session.ID, userAgent, ipAddress)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot update session info: %w", err))
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, sessionContextKey, session)
|
||||
ctx = context.WithValue(ctx, identityContextKey, user)
|
||||
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
|
||||
err = svc.SessionService.UpdateSessionData(ctx, session.ID, session.Data)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot update session data: %w", err))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
27
pkg/server/api/connect/v1/types/identity.go
Normal file
27
pkg/server/api/connect/v1/types/identity.go
Normal file
@@ -0,0 +1,27 @@
|
||||
// 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 NewIdentity(identity *coredata.User) *Identity {
|
||||
return &Identity{
|
||||
ID: identity.ID,
|
||||
Email: identity.EmailAddress,
|
||||
EmailVerified: identity.EmailAddressVerified,
|
||||
CreatedAt: identity.CreatedAt,
|
||||
UpdatedAt: identity.UpdatedAt,
|
||||
}
|
||||
}
|
||||
@@ -21,14 +21,16 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
InvitationOrderBy OrderBy[coredata.InvitationOrderField]
|
||||
|
||||
InvitationConnection struct {
|
||||
TotalCount int `json:"totalCount"`
|
||||
Edges []*InvitationEdge `json:"edges"`
|
||||
PageInfo *PageInfo `json:"pageInfo"`
|
||||
TotalCount int
|
||||
Edges []*InvitationEdge
|
||||
PageInfo PageInfo
|
||||
|
||||
Resolver any
|
||||
ParentID gid.GID
|
||||
Filter *InvitationFilter
|
||||
Filters *coredata.InvitationFilter
|
||||
}
|
||||
)
|
||||
|
||||
@@ -36,39 +38,37 @@ func NewInvitationConnection(
|
||||
p *page.Page[*coredata.Invitation, coredata.InvitationOrderField],
|
||||
resolver any,
|
||||
parentID gid.GID,
|
||||
filter *InvitationFilter,
|
||||
filters *coredata.InvitationFilter,
|
||||
) *InvitationConnection {
|
||||
var edges = make([]*InvitationEdge, len(p.Data))
|
||||
|
||||
for i := range edges {
|
||||
edges[i] = NewInvitationEdge(p.Data[i], p.Cursor.OrderBy.Field)
|
||||
edges := make([]*InvitationEdge, len(p.Data))
|
||||
for i, invitation := range p.Data {
|
||||
edges[i] = NewInvitationEdge(invitation, p.Cursor.OrderBy.Field)
|
||||
}
|
||||
|
||||
return &InvitationConnection{
|
||||
Edges: edges,
|
||||
PageInfo: NewPageInfo(p),
|
||||
PageInfo: *NewPageInfo(p),
|
||||
|
||||
Resolver: resolver,
|
||||
ParentID: parentID,
|
||||
Filter: filter,
|
||||
Filters: filters,
|
||||
}
|
||||
}
|
||||
|
||||
func NewInvitationEdge(invitation *coredata.Invitation, orderBy coredata.InvitationOrderField) *InvitationEdge {
|
||||
func NewInvitationEdge(invitation *coredata.Invitation, orderField coredata.InvitationOrderField) *InvitationEdge {
|
||||
return &InvitationEdge{
|
||||
Cursor: invitation.CursorKey(orderBy),
|
||||
Node: NewInvitation(invitation),
|
||||
Cursor: invitation.CursorKey(orderField),
|
||||
}
|
||||
}
|
||||
|
||||
func NewInvitation(i *coredata.Invitation) *Invitation {
|
||||
func NewInvitation(invitation *coredata.Invitation) *Invitation {
|
||||
return &Invitation{
|
||||
ID: i.ID,
|
||||
Email: i.Email,
|
||||
FullName: i.FullName,
|
||||
Role: i.Role,
|
||||
Status: i.Status,
|
||||
ExpiresAt: i.ExpiresAt,
|
||||
AcceptedAt: i.AcceptedAt,
|
||||
CreatedAt: i.CreatedAt,
|
||||
ID: invitation.ID,
|
||||
Email: invitation.Email,
|
||||
ExpiresAt: invitation.ExpiresAt,
|
||||
AcceptedAt: invitation.AcceptedAt,
|
||||
CreatedAt: invitation.CreatedAt,
|
||||
Status: invitation.Status,
|
||||
}
|
||||
}
|
||||
@@ -21,16 +21,16 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
MembershipOrderBy OrderBy[coredata.MembershipOrderField]
|
||||
|
||||
MembershipConnection struct {
|
||||
TotalCount int `json:"totalCount"`
|
||||
Edges []*MembershipEdge `json:"edges"`
|
||||
PageInfo *PageInfo `json:"pageInfo"`
|
||||
TotalCount int
|
||||
Edges []*MembershipEdge
|
||||
PageInfo PageInfo
|
||||
|
||||
Resolver any
|
||||
ParentID gid.GID
|
||||
}
|
||||
|
||||
MembershipOrderBy OrderBy[coredata.MembershipOrderField]
|
||||
)
|
||||
|
||||
func NewMembershipConnection(
|
||||
@@ -38,36 +38,34 @@ func NewMembershipConnection(
|
||||
resolver any,
|
||||
parentID gid.GID,
|
||||
) *MembershipConnection {
|
||||
var edges = make([]*MembershipEdge, len(p.Data))
|
||||
|
||||
for i := range edges {
|
||||
edges[i] = NewMembershipEdge(p.Data[i], p.Cursor.OrderBy.Field)
|
||||
edges := make([]*MembershipEdge, len(p.Data))
|
||||
for i, membership := range p.Data {
|
||||
edges[i] = NewMembershipEdge(membership, p.Cursor.OrderBy.Field)
|
||||
}
|
||||
|
||||
return &MembershipConnection{
|
||||
Edges: edges,
|
||||
PageInfo: NewPageInfo(p),
|
||||
PageInfo: *NewPageInfo(p),
|
||||
|
||||
Resolver: resolver,
|
||||
ParentID: parentID,
|
||||
}
|
||||
}
|
||||
|
||||
func NewMembershipEdge(membership *coredata.Membership, orderBy coredata.MembershipOrderField) *MembershipEdge {
|
||||
func NewMembershipEdge(membership *coredata.Membership, orderField coredata.MembershipOrderField) *MembershipEdge {
|
||||
return &MembershipEdge{
|
||||
Cursor: membership.CursorKey(orderBy),
|
||||
Node: NewMembership(membership),
|
||||
Cursor: membership.CursorKey(orderField),
|
||||
}
|
||||
}
|
||||
|
||||
func NewMembership(m *coredata.Membership) *Membership {
|
||||
func NewMembership(membership *coredata.Membership) *Membership {
|
||||
return &Membership{
|
||||
ID: m.ID,
|
||||
UserID: m.UserID,
|
||||
OrganizationID: m.OrganizationID,
|
||||
Role: m.Role,
|
||||
FullName: m.FullName,
|
||||
EmailAddress: m.EmailAddress,
|
||||
CreatedAt: m.CreatedAt,
|
||||
UpdatedAt: m.UpdatedAt,
|
||||
ID: membership.ID,
|
||||
CreatedAt: membership.CreatedAt,
|
||||
// Permissions: membership.Permissions,
|
||||
// ProvisionedBy: membership.ProvisionedBy,
|
||||
// Active: membership.Active,
|
||||
// LastSyncedAt: membership.LastSyncedAt,
|
||||
}
|
||||
}
|
||||
@@ -12,24 +12,13 @@
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package mcp_v1
|
||||
package types
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
import "go.probo.inc/probo/pkg/page"
|
||||
|
||||
type (
|
||||
Config struct {
|
||||
Version string
|
||||
RequestTimeout time.Duration
|
||||
MaxRequestSize int64
|
||||
OrderBy[T page.OrderField] struct {
|
||||
Field T
|
||||
Direction page.OrderDirection
|
||||
}
|
||||
)
|
||||
|
||||
func DefaultConfig() Config {
|
||||
return Config{
|
||||
Version: "1.0.0",
|
||||
RequestTimeout: 30 * time.Second,
|
||||
MaxRequestSize: 10 * 1024 * 1024, // 10MB
|
||||
}
|
||||
}
|
||||
@@ -12,27 +12,21 @@
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
package auth
|
||||
package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
authsvc "go.probo.inc/probo/pkg/auth"
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
)
|
||||
|
||||
// SAMLMetadataHandler returns an HTTP handler that serves the SAML Service Provider metadata XML
|
||||
// Uses global SP certificate configured at service startup
|
||||
func SAMLMetadataHandler(samlSvc *authsvc.SAMLService) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
metadataXML, err := samlSvc.GenerateMetadata()
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("cannot generate metadata: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
type (
|
||||
OrganizationOrderBy OrderBy[coredata.OrganizationOrderField]
|
||||
)
|
||||
|
||||
w.Header().Set("Content-Type", "application/samlmetadata+xml")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(metadataXML)
|
||||
func NewOrganization(organization *coredata.Organization) *Organization {
|
||||
return &Organization{
|
||||
ID: organization.ID,
|
||||
Name: organization.Name,
|
||||
CreatedAt: organization.CreatedAt,
|
||||
UpdatedAt: organization.UpdatedAt,
|
||||
}
|
||||
}
|
||||
30
pkg/server/api/connect/v1/types/page_info.go
Normal file
30
pkg/server/api/connect/v1/types/page_info.go
Normal file
@@ -0,0 +1,30 @@
|
||||
// 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/page"
|
||||
"go.probo.inc/probo/pkg/server/gqlutils/types/pageinfo"
|
||||
)
|
||||
|
||||
func NewPageInfo[T page.Paginable[O], O page.OrderField](p *page.Page[T, O]) *PageInfo {
|
||||
data := pageinfo.NewPageInfo(p)
|
||||
return &PageInfo{
|
||||
HasNextPage: data.HasNextPage,
|
||||
HasPreviousPage: data.HasPreviousPage,
|
||||
StartCursor: data.StartCursor,
|
||||
EndCursor: data.EndCursor,
|
||||
}
|
||||
}
|
||||
69
pkg/server/api/connect/v1/types/personal_api_key.go
Normal file
69
pkg/server/api/connect/v1/types/personal_api_key.go
Normal file
@@ -0,0 +1,69 @@
|
||||
// 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"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/page"
|
||||
)
|
||||
|
||||
type (
|
||||
PersonalAPIKeyOrderBy OrderBy[coredata.UserAPIKeyOrderField]
|
||||
|
||||
PersonalAPIKeyConnection struct {
|
||||
TotalCount int
|
||||
Edges []*PersonalAPIKeyEdge
|
||||
PageInfo PageInfo
|
||||
|
||||
Resolver any
|
||||
ParentID gid.GID
|
||||
}
|
||||
)
|
||||
|
||||
func NewPersonalAPIKeyConnection(
|
||||
p *page.Page[*coredata.UserAPIKey, coredata.UserAPIKeyOrderField],
|
||||
resolver any,
|
||||
parentID gid.GID,
|
||||
) *PersonalAPIKeyConnection {
|
||||
edges := make([]*PersonalAPIKeyEdge, len(p.Data))
|
||||
for i, personalAPIKey := range p.Data {
|
||||
edges[i] = NewPersonalAPIKeyEdge(personalAPIKey, p.Cursor.OrderBy.Field)
|
||||
}
|
||||
|
||||
return &PersonalAPIKeyConnection{
|
||||
Edges: edges,
|
||||
PageInfo: *NewPageInfo(p),
|
||||
|
||||
Resolver: resolver,
|
||||
ParentID: parentID,
|
||||
}
|
||||
}
|
||||
|
||||
func NewPersonalAPIKeyEdge(personalAPIKey *coredata.UserAPIKey, orderField coredata.UserAPIKeyOrderField) *PersonalAPIKeyEdge {
|
||||
return &PersonalAPIKeyEdge{
|
||||
Node: NewPersonalAPIKey(personalAPIKey),
|
||||
Cursor: personalAPIKey.CursorKey(orderField),
|
||||
}
|
||||
}
|
||||
|
||||
func NewPersonalAPIKey(personalAPIKey *coredata.UserAPIKey) *PersonalAPIKey {
|
||||
return &PersonalAPIKey{
|
||||
ID: personalAPIKey.ID,
|
||||
Name: personalAPIKey.Name,
|
||||
ExpiresAt: personalAPIKey.ExpiresAt,
|
||||
CreatedAt: personalAPIKey.CreatedAt,
|
||||
}
|
||||
}
|
||||
82
pkg/server/api/connect/v1/types/saml_configuration.go
Normal file
82
pkg/server/api/connect/v1/types/saml_configuration.go
Normal file
@@ -0,0 +1,82 @@
|
||||
// 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"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/page"
|
||||
)
|
||||
|
||||
type (
|
||||
SAMLConfigurationOrderBy OrderBy[coredata.SAMLConfigurationOrderField]
|
||||
|
||||
SAMLConfigurationConnection struct {
|
||||
TotalCount int
|
||||
Edges []*SAMLConfigurationEdge
|
||||
PageInfo PageInfo
|
||||
|
||||
Resolver any
|
||||
ParentID gid.GID
|
||||
}
|
||||
)
|
||||
|
||||
func NewSAMLConfigurationConnection(
|
||||
p *page.Page[*coredata.SAMLConfiguration, coredata.SAMLConfigurationOrderField],
|
||||
resolver any,
|
||||
parentID gid.GID,
|
||||
) *SAMLConfigurationConnection {
|
||||
edges := make([]*SAMLConfigurationEdge, len(p.Data))
|
||||
for i, samlConfiguration := range p.Data {
|
||||
edges[i] = NewSAMLConfigurationEdge(samlConfiguration, p.Cursor.OrderBy.Field)
|
||||
}
|
||||
|
||||
return &SAMLConfigurationConnection{
|
||||
Edges: edges,
|
||||
PageInfo: *NewPageInfo(p),
|
||||
|
||||
Resolver: resolver,
|
||||
ParentID: parentID,
|
||||
}
|
||||
}
|
||||
|
||||
func NewSAMLConfigurationEdge(samlConfiguration *coredata.SAMLConfiguration, orderField coredata.SAMLConfigurationOrderField) *SAMLConfigurationEdge {
|
||||
return &SAMLConfigurationEdge{
|
||||
Node: NewSAMLConfiguration(samlConfiguration),
|
||||
Cursor: samlConfiguration.CursorKey(orderField),
|
||||
}
|
||||
}
|
||||
|
||||
func NewSAMLConfiguration(samlConfiguration *coredata.SAMLConfiguration) *SAMLConfiguration {
|
||||
return &SAMLConfiguration{
|
||||
ID: samlConfiguration.ID,
|
||||
EmailDomain: samlConfiguration.EmailDomain,
|
||||
EnforcementPolicy: samlConfiguration.EnforcementPolicy,
|
||||
DomainVerified: samlConfiguration.DomainVerified,
|
||||
DomainVerifiedAt: samlConfiguration.DomainVerifiedAt,
|
||||
DomainVerificationToken: samlConfiguration.DomainVerificationToken,
|
||||
IdpEntityID: samlConfiguration.IdPEntityID,
|
||||
IdpSsoURL: samlConfiguration.IdPSsoURL,
|
||||
IdpCertificate: samlConfiguration.IdPCertificate,
|
||||
CreatedAt: samlConfiguration.CreatedAt,
|
||||
UpdatedAt: samlConfiguration.UpdatedAt,
|
||||
AttributeMappings: &SAMLAttributeMappings{
|
||||
Email: samlConfiguration.AttributeEmail,
|
||||
FirstName: samlConfiguration.AttributeFirstname,
|
||||
LastName: samlConfiguration.AttributeLastname,
|
||||
Role: samlConfiguration.AttributeRole,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -21,50 +21,51 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
UserConnection struct {
|
||||
TotalCount int `json:"totalCount"`
|
||||
Edges []*UserEdge `json:"edges"`
|
||||
PageInfo *PageInfo `json:"pageInfo"`
|
||||
SessionOrderBy OrderBy[coredata.SessionOrderField]
|
||||
|
||||
SessionConnection struct {
|
||||
TotalCount int
|
||||
Edges []*SessionEdge
|
||||
PageInfo PageInfo
|
||||
|
||||
Resolver any
|
||||
ParentID gid.GID
|
||||
}
|
||||
|
||||
UserOrderBy OrderBy[coredata.UserOrderField]
|
||||
)
|
||||
|
||||
func NewUserConnection(
|
||||
p *page.Page[*coredata.User, coredata.UserOrderField],
|
||||
func NewSessionConnection(
|
||||
p *page.Page[*coredata.Session, coredata.SessionOrderField],
|
||||
resolver any,
|
||||
parentID gid.GID,
|
||||
) *UserConnection {
|
||||
var edges = make([]*UserEdge, len(p.Data))
|
||||
|
||||
for i := range edges {
|
||||
edges[i] = NewUserEdge(p.Data[i], p.Cursor.OrderBy.Field)
|
||||
) *SessionConnection {
|
||||
edges := make([]*SessionEdge, len(p.Data))
|
||||
for i, session := range p.Data {
|
||||
edges[i] = NewSessionEdge(session, p.Cursor.OrderBy.Field)
|
||||
}
|
||||
|
||||
return &UserConnection{
|
||||
return &SessionConnection{
|
||||
Edges: edges,
|
||||
PageInfo: NewPageInfo(p),
|
||||
PageInfo: *NewPageInfo(p),
|
||||
|
||||
Resolver: resolver,
|
||||
ParentID: parentID,
|
||||
}
|
||||
}
|
||||
|
||||
func NewUserEdge(user *coredata.User, orderBy coredata.UserOrderField) *UserEdge {
|
||||
return &UserEdge{
|
||||
Cursor: user.CursorKey(orderBy),
|
||||
Node: NewUser(user),
|
||||
func NewSessionEdge(session *coredata.Session, orderField coredata.SessionOrderField) *SessionEdge {
|
||||
return &SessionEdge{
|
||||
Node: NewSession(session),
|
||||
Cursor: session.CursorKey(orderField),
|
||||
}
|
||||
}
|
||||
|
||||
func NewUser(u *coredata.User) *User {
|
||||
return &User{
|
||||
ID: u.ID,
|
||||
Email: u.EmailAddress,
|
||||
FullName: u.FullName,
|
||||
CreatedAt: u.CreatedAt,
|
||||
UpdatedAt: u.UpdatedAt,
|
||||
func NewSession(session *coredata.Session) *Session {
|
||||
return &Session{
|
||||
ID: session.ID,
|
||||
IPAddress: session.IPAddress.String(),
|
||||
UserAgent: session.UserAgent,
|
||||
UpdatedAt: session.UpdatedAt,
|
||||
CreatedAt: session.CreatedAt,
|
||||
ExpiresAt: session.ExpiredAt,
|
||||
}
|
||||
}
|
||||
966
pkg/server/api/connect/v1/types/types.go
Normal file
966
pkg/server/api/connect/v1/types/types.go
Normal file
@@ -0,0 +1,966 @@
|
||||
// Code generated by github.com/99designs/gqlgen, DO NOT EDIT.
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/mail"
|
||||
"go.probo.inc/probo/pkg/page"
|
||||
)
|
||||
|
||||
type Node interface {
|
||||
IsNode()
|
||||
GetID() gid.GID
|
||||
}
|
||||
|
||||
type AcceptInvitationInput struct {
|
||||
InvitationID gid.GID `json:"invitationId"`
|
||||
}
|
||||
|
||||
type AcceptInvitationPayload struct {
|
||||
MembershipEdge *MembershipEdge `json:"membershipEdge"`
|
||||
}
|
||||
|
||||
type AddIPAllowlistEntryInput struct {
|
||||
OrganizationID gid.GID `json:"organizationId"`
|
||||
Cidr string `json:"cidr"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
type Application struct {
|
||||
ID ApplicationID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
AvailableAccessLevels []AccessLevel `json:"availableAccessLevels"`
|
||||
}
|
||||
|
||||
type ChangeEmailInput struct {
|
||||
NewEmail mail.Addr `json:"newEmail"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type ChangeEmailPayload struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
type ChangePasswordInput struct {
|
||||
CurrentPassword string `json:"currentPassword"`
|
||||
NewPassword string `json:"newPassword"`
|
||||
}
|
||||
|
||||
type ChangePasswordPayload struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
type CreateOrganizationInput struct {
|
||||
Name string `json:"name"`
|
||||
LogoFile *graphql.Upload `json:"logoFile,omitempty"`
|
||||
HorizontalLogoFile *graphql.Upload `json:"horizontalLogoFile,omitempty"`
|
||||
}
|
||||
|
||||
type CreateOrganizationPayload struct {
|
||||
Organization *Organization `json:"organization,omitempty"`
|
||||
MembershipEdge *MembershipEdge `json:"membershipEdge"`
|
||||
}
|
||||
|
||||
type CreatePersonalAPIKeyInput struct {
|
||||
Name string `json:"name"`
|
||||
ExpiresAt time.Time `json:"expiresAt"`
|
||||
OrganizationIds []gid.GID `json:"organizationIds"`
|
||||
}
|
||||
|
||||
type CreatePersonalAPIKeyPayload struct {
|
||||
PersonalAPIKeyEdge *PersonalAPIKeyEdge `json:"personalAPIKeyEdge"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type CreateSAMLConfigurationInput struct {
|
||||
OrganizationID gid.GID `json:"organizationId"`
|
||||
EmailDomain string `json:"emailDomain"`
|
||||
IdpEntityID string `json:"idpEntityId"`
|
||||
IdpSsoURL string `json:"idpSsoUrl"`
|
||||
IdpCertificate string `json:"idpCertificate"`
|
||||
AutoSignupEnabled bool `json:"autoSignupEnabled"`
|
||||
AttributeMappings *SAMLAttributeMappingsInput `json:"attributeMappings,omitempty"`
|
||||
}
|
||||
|
||||
type CreateSAMLConfigurationPayload struct {
|
||||
SamlConfigurationEdge *SAMLConfigurationEdge `json:"samlConfigurationEdge"`
|
||||
}
|
||||
|
||||
type CustomAttribute struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type DeactivateAccountInput struct {
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type DeactivateAccountPayload struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
type DeleteAccountInput struct {
|
||||
Password string `json:"password"`
|
||||
Confirmation string `json:"confirmation"`
|
||||
}
|
||||
|
||||
type DeleteAccountPayload struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
type DeleteInvitationInput struct {
|
||||
OrganizationID gid.GID `json:"organizationId"`
|
||||
InvitationID gid.GID `json:"invitationId"`
|
||||
}
|
||||
|
||||
type DeleteInvitationPayload struct {
|
||||
DeletedInvitationID gid.GID `json:"deletedInvitationId"`
|
||||
}
|
||||
|
||||
type DeleteOrganizationInput struct {
|
||||
OrganizationID gid.GID `json:"organizationId"`
|
||||
}
|
||||
|
||||
type DeleteOrganizationPayload struct {
|
||||
DeletedOrganizationID gid.GID `json:"deletedOrganizationId"`
|
||||
}
|
||||
|
||||
type DeleteSAMLConfigurationInput struct {
|
||||
OrganizationID gid.GID `json:"organizationId"`
|
||||
SamlConfigurationID gid.GID `json:"samlConfigurationId"`
|
||||
}
|
||||
|
||||
type DeleteSAMLConfigurationPayload struct {
|
||||
DeletedSamlConfigurationID gid.GID `json:"deletedSamlConfigurationId"`
|
||||
}
|
||||
|
||||
type ForgotPasswordInput struct {
|
||||
Email mail.Addr `json:"email"`
|
||||
}
|
||||
|
||||
type ForgotPasswordPayload struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
type Identity struct {
|
||||
ID gid.GID `json:"id"`
|
||||
Email mail.Addr `json:"email"`
|
||||
EmailVerified bool `json:"emailVerified"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
Memberships *MembershipConnection `json:"memberships"`
|
||||
PendingInvitations *InvitationConnection `json:"pendingInvitations"`
|
||||
Sessions *SessionConnection `json:"sessions"`
|
||||
PersonalAPIKeys *PersonalAPIKeyConnection `json:"personalAPIKeys"`
|
||||
ProfileFor *IdentityProfile `json:"profileFor,omitempty"`
|
||||
}
|
||||
|
||||
func (Identity) IsNode() {}
|
||||
func (this Identity) GetID() gid.GID { return this.ID }
|
||||
|
||||
type IdentityProfile struct {
|
||||
ID gid.GID `json:"id"`
|
||||
DisplayName string `json:"displayName"`
|
||||
FirstName *string `json:"firstName,omitempty"`
|
||||
LastName *string `json:"lastName,omitempty"`
|
||||
JobTitle *string `json:"jobTitle,omitempty"`
|
||||
Department *string `json:"department,omitempty"`
|
||||
PhoneNumber *string `json:"phoneNumber,omitempty"`
|
||||
AvatarURL *string `json:"avatarUrl,omitempty"`
|
||||
Manager *IdentityProfile `json:"manager,omitempty"`
|
||||
Timezone *string `json:"timezone,omitempty"`
|
||||
Locale *string `json:"locale,omitempty"`
|
||||
CustomAttributes []*CustomAttribute `json:"customAttributes"`
|
||||
ProvisionedBy ProvisioningSource `json:"provisionedBy"`
|
||||
ExternalID *string `json:"externalId,omitempty"`
|
||||
Identity *Identity `json:"identity"`
|
||||
Organization *Organization `json:"organization"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
func (IdentityProfile) IsNode() {}
|
||||
func (this IdentityProfile) GetID() gid.GID { return this.ID }
|
||||
|
||||
type Invitation struct {
|
||||
ID gid.GID `json:"id"`
|
||||
Email mail.Addr `json:"email"`
|
||||
ExpiresAt time.Time `json:"expiresAt"`
|
||||
AcceptedAt *time.Time `json:"acceptedAt,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
Status coredata.InvitationStatus `json:"status"`
|
||||
}
|
||||
|
||||
func (Invitation) IsNode() {}
|
||||
func (this Invitation) GetID() gid.GID { return this.ID }
|
||||
|
||||
type InvitationEdge struct {
|
||||
Node *Invitation `json:"node"`
|
||||
Cursor page.CursorKey `json:"cursor"`
|
||||
}
|
||||
|
||||
type InvitationProfile struct {
|
||||
DisplayName string `json:"displayName"`
|
||||
FirstName *string `json:"firstName,omitempty"`
|
||||
LastName *string `json:"lastName,omitempty"`
|
||||
JobTitle *string `json:"jobTitle,omitempty"`
|
||||
Department *string `json:"department,omitempty"`
|
||||
}
|
||||
|
||||
type InvitationProfileInput struct {
|
||||
DisplayName string `json:"displayName"`
|
||||
FirstName *string `json:"firstName,omitempty"`
|
||||
LastName *string `json:"lastName,omitempty"`
|
||||
JobTitle *string `json:"jobTitle,omitempty"`
|
||||
Department *string `json:"department,omitempty"`
|
||||
}
|
||||
|
||||
type InviteMemberInput struct {
|
||||
OrganizationID gid.GID `json:"organizationId"`
|
||||
Email mail.Addr `json:"email"`
|
||||
FullName string `json:"fullName"`
|
||||
}
|
||||
|
||||
type InviteMemberPayload struct {
|
||||
InvitationEdge *InvitationEdge `json:"invitationEdge"`
|
||||
}
|
||||
|
||||
type Membership struct {
|
||||
ID gid.GID `json:"id"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
Profile *IdentityProfile `json:"profile"`
|
||||
Identity *Identity `json:"identity"`
|
||||
Organization *Organization `json:"organization"`
|
||||
Permissions []*Permission `json:"permissions"`
|
||||
ProvisionedBy ProvisioningSource `json:"provisionedBy"`
|
||||
Active bool `json:"active"`
|
||||
LastSyncedAt *time.Time `json:"lastSyncedAt,omitempty"`
|
||||
}
|
||||
|
||||
func (Membership) IsNode() {}
|
||||
func (this Membership) GetID() gid.GID { return this.ID }
|
||||
|
||||
type MembershipEdge struct {
|
||||
Node *Membership `json:"node"`
|
||||
Cursor page.CursorKey `json:"cursor"`
|
||||
}
|
||||
|
||||
type Mutation struct {
|
||||
}
|
||||
|
||||
type Organization struct {
|
||||
ID gid.GID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
LogoURL *string `json:"logoUrl,omitempty"`
|
||||
HorizontalLogoURL *string `json:"horizontalLogoUrl,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
Members *MembershipConnection `json:"members"`
|
||||
Invitations *InvitationConnection `json:"invitations"`
|
||||
SamlConfigurations *SAMLConfigurationConnection `json:"samlConfigurations"`
|
||||
AvailableApplications []*Application `json:"availableApplications"`
|
||||
}
|
||||
|
||||
func (Organization) IsNode() {}
|
||||
func (this Organization) GetID() gid.GID { return this.ID }
|
||||
|
||||
type PageInfo struct {
|
||||
HasNextPage bool `json:"hasNextPage"`
|
||||
HasPreviousPage bool `json:"hasPreviousPage"`
|
||||
StartCursor *page.CursorKey `json:"startCursor,omitempty"`
|
||||
EndCursor *page.CursorKey `json:"endCursor,omitempty"`
|
||||
}
|
||||
|
||||
type Permission struct {
|
||||
ID gid.GID `json:"id"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
Application *Application `json:"application"`
|
||||
AccessLevel AccessLevel `json:"accessLevel"`
|
||||
Organization *Organization `json:"organization"`
|
||||
PrincipalType PrincipalType `json:"principalType"`
|
||||
PrincipalID gid.GID `json:"principalId"`
|
||||
}
|
||||
|
||||
func (Permission) IsNode() {}
|
||||
func (this Permission) GetID() gid.GID { return this.ID }
|
||||
|
||||
type PermissionGrant struct {
|
||||
Application *Application `json:"application"`
|
||||
AccessLevel AccessLevel `json:"accessLevel"`
|
||||
}
|
||||
|
||||
type PersonalAPIKey struct {
|
||||
ID gid.GID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
LastUsedAt *time.Time `json:"lastUsedAt,omitempty"`
|
||||
ExpiresAt time.Time `json:"expiresAt"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
Scopes []TokenScope `json:"scopes"`
|
||||
Organizations []*Organization `json:"organizations"`
|
||||
}
|
||||
|
||||
func (PersonalAPIKey) IsNode() {}
|
||||
func (this PersonalAPIKey) GetID() gid.GID { return this.ID }
|
||||
|
||||
type PersonalAPIKeyEdge struct {
|
||||
Node *PersonalAPIKey `json:"node"`
|
||||
Cursor page.CursorKey `json:"cursor"`
|
||||
}
|
||||
|
||||
type Query struct {
|
||||
}
|
||||
|
||||
type RemoveIPAllowlistEntryInput struct {
|
||||
EntryID gid.GID `json:"entryId"`
|
||||
}
|
||||
|
||||
type RemoveMemberInput struct {
|
||||
OrganizationID gid.GID `json:"organizationId"`
|
||||
MembershipID gid.GID `json:"membershipId"`
|
||||
}
|
||||
|
||||
type RemoveMemberPayload struct {
|
||||
DeletedMembershipID gid.GID `json:"deletedMembershipId"`
|
||||
}
|
||||
|
||||
type ResetPasswordInput struct {
|
||||
Token string `json:"token"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type ResetPasswordPayload struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
type RevokeAllSessionsPayload struct {
|
||||
RevokedCount int `json:"revokedCount"`
|
||||
}
|
||||
|
||||
type RevokePersonalAPIKeyInput struct {
|
||||
TokenID gid.GID `json:"tokenId"`
|
||||
}
|
||||
|
||||
type RevokePersonalAPIKeyPayload struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
type RevokeSessionInput struct {
|
||||
SessionID gid.GID `json:"sessionId"`
|
||||
}
|
||||
|
||||
type RevokeSessionPayload struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
type SAMLAttributeMappings struct {
|
||||
Email string `json:"email"`
|
||||
FirstName string `json:"firstName"`
|
||||
LastName string `json:"lastName"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
|
||||
type SAMLAttributeMappingsInput struct {
|
||||
Email *string `json:"email,omitempty"`
|
||||
FirstName *string `json:"firstName,omitempty"`
|
||||
LastName *string `json:"lastName,omitempty"`
|
||||
Role *string `json:"role,omitempty"`
|
||||
}
|
||||
|
||||
type SAMLConfiguration struct {
|
||||
ID gid.GID `json:"id"`
|
||||
EmailDomain string `json:"emailDomain"`
|
||||
Enabled bool `json:"enabled"`
|
||||
EnforcementPolicy coredata.SAMLEnforcementPolicy `json:"enforcementPolicy"`
|
||||
DomainVerified bool `json:"domainVerified"`
|
||||
DomainVerifiedAt *time.Time `json:"domainVerifiedAt,omitempty"`
|
||||
DomainVerificationToken *string `json:"domainVerificationToken,omitempty"`
|
||||
IdpEntityID string `json:"idpEntityId"`
|
||||
IdpSsoURL string `json:"idpSsoUrl"`
|
||||
IdpCertificate string `json:"idpCertificate"`
|
||||
AutoSignupEnabled bool `json:"autoSignupEnabled"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
SpMetadataURL string `json:"spMetadataUrl"`
|
||||
TestLoginURL string `json:"testLoginUrl"`
|
||||
AttributeMappings *SAMLAttributeMappings `json:"attributeMappings"`
|
||||
DefaultPermissions []*PermissionGrant `json:"defaultPermissions"`
|
||||
}
|
||||
|
||||
func (SAMLConfiguration) IsNode() {}
|
||||
func (this SAMLConfiguration) GetID() gid.GID { return this.ID }
|
||||
|
||||
type SAMLConfigurationEdge struct {
|
||||
Node *SAMLConfiguration `json:"node"`
|
||||
Cursor page.CursorKey `json:"cursor"`
|
||||
}
|
||||
|
||||
type SSOAvailability struct {
|
||||
Available bool `json:"available"`
|
||||
SamlConfigID *gid.GID `json:"samlConfigId,omitempty"`
|
||||
OrganizationID *gid.GID `json:"organizationId,omitempty"`
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
ID gid.GID `json:"id"`
|
||||
IPAddress string `json:"ipAddress"`
|
||||
UserAgent string `json:"userAgent"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
ExpiresAt time.Time `json:"expiresAt"`
|
||||
}
|
||||
|
||||
func (Session) IsNode() {}
|
||||
func (this Session) GetID() gid.GID { return this.ID }
|
||||
|
||||
type SessionEdge struct {
|
||||
Node *Session `json:"node"`
|
||||
Cursor page.CursorKey `json:"cursor"`
|
||||
}
|
||||
|
||||
type SessionOrder struct {
|
||||
Direction page.OrderDirection `json:"direction"`
|
||||
Field coredata.SessionOrderField `json:"field"`
|
||||
}
|
||||
|
||||
type SessionPolicy struct {
|
||||
MaxSessionDurationHours int `json:"maxSessionDurationHours"`
|
||||
IdleTimeoutMinutes int `json:"idleTimeoutMinutes"`
|
||||
MaxConcurrentSessions *int `json:"maxConcurrentSessions,omitempty"`
|
||||
RequireReauthForSensitiveActions bool `json:"requireReauthForSensitiveActions"`
|
||||
}
|
||||
|
||||
type SessionPolicyInput struct {
|
||||
MaxSessionDurationHours *int `json:"maxSessionDurationHours,omitempty"`
|
||||
IdleTimeoutMinutes *int `json:"idleTimeoutMinutes,omitempty"`
|
||||
MaxConcurrentSessions *int `json:"maxConcurrentSessions,omitempty"`
|
||||
RequireReauthForSensitiveActions *bool `json:"requireReauthForSensitiveActions,omitempty"`
|
||||
}
|
||||
|
||||
type SignInInput struct {
|
||||
Email mail.Addr `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type SignInPayload struct {
|
||||
Identity *Identity `json:"identity,omitempty"`
|
||||
}
|
||||
|
||||
type SignOutPayload struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
type SignUpFromInvitationInput struct {
|
||||
Token string `json:"token"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type SignUpFromInvitationPayload struct {
|
||||
Identity *Identity `json:"identity,omitempty"`
|
||||
}
|
||||
|
||||
type SignUpInput struct {
|
||||
Email mail.Addr `json:"email"`
|
||||
Password string `json:"password"`
|
||||
FullName string `json:"fullName"`
|
||||
}
|
||||
|
||||
type SignUpPayload struct {
|
||||
Identity *Identity `json:"identity,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateIdentityProfileInput struct {
|
||||
MembershipID gid.GID `json:"membershipId"`
|
||||
DisplayName *string `json:"displayName,omitempty"`
|
||||
FirstName *string `json:"firstName,omitempty"`
|
||||
LastName *string `json:"lastName,omitempty"`
|
||||
JobTitle *string `json:"jobTitle,omitempty"`
|
||||
Department *string `json:"department,omitempty"`
|
||||
PhoneNumber *string `json:"phoneNumber,omitempty"`
|
||||
Timezone *string `json:"timezone,omitempty"`
|
||||
Locale *string `json:"locale,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateIdentityProfilePayload struct {
|
||||
Profile *IdentityProfile `json:"profile,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateOrganizationInput struct {
|
||||
OrganizationID gid.GID `json:"organizationId"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
LogoFile graphql.Omittable[*graphql.Upload] `json:"logoFile,omitempty"`
|
||||
HorizontalLogoFile graphql.Omittable[*graphql.Upload] `json:"horizontalLogoFile,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateOrganizationPayload struct {
|
||||
Organization *Organization `json:"organization,omitempty"`
|
||||
}
|
||||
|
||||
type UpdatePersonalAPIKeyInput struct {
|
||||
TokenID gid.GID `json:"tokenId"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
type UpdatePersonalAPIKeyPayload struct {
|
||||
PersonalAPIKey *PersonalAPIKey `json:"personalAPIKey,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateSAMLConfigurationInput struct {
|
||||
OrganizationID gid.GID `json:"organizationId"`
|
||||
SamlConfigurationID gid.GID `json:"samlConfigurationId"`
|
||||
IdpEntityID *string `json:"idpEntityId,omitempty"`
|
||||
IdpSsoURL *string `json:"idpSsoUrl,omitempty"`
|
||||
IdpCertificate *string `json:"idpCertificate,omitempty"`
|
||||
AutoSignupEnabled *bool `json:"autoSignupEnabled,omitempty"`
|
||||
EnforcementPolicy *coredata.SAMLEnforcementPolicy `json:"enforcementPolicy,omitempty"`
|
||||
AttributeMappings *SAMLAttributeMappingsInput `json:"attributeMappings,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateSAMLConfigurationPayload struct {
|
||||
SamlConfiguration *SAMLConfiguration `json:"samlConfiguration,omitempty"`
|
||||
}
|
||||
|
||||
type VerifyEmailInput struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type VerifyEmailPayload struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
type AccessLevel string
|
||||
|
||||
const (
|
||||
AccessLevelRead AccessLevel = "READ"
|
||||
AccessLevelWrite AccessLevel = "WRITE"
|
||||
AccessLevelAdmin AccessLevel = "ADMIN"
|
||||
)
|
||||
|
||||
var AllAccessLevel = []AccessLevel{
|
||||
AccessLevelRead,
|
||||
AccessLevelWrite,
|
||||
AccessLevelAdmin,
|
||||
}
|
||||
|
||||
func (e AccessLevel) IsValid() bool {
|
||||
switch e {
|
||||
case AccessLevelRead, AccessLevelWrite, AccessLevelAdmin:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (e AccessLevel) String() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func (e *AccessLevel) UnmarshalGQL(v any) error {
|
||||
str, ok := v.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("enums must be strings")
|
||||
}
|
||||
|
||||
*e = AccessLevel(str)
|
||||
if !e.IsValid() {
|
||||
return fmt.Errorf("%s is not a valid AccessLevel", str)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e AccessLevel) MarshalGQL(w io.Writer) {
|
||||
fmt.Fprint(w, strconv.Quote(e.String()))
|
||||
}
|
||||
|
||||
func (e *AccessLevel) UnmarshalJSON(b []byte) error {
|
||||
s, err := strconv.Unquote(string(b))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return e.UnmarshalGQL(s)
|
||||
}
|
||||
|
||||
func (e AccessLevel) MarshalJSON() ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
e.MarshalGQL(&buf)
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
type ApplicationID string
|
||||
|
||||
const (
|
||||
ApplicationIDConsole ApplicationID = "CONSOLE"
|
||||
ApplicationIDCompliance ApplicationID = "COMPLIANCE"
|
||||
ApplicationIDRisk ApplicationID = "RISK"
|
||||
ApplicationIDVendor ApplicationID = "VENDOR"
|
||||
ApplicationIDDocuments ApplicationID = "DOCUMENTS"
|
||||
ApplicationIDTrustCenter ApplicationID = "TRUST_CENTER"
|
||||
ApplicationIDSettings ApplicationID = "SETTINGS"
|
||||
ApplicationIDAPI ApplicationID = "API"
|
||||
)
|
||||
|
||||
var AllApplicationID = []ApplicationID{
|
||||
ApplicationIDConsole,
|
||||
ApplicationIDCompliance,
|
||||
ApplicationIDRisk,
|
||||
ApplicationIDVendor,
|
||||
ApplicationIDDocuments,
|
||||
ApplicationIDTrustCenter,
|
||||
ApplicationIDSettings,
|
||||
ApplicationIDAPI,
|
||||
}
|
||||
|
||||
func (e ApplicationID) IsValid() bool {
|
||||
switch e {
|
||||
case ApplicationIDConsole, ApplicationIDCompliance, ApplicationIDRisk, ApplicationIDVendor, ApplicationIDDocuments, ApplicationIDTrustCenter, ApplicationIDSettings, ApplicationIDAPI:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (e ApplicationID) String() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func (e *ApplicationID) UnmarshalGQL(v any) error {
|
||||
str, ok := v.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("enums must be strings")
|
||||
}
|
||||
|
||||
*e = ApplicationID(str)
|
||||
if !e.IsValid() {
|
||||
return fmt.Errorf("%s is not a valid ApplicationId", str)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e ApplicationID) MarshalGQL(w io.Writer) {
|
||||
fmt.Fprint(w, strconv.Quote(e.String()))
|
||||
}
|
||||
|
||||
func (e *ApplicationID) UnmarshalJSON(b []byte) error {
|
||||
s, err := strconv.Unquote(string(b))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return e.UnmarshalGQL(s)
|
||||
}
|
||||
|
||||
func (e ApplicationID) MarshalJSON() ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
e.MarshalGQL(&buf)
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
type AuthMethod string
|
||||
|
||||
const (
|
||||
AuthMethodPassword AuthMethod = "PASSWORD"
|
||||
AuthMethodSaml AuthMethod = "SAML"
|
||||
AuthMethodRecoveryCode AuthMethod = "RECOVERY_CODE"
|
||||
)
|
||||
|
||||
var AllAuthMethod = []AuthMethod{
|
||||
AuthMethodPassword,
|
||||
AuthMethodSaml,
|
||||
AuthMethodRecoveryCode,
|
||||
}
|
||||
|
||||
func (e AuthMethod) IsValid() bool {
|
||||
switch e {
|
||||
case AuthMethodPassword, AuthMethodSaml, AuthMethodRecoveryCode:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (e AuthMethod) String() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func (e *AuthMethod) UnmarshalGQL(v any) error {
|
||||
str, ok := v.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("enums must be strings")
|
||||
}
|
||||
|
||||
*e = AuthMethod(str)
|
||||
if !e.IsValid() {
|
||||
return fmt.Errorf("%s is not a valid AuthMethod", str)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e AuthMethod) MarshalGQL(w io.Writer) {
|
||||
fmt.Fprint(w, strconv.Quote(e.String()))
|
||||
}
|
||||
|
||||
func (e *AuthMethod) UnmarshalJSON(b []byte) error {
|
||||
s, err := strconv.Unquote(string(b))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return e.UnmarshalGQL(s)
|
||||
}
|
||||
|
||||
func (e AuthMethod) MarshalJSON() ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
e.MarshalGQL(&buf)
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
type PrincipalType string
|
||||
|
||||
const (
|
||||
PrincipalTypeIdentity PrincipalType = "IDENTITY"
|
||||
PrincipalTypeServiceAccount PrincipalType = "SERVICE_ACCOUNT"
|
||||
)
|
||||
|
||||
var AllPrincipalType = []PrincipalType{
|
||||
PrincipalTypeIdentity,
|
||||
PrincipalTypeServiceAccount,
|
||||
}
|
||||
|
||||
func (e PrincipalType) IsValid() bool {
|
||||
switch e {
|
||||
case PrincipalTypeIdentity, PrincipalTypeServiceAccount:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (e PrincipalType) String() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func (e *PrincipalType) UnmarshalGQL(v any) error {
|
||||
str, ok := v.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("enums must be strings")
|
||||
}
|
||||
|
||||
*e = PrincipalType(str)
|
||||
if !e.IsValid() {
|
||||
return fmt.Errorf("%s is not a valid PrincipalType", str)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e PrincipalType) MarshalGQL(w io.Writer) {
|
||||
fmt.Fprint(w, strconv.Quote(e.String()))
|
||||
}
|
||||
|
||||
func (e *PrincipalType) UnmarshalJSON(b []byte) error {
|
||||
s, err := strconv.Unquote(string(b))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return e.UnmarshalGQL(s)
|
||||
}
|
||||
|
||||
func (e PrincipalType) MarshalJSON() ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
e.MarshalGQL(&buf)
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
type ProvisioningSource string
|
||||
|
||||
const (
|
||||
ProvisioningSourceManual ProvisioningSource = "MANUAL"
|
||||
ProvisioningSourceInvitation ProvisioningSource = "INVITATION"
|
||||
ProvisioningSourceSaml ProvisioningSource = "SAML"
|
||||
)
|
||||
|
||||
var AllProvisioningSource = []ProvisioningSource{
|
||||
ProvisioningSourceManual,
|
||||
ProvisioningSourceInvitation,
|
||||
ProvisioningSourceSaml,
|
||||
}
|
||||
|
||||
func (e ProvisioningSource) IsValid() bool {
|
||||
switch e {
|
||||
case ProvisioningSourceManual, ProvisioningSourceInvitation, ProvisioningSourceSaml:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (e ProvisioningSource) String() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func (e *ProvisioningSource) UnmarshalGQL(v any) error {
|
||||
str, ok := v.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("enums must be strings")
|
||||
}
|
||||
|
||||
*e = ProvisioningSource(str)
|
||||
if !e.IsValid() {
|
||||
return fmt.Errorf("%s is not a valid ProvisioningSource", str)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e ProvisioningSource) MarshalGQL(w io.Writer) {
|
||||
fmt.Fprint(w, strconv.Quote(e.String()))
|
||||
}
|
||||
|
||||
func (e *ProvisioningSource) UnmarshalJSON(b []byte) error {
|
||||
s, err := strconv.Unquote(string(b))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return e.UnmarshalGQL(s)
|
||||
}
|
||||
|
||||
func (e ProvisioningSource) MarshalJSON() ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
e.MarshalGQL(&buf)
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
type SessionRequirement string
|
||||
|
||||
const (
|
||||
SessionRequirementPresent SessionRequirement = "PRESENT"
|
||||
SessionRequirementNone SessionRequirement = "NONE"
|
||||
SessionRequirementOptional SessionRequirement = "OPTIONAL"
|
||||
)
|
||||
|
||||
var AllSessionRequirement = []SessionRequirement{
|
||||
SessionRequirementPresent,
|
||||
SessionRequirementNone,
|
||||
SessionRequirementOptional,
|
||||
}
|
||||
|
||||
func (e SessionRequirement) IsValid() bool {
|
||||
switch e {
|
||||
case SessionRequirementPresent, SessionRequirementNone, SessionRequirementOptional:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (e SessionRequirement) String() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func (e *SessionRequirement) UnmarshalGQL(v any) error {
|
||||
str, ok := v.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("enums must be strings")
|
||||
}
|
||||
|
||||
*e = SessionRequirement(str)
|
||||
if !e.IsValid() {
|
||||
return fmt.Errorf("%s is not a valid SessionRequirement", str)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e SessionRequirement) MarshalGQL(w io.Writer) {
|
||||
fmt.Fprint(w, strconv.Quote(e.String()))
|
||||
}
|
||||
|
||||
func (e *SessionRequirement) UnmarshalJSON(b []byte) error {
|
||||
s, err := strconv.Unquote(string(b))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return e.UnmarshalGQL(s)
|
||||
}
|
||||
|
||||
func (e SessionRequirement) MarshalJSON() ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
e.MarshalGQL(&buf)
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
type TokenScope string
|
||||
|
||||
const (
|
||||
TokenScopeReadOrganization TokenScope = "READ_ORGANIZATION"
|
||||
TokenScopeWriteOrganization TokenScope = "WRITE_ORGANIZATION"
|
||||
TokenScopeReadCompliance TokenScope = "READ_COMPLIANCE"
|
||||
TokenScopeWriteCompliance TokenScope = "WRITE_COMPLIANCE"
|
||||
TokenScopeReadRisk TokenScope = "READ_RISK"
|
||||
TokenScopeWriteRisk TokenScope = "WRITE_RISK"
|
||||
TokenScopeReadVendor TokenScope = "READ_VENDOR"
|
||||
TokenScopeWriteVendor TokenScope = "WRITE_VENDOR"
|
||||
TokenScopeReadDocuments TokenScope = "READ_DOCUMENTS"
|
||||
TokenScopeWriteDocuments TokenScope = "WRITE_DOCUMENTS"
|
||||
TokenScopeReadTrustCenter TokenScope = "READ_TRUST_CENTER"
|
||||
TokenScopeWriteTrustCenter TokenScope = "WRITE_TRUST_CENTER"
|
||||
TokenScopeAdmin TokenScope = "ADMIN"
|
||||
)
|
||||
|
||||
var AllTokenScope = []TokenScope{
|
||||
TokenScopeReadOrganization,
|
||||
TokenScopeWriteOrganization,
|
||||
TokenScopeReadCompliance,
|
||||
TokenScopeWriteCompliance,
|
||||
TokenScopeReadRisk,
|
||||
TokenScopeWriteRisk,
|
||||
TokenScopeReadVendor,
|
||||
TokenScopeWriteVendor,
|
||||
TokenScopeReadDocuments,
|
||||
TokenScopeWriteDocuments,
|
||||
TokenScopeReadTrustCenter,
|
||||
TokenScopeWriteTrustCenter,
|
||||
TokenScopeAdmin,
|
||||
}
|
||||
|
||||
func (e TokenScope) IsValid() bool {
|
||||
switch e {
|
||||
case TokenScopeReadOrganization, TokenScopeWriteOrganization, TokenScopeReadCompliance, TokenScopeWriteCompliance, TokenScopeReadRisk, TokenScopeWriteRisk, TokenScopeReadVendor, TokenScopeWriteVendor, TokenScopeReadDocuments, TokenScopeWriteDocuments, TokenScopeReadTrustCenter, TokenScopeWriteTrustCenter, TokenScopeAdmin:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (e TokenScope) String() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func (e *TokenScope) UnmarshalGQL(v any) error {
|
||||
str, ok := v.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("enums must be strings")
|
||||
}
|
||||
|
||||
*e = TokenScope(str)
|
||||
if !e.IsValid() {
|
||||
return fmt.Errorf("%s is not a valid TokenScope", str)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e TokenScope) MarshalGQL(w io.Writer) {
|
||||
fmt.Fprint(w, strconv.Quote(e.String()))
|
||||
}
|
||||
|
||||
func (e *TokenScope) UnmarshalJSON(b []byte) error {
|
||||
s, err := strconv.Unquote(string(b))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return e.UnmarshalGQL(s)
|
||||
}
|
||||
|
||||
func (e TokenScope) MarshalJSON() ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
e.MarshalGQL(&buf)
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
930
pkg/server/api/connect/v1/v1_resolver.go
Normal file
930
pkg/server/api/connect/v1/v1_resolver.go
Normal file
@@ -0,0 +1,930 @@
|
||||
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.84
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/vektah/gqlparser/v2/gqlerror"
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/iam"
|
||||
"go.probo.inc/probo/pkg/page"
|
||||
"go.probo.inc/probo/pkg/securecookie"
|
||||
"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"
|
||||
"go.probo.inc/probo/pkg/server/gqlutils/types/cursor"
|
||||
)
|
||||
|
||||
// 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) (*types.MembershipConnection, error) {
|
||||
pageOrderBy := page.OrderBy[coredata.MembershipOrderField]{
|
||||
Field: coredata.MembershipOrderFieldCreatedAt,
|
||||
Direction: page.OrderDirectionDesc,
|
||||
}
|
||||
|
||||
cursor := cursor.NewCursor(first, after, last, before, pageOrderBy)
|
||||
|
||||
page, err := r.iam.AccountService.ListMemberships(ctx, obj.ID, cursor)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot list memberships: %w", err))
|
||||
}
|
||||
|
||||
return types.NewMembershipConnection(page, r, obj.ID), nil
|
||||
}
|
||||
|
||||
// PendingInvitations is the resolver for the pendingInvitations field.
|
||||
func (r *identityResolver) PendingInvitations(ctx context.Context, obj *types.Identity, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.InvitationConnection, error) {
|
||||
pageOrderBy := page.OrderBy[coredata.InvitationOrderField]{
|
||||
Field: coredata.InvitationOrderFieldCreatedAt,
|
||||
Direction: page.OrderDirectionDesc,
|
||||
}
|
||||
|
||||
cursor := cursor.NewCursor(first, after, last, before, pageOrderBy)
|
||||
|
||||
page, err := r.iam.AccountService.ListPendingInvitations(ctx, obj.ID, cursor)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot list pending invitations: %w", err))
|
||||
}
|
||||
|
||||
return types.NewInvitationConnection(page, r, obj.ID, nil), nil
|
||||
}
|
||||
|
||||
// Sessions is the resolver for the sessions field.
|
||||
func (r *identityResolver) Sessions(ctx context.Context, obj *types.Identity, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.SessionOrder) (*types.SessionConnection, error) {
|
||||
pageOrderBy := page.OrderBy[coredata.SessionOrderField]{
|
||||
Field: coredata.SessionOrderFieldCreatedAt,
|
||||
Direction: page.OrderDirectionDesc,
|
||||
}
|
||||
if orderBy != nil {
|
||||
pageOrderBy = page.OrderBy[coredata.SessionOrderField]{
|
||||
Field: orderBy.Field,
|
||||
Direction: orderBy.Direction,
|
||||
}
|
||||
}
|
||||
|
||||
cursor := cursor.NewCursor(first, after, last, before, pageOrderBy)
|
||||
|
||||
page, err := r.iam.AccountService.ListSessions(ctx, obj.ID, cursor)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot list sessions: %w", err))
|
||||
}
|
||||
|
||||
return types.NewSessionConnection(page, r, obj.ID), nil
|
||||
}
|
||||
|
||||
// PersonalAPIKeys is the resolver for the personalAPIKeys field.
|
||||
func (r *identityResolver) PersonalAPIKeys(ctx context.Context, obj *types.Identity, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.PersonalAPIKeyConnection, error) {
|
||||
pageOrderBy := page.OrderBy[coredata.UserAPIKeyOrderField]{
|
||||
Field: coredata.UserAPIKeyOrderFieldCreatedAt,
|
||||
Direction: page.OrderDirectionDesc,
|
||||
}
|
||||
|
||||
cursor := cursor.NewCursor(first, after, last, before, pageOrderBy)
|
||||
|
||||
page, err := r.iam.AccountService.ListPersonalAPIKeys(ctx, obj.ID, cursor)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot list personal api keys: %w", err))
|
||||
}
|
||||
|
||||
return types.NewPersonalAPIKeyConnection(page, r, obj.ID), nil
|
||||
}
|
||||
|
||||
// TotalCount is the resolver for the totalCount field.
|
||||
func (r *invitationConnectionResolver) TotalCount(ctx context.Context, obj *types.InvitationConnection) (int, error) {
|
||||
switch obj.Resolver.(type) {
|
||||
case *organizationResolver:
|
||||
count, err := r.iam.OrganizationService.CountInvitations(ctx, obj.ParentID, obj.Filters)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot count invitations: %w", err))
|
||||
}
|
||||
return count, nil
|
||||
case *identityResolver:
|
||||
count, err := r.iam.AccountService.CountPendingInvitations(ctx, obj.ParentID)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot count invitations: %w", err))
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
panic(fmt.Errorf("unsupported resolver: %T", obj.Resolver))
|
||||
}
|
||||
|
||||
// Identity is the resolver for the identity field.
|
||||
func (r *membershipResolver) Identity(ctx context.Context, obj *types.Membership) (*types.Identity, error) {
|
||||
identity, err := r.iam.AccountService.GetIdentityForMembership(ctx, obj.ID)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot get identity: %w", err))
|
||||
}
|
||||
|
||||
return types.NewIdentity(identity), nil
|
||||
}
|
||||
|
||||
// Organization is the resolver for the organization field.
|
||||
func (r *membershipResolver) Organization(ctx context.Context, obj *types.Membership) (*types.Organization, error) {
|
||||
organization, err := r.iam.OrganizationService.GetOrganizationForMembership(ctx, obj.ID)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot get organization for membership: %w", err))
|
||||
}
|
||||
|
||||
return types.NewOrganization(organization), nil
|
||||
}
|
||||
|
||||
// TotalCount is the resolver for the totalCount field.
|
||||
func (r *membershipConnectionResolver) TotalCount(ctx context.Context, obj *types.MembershipConnection) (int, error) {
|
||||
switch obj.Resolver.(type) {
|
||||
case *identityResolver:
|
||||
count, err := r.iam.AccountService.CountMemberships(ctx, obj.ParentID)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot count memberships: %w", err))
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
panic(fmt.Errorf("unsupported resolver: %T", obj.Resolver))
|
||||
}
|
||||
|
||||
// SignIn is the resolver for the signIn field.
|
||||
func (r *mutationResolver) SignIn(ctx context.Context, input types.SignInInput) (*types.SignInPayload, error) {
|
||||
user, session, err := r.iam.AuthService.OpenSessionWithPassword(ctx, input.Email, input.Password)
|
||||
if err != nil {
|
||||
var ErrInvalidCredentials *iam.ErrInvalidCredentials
|
||||
if errors.As(err, &ErrInvalidCredentials) {
|
||||
return nil, &gqlerror.Error{
|
||||
Message: err.Error(),
|
||||
Extensions: map[string]any{
|
||||
"code": "INVALID_CREDENTIALS",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// TODO handle error properly here
|
||||
panic(fmt.Errorf("cannot sign in: %w", err))
|
||||
}
|
||||
|
||||
w := HTTPResponseWriterFromContext(ctx)
|
||||
securecookie.Set(
|
||||
w,
|
||||
r.sessionCookieConfig(time.Until(session.ExpiredAt)),
|
||||
session.ID.String(),
|
||||
)
|
||||
|
||||
return &types.SignInPayload{
|
||||
Identity: &types.Identity{
|
||||
ID: user.ID,
|
||||
Email: user.EmailAddress,
|
||||
EmailVerified: user.EmailAddressVerified,
|
||||
CreatedAt: user.CreatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SignUp is the resolver for the signUp field.
|
||||
func (r *mutationResolver) SignUp(ctx context.Context, input types.SignUpInput) (*types.SignUpPayload, error) {
|
||||
identity, session, err := r.iam.AuthService.CreateIdentityWithPassword(
|
||||
ctx,
|
||||
&iam.CreateIdentityWithPasswordRequest{
|
||||
Email: input.Email,
|
||||
Password: input.Password,
|
||||
FullName: input.FullName,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
// TODO handle error properly here
|
||||
panic(fmt.Errorf("cannot create identity with password: %w", err))
|
||||
}
|
||||
|
||||
w := HTTPResponseWriterFromContext(ctx)
|
||||
securecookie.Set(
|
||||
w,
|
||||
r.sessionCookieConfig(time.Until(session.ExpiredAt)),
|
||||
session.ID.String(),
|
||||
)
|
||||
|
||||
return &types.SignUpPayload{
|
||||
Identity: types.NewIdentity(identity),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SignOut is the resolver for the signOut field.
|
||||
func (r *mutationResolver) SignOut(ctx context.Context) (*types.SignOutPayload, error) {
|
||||
session := SessionFromContext(ctx)
|
||||
|
||||
err := r.iam.SessionService.CloseSession(ctx, session.ID)
|
||||
if err != nil {
|
||||
var ErrSessionNotFound *iam.ErrSessionNotFound
|
||||
if errors.As(err, &ErrSessionNotFound) {
|
||||
return &types.SignOutPayload{}, nil
|
||||
}
|
||||
|
||||
panic(fmt.Errorf("cannot close session: %w", err))
|
||||
}
|
||||
|
||||
return &types.SignOutPayload{Success: true}, nil
|
||||
}
|
||||
|
||||
// SignUpFromInvitation is the resolver for the signUpFromInvitation field.
|
||||
func (r *mutationResolver) SignUpFromInvitation(ctx context.Context, input types.SignUpFromInvitationInput) (*types.SignUpFromInvitationPayload, error) {
|
||||
identity, session, err := r.iam.AuthService.CreateIdentityFromInvitation(
|
||||
ctx,
|
||||
&iam.CreateIdentityFromInvitationRequest{
|
||||
InvitationToken: input.Token,
|
||||
Password: input.Password,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
// TODO handle error properly here
|
||||
panic(fmt.Errorf("cannot create identity from invitation: %w", err))
|
||||
}
|
||||
|
||||
w := HTTPResponseWriterFromContext(ctx)
|
||||
securecookie.Set(
|
||||
w,
|
||||
r.sessionCookieConfig(time.Until(session.ExpiredAt)),
|
||||
session.ID.String(),
|
||||
)
|
||||
|
||||
return &types.SignUpFromInvitationPayload{
|
||||
Identity: &types.Identity{
|
||||
ID: identity.ID,
|
||||
Email: identity.EmailAddress,
|
||||
EmailVerified: identity.EmailAddressVerified,
|
||||
CreatedAt: identity.CreatedAt,
|
||||
UpdatedAt: identity.UpdatedAt,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ForgotPassword is the resolver for the forgotPassword field.
|
||||
func (r *mutationResolver) ForgotPassword(ctx context.Context, input types.ForgotPasswordInput) (*types.ForgotPasswordPayload, error) {
|
||||
err := r.iam.AuthService.SendPasswordResetInstructionByEmail(
|
||||
ctx,
|
||||
input.Email,
|
||||
)
|
||||
if err != nil {
|
||||
// TODO handle error properly here
|
||||
panic(fmt.Errorf("cannot send password reset instruction by email: %w", err))
|
||||
}
|
||||
|
||||
return &types.ForgotPasswordPayload{
|
||||
Success: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ResetPassword is the resolver for the resetPassword field.
|
||||
func (r *mutationResolver) ResetPassword(ctx context.Context, input types.ResetPasswordInput) (*types.ResetPasswordPayload, error) {
|
||||
err := r.iam.AuthService.ResetPassword(
|
||||
ctx,
|
||||
&iam.ResetPasswordRequest{
|
||||
Token: input.Token,
|
||||
Password: input.Password,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
var errInvalidToken *iam.ErrInvalidToken
|
||||
if errors.As(err, &errInvalidToken) {
|
||||
return nil, gqlutils.Invalid(err, nil)
|
||||
}
|
||||
|
||||
panic(fmt.Errorf("cannot reset password: %w", err))
|
||||
}
|
||||
|
||||
return &types.ResetPasswordPayload{
|
||||
Success: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// VerifyEmail is the resolver for the verifyEmail field.
|
||||
func (r *mutationResolver) VerifyEmail(ctx context.Context, input types.VerifyEmailInput) (*types.VerifyEmailPayload, error) {
|
||||
err := r.iam.AccountService.VerifyEmail(ctx, input.Token)
|
||||
if err != nil {
|
||||
// TODO handle error properly here
|
||||
panic(fmt.Errorf("cannot verify email: %w", err))
|
||||
}
|
||||
|
||||
return &types.VerifyEmailPayload{
|
||||
Success: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ChangePassword is the resolver for the changePassword field.
|
||||
func (r *mutationResolver) ChangePassword(ctx context.Context, input types.ChangePasswordInput) (*types.ChangePasswordPayload, error) {
|
||||
identity := UserFromContext(ctx)
|
||||
|
||||
err := r.iam.AccountService.ChangePassword(
|
||||
ctx,
|
||||
identity.ID,
|
||||
&iam.ChangePasswordRequest{
|
||||
CurrentPassword: input.CurrentPassword,
|
||||
NewPassword: input.NewPassword,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
// TODO handle error properly here
|
||||
panic(fmt.Errorf("cannot change password: %w", err))
|
||||
}
|
||||
|
||||
return &types.ChangePasswordPayload{
|
||||
Success: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ChangeEmail is the resolver for the changeEmail field.
|
||||
func (r *mutationResolver) ChangeEmail(ctx context.Context, input types.ChangeEmailInput) (*types.ChangeEmailPayload, error) {
|
||||
identity := UserFromContext(ctx)
|
||||
|
||||
err := r.iam.AccountService.ChangeEmail(
|
||||
ctx,
|
||||
identity.ID,
|
||||
&iam.ChangeEmailRequest{
|
||||
NewEmail: input.NewEmail,
|
||||
Password: input.Password,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
// TODO handle error properly here
|
||||
panic(fmt.Errorf("cannot change email: %w", err))
|
||||
}
|
||||
|
||||
return &types.ChangeEmailPayload{
|
||||
Success: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UpdateIdentityProfile is the resolver for the updateIdentityProfile field.
|
||||
func (r *mutationResolver) UpdateIdentityProfile(ctx context.Context, input types.UpdateIdentityProfileInput) (*types.UpdateIdentityProfilePayload, error) {
|
||||
panic(fmt.Errorf("not implemented: UpdateIdentityProfile - updateIdentityProfile"))
|
||||
}
|
||||
|
||||
// RevokeSession is the resolver for the revokeSession field.
|
||||
func (r *mutationResolver) RevokeSession(ctx context.Context, input types.RevokeSessionInput) (*types.RevokeSessionPayload, error) {
|
||||
identity := UserFromContext(ctx)
|
||||
|
||||
err := r.iam.SessionService.RevokeSession(ctx, identity.ID, input.SessionID)
|
||||
if err != nil {
|
||||
var ErrSessionExpired *iam.ErrSessionExpired
|
||||
if errors.As(err, &ErrSessionExpired) {
|
||||
return &types.RevokeSessionPayload{Success: true}, nil
|
||||
}
|
||||
|
||||
panic(fmt.Errorf("cannot revoke session: %w", err))
|
||||
}
|
||||
|
||||
return &types.RevokeSessionPayload{Success: true}, nil
|
||||
}
|
||||
|
||||
// RevokeAllSessions is the resolver for the revokeAllSessions field.
|
||||
func (r *mutationResolver) RevokeAllSessions(ctx context.Context) (*types.RevokeAllSessionsPayload, error) {
|
||||
session := SessionFromContext(ctx)
|
||||
|
||||
revokedCount, err := r.iam.SessionService.RevokeAllSessions(ctx, session.ID)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot revoke all sessions: %w", err))
|
||||
}
|
||||
|
||||
return &types.RevokeAllSessionsPayload{RevokedCount: int(revokedCount)}, nil
|
||||
}
|
||||
|
||||
// CreatePersonalAPIKey is the resolver for the createPersonalAPIKey field.
|
||||
func (r *mutationResolver) CreatePersonalAPIKey(ctx context.Context, input types.CreatePersonalAPIKeyInput) (*types.CreatePersonalAPIKeyPayload, error) {
|
||||
identity := UserFromContext(ctx)
|
||||
|
||||
userAPIKey, token, err := r.iam.AccountService.CreatePersonalAPIKey(
|
||||
ctx,
|
||||
identity.ID,
|
||||
input.Name,
|
||||
input.ExpiresAt,
|
||||
)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot create personal api key: %w", err))
|
||||
}
|
||||
|
||||
return &types.CreatePersonalAPIKeyPayload{
|
||||
PersonalAPIKeyEdge: types.NewPersonalAPIKeyEdge(userAPIKey, coredata.UserAPIKeyOrderFieldCreatedAt),
|
||||
Token: token,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UpdatePersonalAPIKey is the resolver for the updatePersonalAPIKey field.
|
||||
func (r *mutationResolver) UpdatePersonalAPIKey(ctx context.Context, input types.UpdatePersonalAPIKeyInput) (*types.UpdatePersonalAPIKeyPayload, error) {
|
||||
panic(fmt.Errorf("not implemented: UpdatePersonalAPIKey - updatePersonalAPIKey"))
|
||||
}
|
||||
|
||||
// RevokePersonalAPIKey is the resolver for the revokePersonalAPIKey field.
|
||||
func (r *mutationResolver) RevokePersonalAPIKey(ctx context.Context, input types.RevokePersonalAPIKeyInput) (*types.RevokePersonalAPIKeyPayload, error) {
|
||||
identity := UserFromContext(ctx)
|
||||
|
||||
err := r.iam.AccountService.DeletePersonalAPIKey(ctx, identity.ID, input.TokenID)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot delete personal api key: %w", err))
|
||||
}
|
||||
|
||||
return &types.RevokePersonalAPIKeyPayload{Success: true}, nil
|
||||
}
|
||||
|
||||
// CreateOrganization is the resolver for the createOrganization field.
|
||||
func (r *mutationResolver) CreateOrganization(ctx context.Context, input types.CreateOrganizationInput) (*types.CreateOrganizationPayload, error) {
|
||||
identity := UserFromContext(ctx)
|
||||
|
||||
var (
|
||||
logoFile *iam.UploadedFile
|
||||
horizontalLogoFile *iam.UploadedFile
|
||||
)
|
||||
|
||||
if input.LogoFile != nil {
|
||||
logoFile = &iam.UploadedFile{
|
||||
Content: input.LogoFile.File,
|
||||
Filename: input.LogoFile.Filename,
|
||||
ContentType: input.LogoFile.ContentType,
|
||||
Size: input.LogoFile.Size,
|
||||
}
|
||||
}
|
||||
|
||||
if input.HorizontalLogoFile != nil {
|
||||
horizontalLogoFile = &iam.UploadedFile{
|
||||
Content: input.HorizontalLogoFile.File,
|
||||
Filename: input.HorizontalLogoFile.Filename,
|
||||
ContentType: input.HorizontalLogoFile.ContentType,
|
||||
Size: input.HorizontalLogoFile.Size,
|
||||
}
|
||||
}
|
||||
organization, err := r.iam.OrganizationService.CreateOrganization(
|
||||
ctx,
|
||||
identity.ID,
|
||||
&iam.CreateOrganizationRequest{
|
||||
Name: input.Name,
|
||||
LogoFile: logoFile,
|
||||
HorizontalLogoFile: horizontalLogoFile,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot create organization: %w", err))
|
||||
}
|
||||
|
||||
return &types.CreateOrganizationPayload{
|
||||
Organization: types.NewOrganization(organization),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UpdateOrganization is the resolver for the updateOrganization field.
|
||||
func (r *mutationResolver) UpdateOrganization(ctx context.Context, input types.UpdateOrganizationInput) (*types.UpdateOrganizationPayload, error) {
|
||||
organization, err := r.iam.OrganizationService.UpdateOrganization(
|
||||
ctx,
|
||||
input.OrganizationID,
|
||||
&iam.UpdateOrganizationRequest{
|
||||
Name: input.Name,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot update organization: %w", err))
|
||||
}
|
||||
|
||||
return &types.UpdateOrganizationPayload{
|
||||
Organization: &types.Organization{
|
||||
ID: organization.ID,
|
||||
Name: organization.Name,
|
||||
CreatedAt: organization.CreatedAt,
|
||||
UpdatedAt: organization.UpdatedAt,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DeleteOrganization is the resolver for the deleteOrganization field.
|
||||
func (r *mutationResolver) DeleteOrganization(ctx context.Context, input types.DeleteOrganizationInput) (*types.DeleteOrganizationPayload, error) {
|
||||
err := r.iam.OrganizationService.DeleteOrganization(ctx, input.OrganizationID)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot delete organization: %w", err))
|
||||
}
|
||||
|
||||
return &types.DeleteOrganizationPayload{DeletedOrganizationID: input.OrganizationID}, nil
|
||||
}
|
||||
|
||||
// InviteMember is the resolver for the inviteMember field.
|
||||
func (r *mutationResolver) InviteMember(ctx context.Context, input types.InviteMemberInput) (*types.InviteMemberPayload, error) {
|
||||
invitation, err := r.iam.OrganizationService.InviteMember(
|
||||
ctx,
|
||||
input.OrganizationID,
|
||||
input.Email,
|
||||
input.FullName,
|
||||
coredata.MembershipRoleViewer,
|
||||
)
|
||||
if err != nil {
|
||||
var errOrganizationNotFound *iam.ErrOrganizationNotFound
|
||||
var errMembershipAlreadyExists *iam.ErrMembershipAlreadyExists
|
||||
|
||||
if errors.As(err, &errOrganizationNotFound) {
|
||||
return nil, gqlutils.NotFound(err)
|
||||
}
|
||||
|
||||
if errors.As(err, &errMembershipAlreadyExists) {
|
||||
return nil, gqlutils.Conflict(err)
|
||||
}
|
||||
|
||||
panic(fmt.Errorf("cannot add member to organization: %w", err))
|
||||
}
|
||||
|
||||
return &types.InviteMemberPayload{
|
||||
InvitationEdge: types.NewInvitationEdge(invitation, coredata.InvitationOrderFieldCreatedAt),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DeleteInvitation is the resolver for the deleteInvitation field.
|
||||
func (r *mutationResolver) DeleteInvitation(ctx context.Context, input types.DeleteInvitationInput) (*types.DeleteInvitationPayload, error) {
|
||||
err := r.iam.OrganizationService.DeleteInvitation(ctx, input.OrganizationID, input.InvitationID)
|
||||
if err != nil {
|
||||
var errInvitationNotFound *iam.ErrInvitationNotFound
|
||||
var errInvitationNotPending *iam.ErrInvitationNotPending
|
||||
|
||||
if errors.As(err, &errInvitationNotFound) {
|
||||
return nil, gqlutils.NotFound(err)
|
||||
}
|
||||
|
||||
if errors.As(err, &errInvitationNotPending) {
|
||||
return nil, gqlutils.Invalid(err, nil)
|
||||
}
|
||||
|
||||
panic(fmt.Errorf("cannot delete invitation: %w", err))
|
||||
}
|
||||
|
||||
return &types.DeleteInvitationPayload{DeletedInvitationID: input.InvitationID}, nil
|
||||
}
|
||||
|
||||
// RemoveMember is the resolver for the removeMember field.
|
||||
func (r *mutationResolver) RemoveMember(ctx context.Context, input types.RemoveMemberInput) (*types.RemoveMemberPayload, error) {
|
||||
err := r.iam.OrganizationService.RemoveMember(ctx, input.OrganizationID, input.MembershipID)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot remove member from organization: %w", err))
|
||||
}
|
||||
|
||||
return &types.RemoveMemberPayload{DeletedMembershipID: input.MembershipID}, nil
|
||||
}
|
||||
|
||||
// AcceptInvitation is the resolver for the acceptInvitation field.
|
||||
func (r *mutationResolver) AcceptInvitation(ctx context.Context, input types.AcceptInvitationInput) (*types.AcceptInvitationPayload, error) {
|
||||
identity := UserFromContext(ctx)
|
||||
|
||||
membership, err := r.iam.AccountService.AcceptInvitation(ctx, identity.ID, input.InvitationID)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot accept invitation: %w", err))
|
||||
}
|
||||
|
||||
return &types.AcceptInvitationPayload{
|
||||
MembershipEdge: types.NewMembershipEdge(membership, coredata.MembershipOrderFieldCreatedAt),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateSAMLConfiguration is the resolver for the createSAMLConfiguration field.
|
||||
func (r *mutationResolver) CreateSAMLConfiguration(ctx context.Context, input types.CreateSAMLConfigurationInput) (*types.CreateSAMLConfigurationPayload, error) {
|
||||
req := &iam.CreateSAMLConfigurationRequest{
|
||||
EmailDomain: input.EmailDomain,
|
||||
IdPEntityID: input.IdpEntityID,
|
||||
IdPSsoURL: input.IdpSsoURL,
|
||||
IdPCertificate: input.IdpCertificate,
|
||||
AutoSignupEnabled: input.AutoSignupEnabled,
|
||||
}
|
||||
|
||||
if input.AttributeMappings != nil {
|
||||
req.AttributeEmail = input.AttributeMappings.Email
|
||||
req.AttributeFirstname = input.AttributeMappings.FirstName
|
||||
req.AttributeLastname = input.AttributeMappings.LastName
|
||||
req.AttributeRole = input.AttributeMappings.Role
|
||||
}
|
||||
|
||||
samlConfiguration, err := r.iam.OrganizationService.CreateSAMLConfiguration(
|
||||
ctx,
|
||||
input.OrganizationID,
|
||||
req,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot create saml configuration: %w", err))
|
||||
}
|
||||
|
||||
return &types.CreateSAMLConfigurationPayload{
|
||||
SamlConfigurationEdge: types.NewSAMLConfigurationEdge(
|
||||
samlConfiguration,
|
||||
coredata.SAMLConfigurationOrderFieldCreatedAt,
|
||||
),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// UpdateSAMLConfiguration is the resolver for the updateSAMLConfiguration field.
|
||||
func (r *mutationResolver) UpdateSAMLConfiguration(ctx context.Context, input types.UpdateSAMLConfigurationInput) (*types.UpdateSAMLConfigurationPayload, error) {
|
||||
req := &iam.UpdateSAMLConfigurationRequest{
|
||||
IdPEntityID: input.IdpEntityID,
|
||||
IdPSsoURL: input.IdpSsoURL,
|
||||
IdPCertificate: input.IdpCertificate,
|
||||
AutoSignupEnabled: input.AutoSignupEnabled,
|
||||
}
|
||||
|
||||
if input.AttributeMappings != nil {
|
||||
req.AttributeEmail = input.AttributeMappings.Email
|
||||
req.AttributeFirstname = input.AttributeMappings.FirstName
|
||||
req.AttributeLastname = input.AttributeMappings.LastName
|
||||
req.AttributeRole = input.AttributeMappings.Role
|
||||
}
|
||||
|
||||
samlConfiguration, err := r.iam.OrganizationService.UpdateSAMLConfiguration(
|
||||
ctx,
|
||||
input.OrganizationID,
|
||||
input.SamlConfigurationID,
|
||||
req,
|
||||
)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot update saml configuration: %w", err))
|
||||
}
|
||||
|
||||
return &types.UpdateSAMLConfigurationPayload{
|
||||
SamlConfiguration: types.NewSAMLConfiguration(samlConfiguration),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DeleteSAMLConfiguration is the resolver for the deleteSAMLConfiguration field.
|
||||
func (r *mutationResolver) DeleteSAMLConfiguration(ctx context.Context, input types.DeleteSAMLConfigurationInput) (*types.DeleteSAMLConfigurationPayload, error) {
|
||||
err := r.iam.OrganizationService.DeleteSAMLConfiguration(ctx, input.OrganizationID, input.SamlConfigurationID)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot delete saml configuration: %w", err))
|
||||
}
|
||||
|
||||
return &types.DeleteSAMLConfigurationPayload{DeletedSamlConfigurationID: input.SamlConfigurationID}, nil
|
||||
}
|
||||
|
||||
// LogoURL is the resolver for the logoUrl field.
|
||||
func (r *organizationResolver) LogoURL(ctx context.Context, obj *types.Organization) (*string, error) {
|
||||
presignedURL, err := r.iam.OrganizationService.GenerateLogoURL(ctx, obj.ID, 1*time.Hour)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot generate logo URL: %w", err))
|
||||
}
|
||||
|
||||
return presignedURL, nil
|
||||
}
|
||||
|
||||
// HorizontalLogoURL is the resolver for the horizontalLogoUrl field.
|
||||
func (r *organizationResolver) HorizontalLogoURL(ctx context.Context, obj *types.Organization) (*string, error) {
|
||||
presignedURL, err := r.iam.OrganizationService.GenerateHorizontalLogoURL(ctx, obj.ID, 1*time.Hour)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot generate horizontal logo URL: %w", err))
|
||||
}
|
||||
|
||||
return presignedURL, nil
|
||||
}
|
||||
|
||||
// Members is the resolver for the members field.
|
||||
func (r *organizationResolver) Members(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.MembershipConnection, error) {
|
||||
pageOrderBy := page.OrderBy[coredata.MembershipOrderField]{
|
||||
Field: coredata.MembershipOrderFieldCreatedAt,
|
||||
Direction: page.OrderDirectionDesc,
|
||||
}
|
||||
|
||||
cursor := cursor.NewCursor(first, after, last, before, pageOrderBy)
|
||||
|
||||
page, err := r.iam.OrganizationService.ListMembers(ctx, obj.ID, cursor)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot list memberships: %w", err))
|
||||
}
|
||||
|
||||
return types.NewMembershipConnection(page, r, obj.ID), nil
|
||||
}
|
||||
|
||||
// Invitations is the resolver for the invitations field.
|
||||
func (r *organizationResolver) Invitations(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, status *coredata.InvitationStatus) (*types.InvitationConnection, error) {
|
||||
pageOrderBy := page.OrderBy[coredata.InvitationOrderField]{
|
||||
Field: coredata.InvitationOrderFieldCreatedAt,
|
||||
Direction: page.OrderDirectionDesc,
|
||||
}
|
||||
|
||||
filters := coredata.NewInvitationFilter(nil)
|
||||
if status != nil {
|
||||
filters = coredata.NewInvitationFilter([]coredata.InvitationStatus{*status})
|
||||
}
|
||||
|
||||
cursor := cursor.NewCursor(first, after, last, before, pageOrderBy)
|
||||
|
||||
page, err := r.iam.OrganizationService.ListInvitations(ctx, obj.ID, cursor, filters)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot list invitations: %w", err))
|
||||
}
|
||||
|
||||
return types.NewInvitationConnection(page, r, obj.ID, filters), nil
|
||||
}
|
||||
|
||||
// SamlConfigurations is the resolver for the samlConfigurations field.
|
||||
func (r *organizationResolver) SamlConfigurations(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.SAMLConfigurationConnection, error) {
|
||||
pageOrderBy := page.OrderBy[coredata.SAMLConfigurationOrderField]{
|
||||
Field: coredata.SAMLConfigurationOrderFieldCreatedAt,
|
||||
Direction: page.OrderDirectionDesc,
|
||||
}
|
||||
|
||||
cursor := cursor.NewCursor(first, after, last, before, pageOrderBy)
|
||||
|
||||
page, err := r.iam.OrganizationService.ListSAMLConfigurations(ctx, obj.ID, cursor)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot list saml configurations: %w", err))
|
||||
}
|
||||
|
||||
return types.NewSAMLConfigurationConnection(page, r, obj.ID), nil
|
||||
}
|
||||
|
||||
// TotalCount is the resolver for the totalCount field.
|
||||
func (r *personalAPIKeyConnectionResolver) TotalCount(ctx context.Context, obj *types.PersonalAPIKeyConnection) (int, error) {
|
||||
switch obj.Resolver.(type) {
|
||||
case *identityResolver:
|
||||
count, err := r.iam.AccountService.CountPersonalAPIKeys(ctx, obj.ParentID)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot count personal api keys: %w", err))
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
panic(fmt.Errorf("unsupported resolver: %T", obj.Resolver))
|
||||
}
|
||||
|
||||
// Node is the resolver for the node field.
|
||||
func (r *queryResolver) Node(ctx context.Context, id gid.GID) (types.Node, error) {
|
||||
var loadNode func(ctx context.Context, id gid.GID) (types.Node, error)
|
||||
|
||||
user := UserFromContext(ctx)
|
||||
|
||||
r.iam.AccessManagementService.Authorize(ctx, user.ID, nil, id, iam.ActionGet)
|
||||
|
||||
switch id.EntityType() {
|
||||
case coredata.OrganizationEntityType:
|
||||
loadNode = func(ctx context.Context, id gid.GID) (types.Node, error) {
|
||||
organization, err := r.iam.OrganizationService.GetOrganization(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return types.NewOrganization(organization), nil
|
||||
}
|
||||
case coredata.UserEntityType:
|
||||
loadNode = func(ctx context.Context, id gid.GID) (types.Node, error) {
|
||||
identity, err := r.iam.AccountService.GetIdentity(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return types.NewIdentity(identity), nil
|
||||
}
|
||||
case coredata.SessionEntityType:
|
||||
loadNode = func(ctx context.Context, id gid.GID) (types.Node, error) {
|
||||
session, err := r.iam.GetSession(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return types.NewSession(session), nil
|
||||
}
|
||||
case coredata.MembershipEntityType:
|
||||
loadNode = func(ctx context.Context, id gid.GID) (types.Node, error) {
|
||||
membership, err := r.iam.GetMembership(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return types.NewMembership(membership), nil
|
||||
}
|
||||
case coredata.InvitationEntityType:
|
||||
loadNode = func(ctx context.Context, id gid.GID) (types.Node, error) {
|
||||
invitation, err := r.iam.GetInvitation(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return types.NewInvitation(invitation), nil
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported entity type: %d", id.EntityType())
|
||||
}
|
||||
|
||||
node, err := loadNode(ctx, id)
|
||||
if err != nil {
|
||||
var (
|
||||
errOrganizationNotFound *iam.ErrOrganizationNotFound
|
||||
errIdentityNotFound *iam.ErrUserNotFound
|
||||
errSessionNotFound *iam.ErrSessionNotFound
|
||||
errMembershipNotFound *iam.ErrMembershipNotFound
|
||||
errInvitationNotFound *iam.ErrInvitationNotFound
|
||||
)
|
||||
|
||||
if errors.As(err, &errOrganizationNotFound) ||
|
||||
errors.As(err, &errIdentityNotFound) ||
|
||||
errors.As(err, &errSessionNotFound) ||
|
||||
errors.As(err, &errMembershipNotFound) ||
|
||||
errors.As(err, &errInvitationNotFound) {
|
||||
return nil, gqlutils.NotFound(err)
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return node, nil
|
||||
}
|
||||
|
||||
// Viewer is the resolver for the viewer field.
|
||||
func (r *queryResolver) Viewer(ctx context.Context) (*types.Identity, error) {
|
||||
user := UserFromContext(ctx)
|
||||
|
||||
return &types.Identity{
|
||||
ID: user.ID,
|
||||
Email: user.EmailAddress,
|
||||
EmailVerified: user.EmailAddressVerified,
|
||||
CreatedAt: user.CreatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CheckSSOAvailability is the resolver for the checkSSOAvailability field.
|
||||
func (r *queryResolver) CheckSSOAvailability(ctx context.Context, email string) (*types.SSOAvailability, error) {
|
||||
panic(fmt.Errorf("not implemented: CheckSSOAvailability - checkSSOAvailability"))
|
||||
}
|
||||
|
||||
// TotalCount is the resolver for the totalCount field.
|
||||
func (r *sAMLConfigurationConnectionResolver) TotalCount(ctx context.Context, obj *types.SAMLConfigurationConnection) (int, error) {
|
||||
switch obj.Resolver.(type) {
|
||||
case *organizationResolver:
|
||||
count, err := r.iam.OrganizationService.CountSAMLConfigurations(ctx, obj.ParentID)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot count saml configurations: %w", err))
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
panic(fmt.Errorf("unsupported resolver: %T", obj.Resolver))
|
||||
}
|
||||
|
||||
// TotalCount is the resolver for the totalCount field.
|
||||
func (r *sessionConnectionResolver) TotalCount(ctx context.Context, obj *types.SessionConnection) (int, error) {
|
||||
switch obj.Resolver.(type) {
|
||||
case *identityResolver:
|
||||
count, err := r.iam.AccountService.CountSessions(ctx, obj.ParentID)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot count sessions: %w", err))
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
|
||||
panic(fmt.Errorf("unsupported resolver: %T", obj.Resolver))
|
||||
}
|
||||
|
||||
// Identity returns schema.IdentityResolver implementation.
|
||||
func (r *Resolver) Identity() schema.IdentityResolver { return &identityResolver{r} }
|
||||
|
||||
// InvitationConnection returns schema.InvitationConnectionResolver implementation.
|
||||
func (r *Resolver) InvitationConnection() schema.InvitationConnectionResolver {
|
||||
return &invitationConnectionResolver{r}
|
||||
}
|
||||
|
||||
// Membership returns schema.MembershipResolver implementation.
|
||||
func (r *Resolver) Membership() schema.MembershipResolver { return &membershipResolver{r} }
|
||||
|
||||
// MembershipConnection returns schema.MembershipConnectionResolver implementation.
|
||||
func (r *Resolver) MembershipConnection() schema.MembershipConnectionResolver {
|
||||
return &membershipConnectionResolver{r}
|
||||
}
|
||||
|
||||
// Mutation returns schema.MutationResolver implementation.
|
||||
func (r *Resolver) Mutation() schema.MutationResolver { return &mutationResolver{r} }
|
||||
|
||||
// Organization returns schema.OrganizationResolver implementation.
|
||||
func (r *Resolver) Organization() schema.OrganizationResolver { return &organizationResolver{r} }
|
||||
|
||||
// PersonalAPIKeyConnection returns schema.PersonalAPIKeyConnectionResolver implementation.
|
||||
func (r *Resolver) PersonalAPIKeyConnection() schema.PersonalAPIKeyConnectionResolver {
|
||||
return &personalAPIKeyConnectionResolver{r}
|
||||
}
|
||||
|
||||
// Query returns schema.QueryResolver implementation.
|
||||
func (r *Resolver) Query() schema.QueryResolver { return &queryResolver{r} }
|
||||
|
||||
// SAMLConfigurationConnection returns schema.SAMLConfigurationConnectionResolver implementation.
|
||||
func (r *Resolver) SAMLConfigurationConnection() schema.SAMLConfigurationConnectionResolver {
|
||||
return &sAMLConfigurationConnectionResolver{r}
|
||||
}
|
||||
|
||||
// SessionConnection returns schema.SessionConnectionResolver implementation.
|
||||
func (r *Resolver) SessionConnection() schema.SessionConnectionResolver {
|
||||
return &sessionConnectionResolver{r}
|
||||
}
|
||||
|
||||
type identityResolver struct{ *Resolver }
|
||||
type invitationConnectionResolver struct{ *Resolver }
|
||||
type membershipResolver struct{ *Resolver }
|
||||
type membershipConnectionResolver struct{ *Resolver }
|
||||
type mutationResolver struct{ *Resolver }
|
||||
type organizationResolver struct{ *Resolver }
|
||||
type personalAPIKeyConnectionResolver struct{ *Resolver }
|
||||
type queryResolver struct{ *Resolver }
|
||||
type sAMLConfigurationConnectionResolver struct{ *Resolver }
|
||||
type sessionConnectionResolver struct{ *Resolver }
|
||||
@@ -22,85 +22,84 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
"github.com/99designs/gqlgen/graphql/handler"
|
||||
"github.com/99designs/gqlgen/graphql/handler/extension"
|
||||
"github.com/99designs/gqlgen/graphql/handler/transport"
|
||||
"github.com/99designs/gqlgen/graphql/playground"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/vektah/gqlparser/v2/ast"
|
||||
"github.com/vektah/gqlparser/v2/gqlerror"
|
||||
"go.gearno.de/crypto/uuid"
|
||||
"go.gearno.de/kit/httpserver"
|
||||
"go.gearno.de/kit/log"
|
||||
"go.probo.inc/probo/pkg/auth"
|
||||
"go.probo.inc/probo/pkg/authz"
|
||||
"go.probo.inc/probo/pkg/baseurl"
|
||||
"go.probo.inc/probo/pkg/connector"
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/iam"
|
||||
"go.probo.inc/probo/pkg/probo"
|
||||
"go.probo.inc/probo/pkg/saferedirect"
|
||||
"go.probo.inc/probo/pkg/securecookie"
|
||||
connect_v1 "go.probo.inc/probo/pkg/server/api/connect/v1"
|
||||
"go.probo.inc/probo/pkg/server/api/console/v1/schema"
|
||||
serverauth "go.probo.inc/probo/pkg/server/auth"
|
||||
"go.probo.inc/probo/pkg/server/gqlutils"
|
||||
"go.probo.inc/probo/pkg/server/session"
|
||||
"go.probo.inc/probo/pkg/statelesstoken"
|
||||
)
|
||||
|
||||
type (
|
||||
AuthConfig struct {
|
||||
CookieName string
|
||||
CookieDomain string
|
||||
SessionDuration time.Duration
|
||||
CookieSecret string
|
||||
CookieSecure bool
|
||||
}
|
||||
|
||||
Resolver struct {
|
||||
proboSvc *probo.Service
|
||||
authSvc *auth.Service
|
||||
authzSvc *authz.Service
|
||||
samlSvc *auth.SAMLService
|
||||
authCfg AuthConfig
|
||||
probo *probo.Service
|
||||
iam *iam.Service
|
||||
customDomainCname string
|
||||
schema *ast.Schema
|
||||
}
|
||||
)
|
||||
|
||||
func ensureAuthenticated(ctx context.Context, next graphql.OperationHandler) graphql.ResponseHandler {
|
||||
identity := connect_v1.UserFromContext(ctx)
|
||||
|
||||
if identity == nil {
|
||||
return func(ctx context.Context) *graphql.Response {
|
||||
return &graphql.Response{
|
||||
Errors: gqlerror.List{
|
||||
gqlutils.Unauthorized(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctxKey struct{ name string }
|
||||
)
|
||||
|
||||
var (
|
||||
sessionContextKey = &ctxKey{name: "session"}
|
||||
)
|
||||
|
||||
func SessionFromContext(ctx context.Context) *coredata.Session {
|
||||
session, _ := ctx.Value(sessionContextKey).(*coredata.Session)
|
||||
return session
|
||||
}
|
||||
|
||||
func UserFromContext(ctx context.Context) *coredata.User {
|
||||
return serverauth.UserFromContext(ctx)
|
||||
}
|
||||
|
||||
func UserAPIKeyFromContext(ctx context.Context) *coredata.UserAPIKey {
|
||||
return serverauth.UserAPIKeyFromContext(ctx)
|
||||
return next(ctx)
|
||||
}
|
||||
|
||||
func NewMux(
|
||||
logger *log.Logger,
|
||||
proboSvc *probo.Service,
|
||||
authSvc *auth.Service,
|
||||
authzSvc *authz.Service,
|
||||
authCfg AuthConfig,
|
||||
iamSvc *iam.Service,
|
||||
cookieConfig securecookie.Config,
|
||||
tokenSecret string,
|
||||
connectorRegistry *connector.ConnectorRegistry,
|
||||
safeRedirect *saferedirect.SafeRedirect,
|
||||
baseURL *baseurl.BaseURL,
|
||||
customDomainCname string,
|
||||
samlSvc *auth.SAMLService,
|
||||
) *chi.Mux {
|
||||
r := chi.NewMux()
|
||||
|
||||
safeRedirect := &saferedirect.SafeRedirect{AllowedHost: baseURL.Host()}
|
||||
|
||||
sessionMiddleware := connect_v1.NewSessionMiddleware(iamSvc, cookieConfig)
|
||||
apiKeyMiddleware := connect_v1.NewAPIKeyMiddleware(iamSvc)
|
||||
|
||||
r.Use(sessionMiddleware)
|
||||
r.Use(apiKeyMiddleware)
|
||||
|
||||
config := schema.Config{
|
||||
Resolvers: &Resolver{
|
||||
probo: proboSvc,
|
||||
iam: iamSvc,
|
||||
customDomainCname: customDomainCname,
|
||||
},
|
||||
}
|
||||
es := schema.NewExecutableSchema(config)
|
||||
h := gqlutils.NewHandler(es, logger)
|
||||
h.AroundOperations(ensureAuthenticated)
|
||||
|
||||
r.Handle("/graphql", h)
|
||||
|
||||
r.Get(
|
||||
"/documents/signing-requests",
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -111,7 +110,7 @@ func NewMux(
|
||||
}
|
||||
|
||||
token = strings.TrimPrefix(token, "Bearer ")
|
||||
data, err := statelesstoken.ValidateToken[probo.SigningRequestData](authCfg.CookieSecret, probo.TokenTypeSigningRequest, token)
|
||||
data, err := statelesstoken.ValidateToken[probo.SigningRequestData](tokenSecret, probo.TokenTypeSigningRequest, token)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid token", http.StatusUnauthorized)
|
||||
return
|
||||
@@ -139,7 +138,7 @@ func NewMux(
|
||||
return
|
||||
}
|
||||
|
||||
data, err := statelesstoken.ValidateToken[probo.SigningRequestData](authCfg.CookieSecret, probo.TokenTypeSigningRequest, token)
|
||||
data, err := statelesstoken.ValidateToken[probo.SigningRequestData](tokenSecret, probo.TokenTypeSigningRequest, token)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid token", http.StatusUnauthorized)
|
||||
return
|
||||
@@ -194,7 +193,7 @@ func NewMux(
|
||||
}
|
||||
|
||||
token = strings.TrimPrefix(token, "Bearer ")
|
||||
data, err := statelesstoken.ValidateToken[probo.SigningRequestData](authCfg.CookieSecret, probo.TokenTypeSigningRequest, token)
|
||||
data, err := statelesstoken.ValidateToken[probo.SigningRequestData](tokenSecret, probo.TokenTypeSigningRequest, token)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid token", http.StatusUnauthorized)
|
||||
return
|
||||
@@ -217,7 +216,7 @@ func NewMux(
|
||||
},
|
||||
)
|
||||
|
||||
r.Get("/connectors/initiate", WithSession(authSvc, authzSvc, authCfg, func(w http.ResponseWriter, r *http.Request) {
|
||||
r.Get("/connectors/initiate", func(w http.ResponseWriter, r *http.Request) {
|
||||
provider := r.URL.Query().Get("provider")
|
||||
if provider != "SLACK" {
|
||||
httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("unsupported provider"))
|
||||
@@ -229,7 +228,23 @@ func NewMux(
|
||||
panic(fmt.Errorf("cannot parse organization id: %w", err))
|
||||
}
|
||||
|
||||
_ = GetTenantService(r.Context(), proboSvc, organizationID.TenantID())
|
||||
identity := connect_v1.UserFromContext(r.Context())
|
||||
apiKey := connect_v1.APIKeyFromContext(r.Context())
|
||||
if identity == nil {
|
||||
httpserver.RenderError(w, http.StatusUnauthorized, fmt.Errorf("authentication required"))
|
||||
return
|
||||
}
|
||||
|
||||
var credentialID *gid.GID
|
||||
if apiKey != nil {
|
||||
credentialID = &apiKey.ID
|
||||
}
|
||||
|
||||
// Ensure the actor (and optional API key) can access this organization.
|
||||
if err := iamSvc.AccessManagementService.Authorize(r.Context(), identity.ID, credentialID, organizationID, iam.ActionGet); err != nil {
|
||||
httpserver.RenderError(w, http.StatusForbidden, err)
|
||||
return
|
||||
}
|
||||
|
||||
redirectURL, err := connectorRegistry.Initiate(r.Context(), provider, organizationID, r)
|
||||
if err != nil {
|
||||
@@ -239,7 +254,7 @@ func NewMux(
|
||||
// Allow external redirects for Slack OAuth only for now
|
||||
slackSafeRedirect := &saferedirect.SafeRedirect{AllowedHost: "slack.com"}
|
||||
slackSafeRedirect.Redirect(w, r, redirectURL, "/", http.StatusSeeOther)
|
||||
}))
|
||||
})
|
||||
|
||||
r.Get("/connectors/complete", func(w http.ResponseWriter, r *http.Request) {
|
||||
provider := r.URL.Query().Get("provider")
|
||||
@@ -288,146 +303,16 @@ func NewMux(
|
||||
if continueURL != "" {
|
||||
safeRedirect.Redirect(w, r, continueURL, "/", http.StatusSeeOther)
|
||||
} else {
|
||||
redirectURL := fmt.Sprintf("/organizations/%s", organizationID.String())
|
||||
redirectURL := baseURL.WithPath("/organizations/" + organizationID.String()).MustString()
|
||||
safeRedirect.Redirect(w, r, redirectURL, "/", http.StatusSeeOther)
|
||||
}
|
||||
})
|
||||
|
||||
r.Get("/", playground.Handler("GraphQL", "/api/console/v1/query"))
|
||||
r.Post("/query", graphqlHandler(logger, proboSvc, authSvc, authzSvc, samlSvc, authCfg, customDomainCname))
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// GetSchema returns the parsed GraphQL schema for the console API
|
||||
// This is used by other services like authz to extract permissions from @mustBeAuthorized directives
|
||||
func GetSchema() *ast.Schema {
|
||||
execSchema := schema.NewExecutableSchema(schema.Config{})
|
||||
return execSchema.Schema()
|
||||
}
|
||||
|
||||
func graphqlHandler(logger *log.Logger, proboSvc *probo.Service, authSvc *auth.Service, authzSvc *authz.Service, samlSvc *auth.SAMLService, authCfg AuthConfig, customDomainCname string) http.HandlerFunc {
|
||||
var mb int64 = 1 << 20
|
||||
|
||||
// Parse the schema first to make it available to resolvers
|
||||
execSchema := schema.NewExecutableSchema(schema.Config{})
|
||||
|
||||
cfg := schema.Config{
|
||||
Resolvers: &Resolver{
|
||||
proboSvc: proboSvc,
|
||||
authSvc: authSvc,
|
||||
authzSvc: authzSvc,
|
||||
samlSvc: samlSvc,
|
||||
authCfg: authCfg,
|
||||
customDomainCname: customDomainCname,
|
||||
schema: execSchema.Schema(),
|
||||
},
|
||||
}
|
||||
|
||||
es := schema.NewExecutableSchema(cfg)
|
||||
srv := handler.New(es)
|
||||
srv.AddTransport(transport.POST{})
|
||||
srv.AddTransport(
|
||||
transport.MultipartForm{
|
||||
MaxMemory: 32 * mb,
|
||||
MaxUploadSize: 50 * mb,
|
||||
},
|
||||
)
|
||||
srv.Use(extension.Introspection{})
|
||||
srv.Use(gqlutils.NewTracingExtension(logger))
|
||||
srv.SetRecoverFunc(gqlutils.RecoverFunc)
|
||||
|
||||
srv.AroundOperations(
|
||||
func(ctx context.Context, next graphql.OperationHandler) graphql.ResponseHandler {
|
||||
user := UserFromContext(ctx)
|
||||
|
||||
if user == nil {
|
||||
return func(ctx context.Context) *graphql.Response {
|
||||
return &graphql.Response{
|
||||
Errors: gqlerror.List{
|
||||
&gqlerror.Error{
|
||||
Message: "authentication required",
|
||||
Extensions: map[string]any{
|
||||
"code": "UNAUTHENTICATED",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return next(ctx)
|
||||
},
|
||||
)
|
||||
|
||||
return WithSession(authSvc, authzSvc, authCfg, srv.ServeHTTP)
|
||||
}
|
||||
|
||||
func WithSession(authSvc *auth.Service, authzSvc *authz.Service, authCfg AuthConfig, next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
if authCtx := serverauth.AuthenticateWithAPIKey(ctx, r, authSvc, authzSvc); authCtx != nil {
|
||||
next(w, r.WithContext(authCtx))
|
||||
return
|
||||
}
|
||||
|
||||
sessionAuthCfg := session.AuthConfig{
|
||||
CookieName: authCfg.CookieName,
|
||||
CookieSecret: authCfg.CookieSecret,
|
||||
CookieSecure: authCfg.CookieSecure,
|
||||
}
|
||||
|
||||
errorHandler := session.ErrorHandler{
|
||||
OnCookieError: func(err error) {
|
||||
panic(fmt.Errorf("cannot get session: %w", err))
|
||||
},
|
||||
OnParseError: func(w http.ResponseWriter, authCfg session.AuthConfig) {
|
||||
session.ClearCookie(w, authCfg)
|
||||
},
|
||||
OnSessionError: func(w http.ResponseWriter, authCfg session.AuthConfig) {
|
||||
session.ClearCookie(w, authCfg)
|
||||
},
|
||||
OnUserError: func(w http.ResponseWriter, authCfg session.AuthConfig) {
|
||||
session.ClearCookie(w, authCfg)
|
||||
},
|
||||
OnTenantError: func(err error) {
|
||||
panic(fmt.Errorf("cannot list tenants for user: %w", err))
|
||||
},
|
||||
}
|
||||
|
||||
authResult := session.TryAuth(ctx, w, r, authSvc, authzSvc, sessionAuthCfg, errorHandler)
|
||||
if authResult == nil {
|
||||
next(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, sessionContextKey, authResult.Session)
|
||||
ctx = context.WithValue(ctx, serverauth.UserContextKey, authResult.User)
|
||||
ctx = context.WithValue(ctx, serverauth.UserTenantContextKey, &serverauth.UserTenantAccess{
|
||||
TenantIDs: authResult.TenantIDs,
|
||||
AuthErrors: authResult.AuthErrors,
|
||||
})
|
||||
|
||||
next(w, r.WithContext(ctx))
|
||||
|
||||
// Update session after the handler completes
|
||||
if _, err := authSvc.UpdateSession(ctx, authResult.Session.ID); err != nil {
|
||||
panic(fmt.Errorf("cannot update session: %w", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Resolver) ProboService(ctx context.Context, tenantID gid.TenantID) *probo.TenantService {
|
||||
return GetTenantService(ctx, r.proboSvc, tenantID)
|
||||
}
|
||||
|
||||
func (r *Resolver) AuthzService(ctx context.Context, tenantID gid.TenantID) *authz.TenantAuthzService {
|
||||
return GetTenantAuthzService(ctx, r.authzSvc, tenantID)
|
||||
}
|
||||
|
||||
func (r *Resolver) AuthService(ctx context.Context, tenantID gid.TenantID) *auth.TenantAuthService {
|
||||
return GetTenantAuthService(ctx, r.authSvc, tenantID)
|
||||
return GetTenantService(ctx, r.probo, tenantID)
|
||||
}
|
||||
|
||||
func UnwrapOmittable[T any](field graphql.Omittable[T]) *T {
|
||||
@@ -439,26 +324,19 @@ func UnwrapOmittable[T any](field graphql.Omittable[T]) *T {
|
||||
}
|
||||
|
||||
func GetTenantService(ctx context.Context, proboSvc *probo.Service, tenantID gid.TenantID) *probo.TenantService {
|
||||
serverauth.RequireTenantAccess(ctx, tenantID)
|
||||
return proboSvc.WithTenant(tenantID)
|
||||
}
|
||||
|
||||
func GetTenantAuthzService(ctx context.Context, authzSvc *authz.Service, tenantID gid.TenantID) *authz.TenantAuthzService {
|
||||
serverauth.RequireTenantAccess(ctx, tenantID)
|
||||
return authzSvc.WithTenant(tenantID)
|
||||
}
|
||||
func (r *Resolver) MustBeAuthorized(ctx context.Context, entityID gid.GID, action iam.Action) {
|
||||
user := connect_v1.UserFromContext(ctx)
|
||||
apiKey := connect_v1.APIKeyFromContext(ctx)
|
||||
|
||||
func GetTenantAuthService(ctx context.Context, authSvc *auth.Service, tenantID gid.TenantID) *auth.TenantAuthService {
|
||||
serverauth.RequireTenantAccess(ctx, tenantID)
|
||||
return authSvc.WithTenant(tenantID)
|
||||
}
|
||||
var credentialID *gid.GID
|
||||
if apiKey != nil {
|
||||
credentialID = &apiKey.ID
|
||||
}
|
||||
|
||||
func (r *Resolver) MustBeAuthorized(ctx context.Context, entityID gid.GID, action authz.Action) {
|
||||
user := UserFromContext(ctx)
|
||||
apiKey := UserAPIKeyFromContext(ctx)
|
||||
|
||||
authzSvc := r.AuthzService(ctx, entityID.TenantID())
|
||||
err := authzSvc.Authorize(ctx, user, apiKey, entityID, action)
|
||||
err := r.iam.AccessManagementService.Authorize(ctx, user.ID, credentialID, entityID, action)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -34,15 +34,6 @@ type PageInfo {
|
||||
endCursor: CursorKey
|
||||
}
|
||||
|
||||
# Roles
|
||||
enum Role {
|
||||
OWNER
|
||||
ADMIN
|
||||
VIEWER
|
||||
AUDITOR
|
||||
FULL
|
||||
}
|
||||
|
||||
# Enums
|
||||
enum OrderDirection
|
||||
@goModel(model: "go.probo.inc/probo/pkg/page.OrderDirection") {
|
||||
@@ -83,31 +74,6 @@ enum PeopleKind @goModel(model: "go.probo.inc/probo/pkg/coredata.PeopleKind") {
|
||||
@goEnum(value: "go.probo.inc/probo/pkg/coredata.PeopleKindServiceAccount")
|
||||
}
|
||||
|
||||
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 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")
|
||||
}
|
||||
|
||||
enum APIRole @goModel(model: "go.probo.inc/probo/pkg/coredata.APIRole") {
|
||||
FULL @goEnum(value: "go.probo.inc/probo/pkg/coredata.APIRoleFull")
|
||||
}
|
||||
|
||||
enum DocumentStatus
|
||||
@goModel(model: "go.probo.inc/probo/pkg/coredata.DocumentStatus") {
|
||||
DRAFT @goEnum(value: "go.probo.inc/probo/pkg/coredata.DocumentStatusDraft")
|
||||
@@ -143,26 +109,6 @@ enum AuditState @goModel(model: "go.probo.inc/probo/pkg/coredata.AuditState") {
|
||||
OUTDATED @goEnum(value: "go.probo.inc/probo/pkg/coredata.AuditStateOutdated")
|
||||
}
|
||||
|
||||
enum SAMLEnforcementPolicy
|
||||
@goModel(model: "go.probo.inc/probo/pkg/coredata.SAMLEnforcementPolicy") {
|
||||
OFF @goEnum(value: "go.probo.inc/probo/pkg/coredata.SAMLEnforcementPolicyOff")
|
||||
OPTIONAL
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.SAMLEnforcementPolicyOptional"
|
||||
)
|
||||
REQUIRED
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.SAMLEnforcementPolicyRequired"
|
||||
)
|
||||
}
|
||||
|
||||
enum UserAuthMethod
|
||||
@goModel(model: "go.probo.inc/probo/pkg/coredata.UserAuthMethod") {
|
||||
PASSWORD
|
||||
@goEnum(value: "go.probo.inc/probo/pkg/coredata.UserAuthMethodPassword")
|
||||
SAML @goEnum(value: "go.probo.inc/probo/pkg/coredata.UserAuthMethodSAML")
|
||||
}
|
||||
|
||||
enum TrustCenterVisibility
|
||||
@goModel(model: "go.probo.inc/probo/pkg/coredata.TrustCenterVisibility") {
|
||||
NONE
|
||||
@@ -226,8 +172,7 @@ enum ObligationStatus
|
||||
|
||||
enum ObligationType
|
||||
@goModel(model: "go.probo.inc/probo/pkg/coredata.ObligationType") {
|
||||
LEGAL
|
||||
@goEnum(value: "go.probo.inc/probo/pkg/coredata.ObligationTypeLegal")
|
||||
LEGAL @goEnum(value: "go.probo.inc/probo/pkg/coredata.ObligationTypeLegal")
|
||||
CONTRACTUAL
|
||||
@goEnum(value: "go.probo.inc/probo/pkg/coredata.ObligationTypeContractual")
|
||||
}
|
||||
@@ -269,17 +214,11 @@ enum ContinualImprovementPriority
|
||||
}
|
||||
|
||||
enum RightsRequestType
|
||||
@goModel(
|
||||
model: "go.probo.inc/probo/pkg/coredata.RightsRequestType"
|
||||
) {
|
||||
@goModel(model: "go.probo.inc/probo/pkg/coredata.RightsRequestType") {
|
||||
ACCESS
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.RightsRequestTypeAccess"
|
||||
)
|
||||
@goEnum(value: "go.probo.inc/probo/pkg/coredata.RightsRequestTypeAccess")
|
||||
DELETION
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.RightsRequestTypeDeletion"
|
||||
)
|
||||
@goEnum(value: "go.probo.inc/probo/pkg/coredata.RightsRequestTypeDeletion")
|
||||
PORTABILITY
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.RightsRequestTypePortability"
|
||||
@@ -287,21 +226,13 @@ enum RightsRequestType
|
||||
}
|
||||
|
||||
enum RightsRequestState
|
||||
@goModel(
|
||||
model: "go.probo.inc/probo/pkg/coredata.RightsRequestState"
|
||||
) {
|
||||
TODO
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.RightsRequestStateTodo"
|
||||
)
|
||||
@goModel(model: "go.probo.inc/probo/pkg/coredata.RightsRequestState") {
|
||||
TODO @goEnum(value: "go.probo.inc/probo/pkg/coredata.RightsRequestStateTodo")
|
||||
IN_PROGRESS
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.RightsRequestStateInProgress"
|
||||
)
|
||||
DONE
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.RightsRequestStateDone"
|
||||
)
|
||||
DONE @goEnum(value: "go.probo.inc/probo/pkg/coredata.RightsRequestStateDone")
|
||||
}
|
||||
|
||||
enum ProcessingActivitySpecialOrCriminalDatum
|
||||
@@ -429,9 +360,7 @@ enum DataProtectionImpactAssessmentResidualRisk
|
||||
}
|
||||
|
||||
enum ProcessingActivityRole
|
||||
@goModel(
|
||||
model: "go.probo.inc/probo/pkg/coredata.ProcessingActivityRole"
|
||||
) {
|
||||
@goModel(model: "go.probo.inc/probo/pkg/coredata.ProcessingActivityRole") {
|
||||
CONTROLLER
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.ProcessingActivityRoleController"
|
||||
@@ -443,12 +372,6 @@ enum ProcessingActivityRole
|
||||
}
|
||||
|
||||
# Order Field Enums
|
||||
enum UserOrderField
|
||||
@goModel(model: "go.probo.inc/probo/pkg/coredata.UserOrderField") {
|
||||
CREATED_AT
|
||||
@goEnum(value: "go.probo.inc/probo/pkg/coredata.UserOrderFieldCreatedAt")
|
||||
}
|
||||
|
||||
enum PeopleOrderField
|
||||
@goModel(model: "go.probo.inc/probo/pkg/coredata.PeopleOrderField") {
|
||||
FULL_NAME
|
||||
@@ -1110,9 +1033,7 @@ enum ContinualImprovementOrderField
|
||||
}
|
||||
|
||||
enum RightsRequestOrderField
|
||||
@goModel(
|
||||
model: "go.probo.inc/probo/pkg/coredata.RightsRequestOrderField"
|
||||
) {
|
||||
@goModel(model: "go.probo.inc/probo/pkg/coredata.RightsRequestOrderField") {
|
||||
CREATED_AT
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.RightsRequestOrderFieldCreatedAt"
|
||||
@@ -1259,57 +1180,7 @@ enum SnapshotOrderField
|
||||
TYPE @goEnum(value: "go.probo.inc/probo/pkg/coredata.SnapshotOrderFieldType")
|
||||
}
|
||||
|
||||
enum MembershipOrderField
|
||||
@goModel(model: "go.probo.inc/probo/pkg/coredata.MembershipOrderField") {
|
||||
FULL_NAME
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.MembershipOrderFieldFullName"
|
||||
)
|
||||
EMAIL_ADDRESS
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.MembershipOrderFieldEmailAddress"
|
||||
)
|
||||
ROLE
|
||||
@goEnum(value: "go.probo.inc/probo/pkg/coredata.MembershipOrderFieldRole")
|
||||
CREATED_AT
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.MembershipOrderFieldCreatedAt"
|
||||
)
|
||||
}
|
||||
|
||||
enum InvitationOrderField
|
||||
@goModel(model: "go.probo.inc/probo/pkg/coredata.InvitationOrderField") {
|
||||
FULL_NAME
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.InvitationOrderFieldFullName"
|
||||
)
|
||||
EMAIL
|
||||
@goEnum(value: "go.probo.inc/probo/pkg/coredata.InvitationOrderFieldEmail")
|
||||
ROLE
|
||||
@goEnum(value: "go.probo.inc/probo/pkg/coredata.InvitationOrderFieldRole")
|
||||
CREATED_AT
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.InvitationOrderFieldCreatedAt"
|
||||
)
|
||||
EXPIRES_AT
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.InvitationOrderFieldExpiresAt"
|
||||
)
|
||||
ACCEPTED_AT
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.InvitationOrderFieldAcceptedAt"
|
||||
)
|
||||
}
|
||||
|
||||
# Input Types
|
||||
input UserOrder
|
||||
@goModel(
|
||||
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.UserOrderBy"
|
||||
) {
|
||||
direction: OrderDirection!
|
||||
field: UserOrderField!
|
||||
}
|
||||
|
||||
input PeopleOrder
|
||||
@goModel(
|
||||
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.PeopleOrderBy"
|
||||
@@ -1531,23 +1402,6 @@ input SnapshotOrder
|
||||
field: SnapshotOrderField!
|
||||
}
|
||||
|
||||
input MembershipOrder
|
||||
@goModel(
|
||||
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.MembershipOrderBy"
|
||||
) {
|
||||
direction: OrderDirection!
|
||||
field: MembershipOrderField!
|
||||
}
|
||||
|
||||
input InvitationOrder {
|
||||
direction: OrderDirection!
|
||||
field: InvitationOrderField!
|
||||
}
|
||||
|
||||
input InvitationFilter {
|
||||
statuses: [InvitationStatus!]
|
||||
}
|
||||
|
||||
input DocumentVersionFilter {
|
||||
status: DocumentStatus
|
||||
}
|
||||
@@ -1595,7 +1449,6 @@ input ContinualImprovementFilter {
|
||||
snapshotId: ID
|
||||
}
|
||||
|
||||
|
||||
input ProcessingActivityFilter {
|
||||
snapshotId: ID
|
||||
}
|
||||
@@ -1655,23 +1508,6 @@ type Organization implements Node {
|
||||
headquarterAddress: String
|
||||
context: OrganizationContext @goField(forceResolver: true)
|
||||
|
||||
memberships(
|
||||
first: Int
|
||||
after: CursorKey
|
||||
last: Int
|
||||
before: CursorKey
|
||||
orderBy: MembershipOrder
|
||||
): MembershipConnection! @goField(forceResolver: true)
|
||||
|
||||
invitations(
|
||||
first: Int
|
||||
after: CursorKey
|
||||
last: Int
|
||||
before: CursorKey
|
||||
orderBy: InvitationOrder
|
||||
filter: InvitationFilter
|
||||
): InvitationConnection! @goField(forceResolver: true)
|
||||
|
||||
slackConnections(
|
||||
first: Int
|
||||
after: CursorKey
|
||||
@@ -1874,44 +1710,10 @@ type Organization implements Node {
|
||||
|
||||
customDomain: CustomDomain @goField(forceResolver: true)
|
||||
|
||||
samlConfigurations: [SAMLConfiguration!]! @goField(forceResolver: true)
|
||||
|
||||
createdAt: Datetime!
|
||||
updatedAt: Datetime!
|
||||
}
|
||||
|
||||
type User implements Node {
|
||||
id: ID!
|
||||
fullName: String!
|
||||
email: EmailAddr!
|
||||
createdAt: Datetime!
|
||||
updatedAt: Datetime!
|
||||
}
|
||||
|
||||
type Membership implements Node {
|
||||
id: ID!
|
||||
userID: ID!
|
||||
organizationID: ID!
|
||||
role: MembershipRole!
|
||||
fullName: String!
|
||||
emailAddress: EmailAddr!
|
||||
authMethod: UserAuthMethod! @goField(forceResolver: true)
|
||||
createdAt: Datetime!
|
||||
updatedAt: Datetime!
|
||||
}
|
||||
|
||||
type Invitation implements Node {
|
||||
id: ID!
|
||||
email: EmailAddr!
|
||||
fullName: String!
|
||||
role: MembershipRole!
|
||||
status: InvitationStatus!
|
||||
expiresAt: Datetime!
|
||||
acceptedAt: Datetime
|
||||
createdAt: Datetime!
|
||||
organization: Organization! @goField(forceResolver: true)
|
||||
}
|
||||
|
||||
type SlackConnection {
|
||||
id: ID!
|
||||
channel: String
|
||||
@@ -2324,7 +2126,8 @@ type StateOfApplicability implements Node {
|
||||
orderBy: ControlOrder
|
||||
filter: ControlFilter
|
||||
): ControlConnection! @goField(forceResolver: true)
|
||||
availableControls: [AvailableStateOfApplicabilityControl!]! @goField(forceResolver: true)
|
||||
availableControls: [AvailableStateOfApplicabilityControl!]!
|
||||
@goField(forceResolver: true)
|
||||
createdAt: Datetime!
|
||||
updatedAt: Datetime!
|
||||
}
|
||||
@@ -2510,8 +2313,10 @@ type ProcessingActivity implements Node {
|
||||
before: CursorKey
|
||||
orderBy: VendorOrder
|
||||
): VendorConnection! @goField(forceResolver: true)
|
||||
dataProtectionImpactAssessment: DataProtectionImpactAssessment @goField(forceResolver: true)
|
||||
transferImpactAssessment: TransferImpactAssessment @goField(forceResolver: true)
|
||||
dataProtectionImpactAssessment: DataProtectionImpactAssessment
|
||||
@goField(forceResolver: true)
|
||||
transferImpactAssessment: TransferImpactAssessment
|
||||
@goField(forceResolver: true)
|
||||
createdAt: Datetime!
|
||||
updatedAt: Datetime!
|
||||
}
|
||||
@@ -2580,15 +2385,6 @@ type Session {
|
||||
|
||||
type Viewer {
|
||||
id: ID!
|
||||
user: User!
|
||||
|
||||
organizations(
|
||||
first: Int
|
||||
after: CursorKey
|
||||
last: Int
|
||||
before: CursorKey
|
||||
orderBy: OrganizationOrder
|
||||
): OrganizationConnection! @goField(forceResolver: true)
|
||||
|
||||
signableDocuments(
|
||||
organizationId: ID!
|
||||
@@ -2602,17 +2398,6 @@ type Viewer {
|
||||
signableDocument(id: ID!): SignableDocument @goField(forceResolver: true)
|
||||
}
|
||||
|
||||
# Connection Types
|
||||
type OrganizationConnection {
|
||||
edges: [OrganizationEdge!]!
|
||||
pageInfo: PageInfo!
|
||||
}
|
||||
|
||||
type OrganizationEdge {
|
||||
cursor: CursorKey!
|
||||
node: Organization!
|
||||
}
|
||||
|
||||
type TrustCenterConnection {
|
||||
edges: [TrustCenterEdge!]!
|
||||
pageInfo: PageInfo!
|
||||
@@ -2729,34 +2514,6 @@ type TrustCenterFileEdge {
|
||||
node: TrustCenterFile!
|
||||
}
|
||||
|
||||
type UserConnection
|
||||
@goModel(
|
||||
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.UserConnection"
|
||||
) {
|
||||
totalCount: Int! @goField(forceResolver: true)
|
||||
edges: [UserEdge!]!
|
||||
pageInfo: PageInfo!
|
||||
}
|
||||
|
||||
type MembershipConnection
|
||||
@goModel(
|
||||
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.MembershipConnection"
|
||||
) {
|
||||
totalCount: Int! @goField(forceResolver: true)
|
||||
edges: [MembershipEdge!]!
|
||||
pageInfo: PageInfo!
|
||||
}
|
||||
|
||||
type MembershipEdge {
|
||||
cursor: CursorKey!
|
||||
node: Membership!
|
||||
}
|
||||
|
||||
type UserEdge {
|
||||
cursor: CursorKey!
|
||||
node: User!
|
||||
}
|
||||
|
||||
type PeopleConnection
|
||||
@goModel(
|
||||
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.PeopleConnection"
|
||||
@@ -3126,20 +2883,6 @@ type File {
|
||||
updatedAt: Datetime!
|
||||
}
|
||||
|
||||
type InvitationConnection
|
||||
@goModel(
|
||||
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.InvitationConnection"
|
||||
) {
|
||||
totalCount: Int! @goField(forceResolver: true)
|
||||
edges: [InvitationEdge!]!
|
||||
pageInfo: PageInfo!
|
||||
}
|
||||
|
||||
type InvitationEdge {
|
||||
cursor: CursorKey!
|
||||
node: Invitation!
|
||||
}
|
||||
|
||||
# Root Types
|
||||
type Query {
|
||||
node(id: ID!): Node!
|
||||
@@ -3147,22 +2890,9 @@ type Query {
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
# Organization mutations
|
||||
createOrganization(
|
||||
input: CreateOrganizationInput!
|
||||
): CreateOrganizationPayload!
|
||||
updateOrganization(
|
||||
input: UpdateOrganizationInput!
|
||||
): UpdateOrganizationPayload!
|
||||
updateOrganizationContext(
|
||||
input: UpdateOrganizationContextInput!
|
||||
): UpdateOrganizationContextPayload!
|
||||
deleteOrganizationHorizontalLogo(
|
||||
input: DeleteOrganizationHorizontalLogoInput!
|
||||
): DeleteOrganizationHorizontalLogoPayload!
|
||||
deleteOrganization(
|
||||
input: DeleteOrganizationInput!
|
||||
): DeleteOrganizationPayload!
|
||||
updateTrustCenter(input: UpdateTrustCenterInput!): UpdateTrustCenterPayload!
|
||||
uploadTrustCenterNDA(
|
||||
input: UploadTrustCenterNDAInput!
|
||||
@@ -3203,13 +2933,7 @@ type Mutation {
|
||||
deleteTrustCenterFile(
|
||||
input: DeleteTrustCenterFileInput!
|
||||
): DeleteTrustCenterFilePayload!
|
||||
# User mutations
|
||||
confirmEmail(input: ConfirmEmailInput!): ConfirmEmailPayload!
|
||||
inviteUser(input: InviteUserInput!): InviteUserPayload!
|
||||
acceptInvitation(input: AcceptInvitationInput!): AcceptInvitationPayload!
|
||||
deleteInvitation(input: DeleteInvitationInput!): DeleteInvitationPayload!
|
||||
removeMember(input: RemoveMemberInput!): RemoveMemberPayload!
|
||||
updateMembership(input: UpdateMembershipInput!): UpdateMembershipPayload!
|
||||
|
||||
# People mutations
|
||||
createPeople(input: CreatePeopleInput!): CreatePeoplePayload!
|
||||
updatePeople(input: UpdatePeopleInput!): UpdatePeoplePayload!
|
||||
@@ -3407,6 +3131,7 @@ type Mutation {
|
||||
input: CancelSignatureRequestInput!
|
||||
): CancelSignatureRequestPayload!
|
||||
signDocument(input: SignDocumentInput!): SignDocumentPayload!
|
||||
|
||||
exportDocumentVersionPDF(
|
||||
input: ExportDocumentVersionPDFInput!
|
||||
): ExportDocumentVersionPDFPayload!
|
||||
@@ -3515,25 +3240,6 @@ type Mutation {
|
||||
deleteCustomDomain(
|
||||
input: DeleteCustomDomainInput!
|
||||
): DeleteCustomDomainPayload!
|
||||
# SAML Configuration mutations (OWNER/ADMIN only)
|
||||
# Step 1: Initiate domain verification (creates SAML config with unverified domain)
|
||||
initiateDomainVerification(
|
||||
input: InitiateDomainVerificationInput!
|
||||
): InitiateDomainVerificationPayload!
|
||||
# Step 2: Verify domain ownership via DNS TXT record
|
||||
verifyDomain(input: VerifyDomainInput!): VerifyDomainPayload!
|
||||
# Step 3: Configure SAML (only allowed after domain is verified)
|
||||
createSAMLConfiguration(
|
||||
input: CreateSAMLConfigurationInput!
|
||||
): CreateSAMLConfigurationPayload!
|
||||
updateSAMLConfiguration(
|
||||
input: UpdateSAMLConfigurationInput!
|
||||
): UpdateSAMLConfigurationPayload!
|
||||
deleteSAMLConfiguration(
|
||||
input: DeleteSAMLConfigurationInput!
|
||||
): DeleteSAMLConfigurationPayload!
|
||||
enableSAML(input: EnableSAMLInput!): EnableSAMLPayload!
|
||||
disableSAML(input: DisableSAMLInput!): DisableSAMLPayload!
|
||||
}
|
||||
|
||||
# Input Types
|
||||
@@ -3545,34 +3251,11 @@ type GenerateFrameworkStateOfApplicabilityPayload {
|
||||
data: String!
|
||||
}
|
||||
|
||||
input CreateOrganizationInput {
|
||||
name: String!
|
||||
}
|
||||
|
||||
input UpdateOrganizationInput {
|
||||
organizationId: ID!
|
||||
name: String
|
||||
description: String @goField(omittable: true)
|
||||
websiteUrl: String @goField(omittable: true)
|
||||
email: String @goField(omittable: true)
|
||||
headquarterAddress: String @goField(omittable: true)
|
||||
logoFile: Upload
|
||||
horizontalLogoFile: Upload
|
||||
}
|
||||
|
||||
input UpdateOrganizationContextInput {
|
||||
organizationId: ID!
|
||||
summary: String @goField(omittable: true)
|
||||
}
|
||||
|
||||
input DeleteOrganizationHorizontalLogoInput {
|
||||
organizationId: ID!
|
||||
}
|
||||
|
||||
input DeleteOrganizationInput {
|
||||
organizationId: ID!
|
||||
}
|
||||
|
||||
input UpdateTrustCenterInput {
|
||||
trustCenterId: ID!
|
||||
active: Boolean
|
||||
@@ -3763,7 +3446,7 @@ input UpdatePeopleInput {
|
||||
id: ID!
|
||||
fullName: String
|
||||
primaryEmailAddress: EmailAddr
|
||||
additionalEmailAddresses: [EmailAddr!]
|
||||
additionalEmailAddresses: [EmailAddr!] @goField(omittable: true)
|
||||
kind: PeopleKind
|
||||
position: String @goField(omittable: true)
|
||||
contractStartDate: Datetime @goField(omittable: true)
|
||||
@@ -3844,7 +3527,6 @@ input DeleteTaskInput {
|
||||
taskId: ID!
|
||||
}
|
||||
|
||||
|
||||
input CreateControlMeasureMappingInput {
|
||||
controlId: ID!
|
||||
measureId: ID!
|
||||
@@ -4152,7 +3834,6 @@ input DeleteStateOfApplicabilityInput {
|
||||
stateOfApplicabilityId: ID!
|
||||
}
|
||||
|
||||
|
||||
type StateOfApplicabilityControl {
|
||||
id: ID!
|
||||
stateOfApplicabilityId: ID!
|
||||
@@ -4162,13 +3843,19 @@ type StateOfApplicabilityControl {
|
||||
justification: String
|
||||
}
|
||||
|
||||
type StateOfApplicabilityControlConnection @goModel(model: "go.probo.inc/probo/pkg/server/api/console/v1/types.StateOfApplicabilityControlConnection") {
|
||||
type StateOfApplicabilityControlConnection
|
||||
@goModel(
|
||||
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.StateOfApplicabilityControlConnection"
|
||||
) {
|
||||
totalCount: Int!
|
||||
edges: [StateOfApplicabilityControlEdge!]!
|
||||
pageInfo: PageInfo!
|
||||
}
|
||||
|
||||
type StateOfApplicabilityControlEdge @goModel(model: "go.probo.inc/probo/pkg/server/api/console/v1/types.StateOfApplicabilityControlEdge") {
|
||||
type StateOfApplicabilityControlEdge
|
||||
@goModel(
|
||||
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.StateOfApplicabilityControlEdge"
|
||||
) {
|
||||
cursor: CursorKey!
|
||||
node: StateOfApplicabilityControl!
|
||||
}
|
||||
@@ -4202,38 +3889,6 @@ enum StateOfApplicabilityOrderField
|
||||
value: "go.probo.inc/probo/pkg/coredata.StateOfApplicabilityOrderFieldCreatedAt"
|
||||
)
|
||||
}
|
||||
|
||||
input ConfirmEmailInput {
|
||||
token: String!
|
||||
}
|
||||
|
||||
input InviteUserInput {
|
||||
organizationId: ID!
|
||||
email: EmailAddr!
|
||||
fullName: String!
|
||||
role: MembershipRole!
|
||||
createPeople: Boolean!
|
||||
}
|
||||
|
||||
input AcceptInvitationInput {
|
||||
invitationId: ID!
|
||||
}
|
||||
|
||||
input DeleteInvitationInput {
|
||||
invitationId: ID!
|
||||
}
|
||||
|
||||
input RemoveMemberInput {
|
||||
organizationId: ID!
|
||||
memberId: ID!
|
||||
}
|
||||
|
||||
input UpdateMembershipInput {
|
||||
organizationId: ID!
|
||||
memberId: ID!
|
||||
role: MembershipRole!
|
||||
}
|
||||
|
||||
input CreateControlInput {
|
||||
frameworkId: ID!
|
||||
sectionTitle: String!
|
||||
@@ -4517,13 +4172,6 @@ input DeleteSnapshotInput {
|
||||
}
|
||||
|
||||
# Payload Types
|
||||
type CreateOrganizationPayload {
|
||||
organizationEdge: OrganizationEdge!
|
||||
}
|
||||
|
||||
type UpdateOrganizationPayload {
|
||||
organization: Organization!
|
||||
}
|
||||
|
||||
type UpdateOrganizationContextPayload {
|
||||
context: OrganizationContext!
|
||||
@@ -4534,14 +4182,6 @@ type OrganizationContext {
|
||||
summary: String
|
||||
}
|
||||
|
||||
type DeleteOrganizationHorizontalLogoPayload {
|
||||
organization: Organization!
|
||||
}
|
||||
|
||||
type DeleteOrganizationPayload {
|
||||
deletedOrganizationId: ID!
|
||||
}
|
||||
|
||||
type UpdateTrustCenterPayload {
|
||||
trustCenter: TrustCenter!
|
||||
}
|
||||
@@ -4698,7 +4338,6 @@ type DeleteTaskPayload {
|
||||
deletedTaskId: ID!
|
||||
}
|
||||
|
||||
|
||||
type CreateControlMeasureMappingPayload {
|
||||
controlEdge: ControlEdge!
|
||||
measureEdge: MeasureEdge!
|
||||
@@ -4906,30 +4545,6 @@ type DeleteStateOfApplicabilityPayload {
|
||||
deletedStateOfApplicabilityId: ID!
|
||||
}
|
||||
|
||||
type ConfirmEmailPayload {
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
type InviteUserPayload {
|
||||
invitationEdge: InvitationEdge!
|
||||
}
|
||||
|
||||
type AcceptInvitationPayload {
|
||||
invitation: Invitation!
|
||||
}
|
||||
|
||||
type DeleteInvitationPayload {
|
||||
deletedInvitationId: ID!
|
||||
}
|
||||
|
||||
type RemoveMemberPayload {
|
||||
deletedMemberId: ID!
|
||||
}
|
||||
|
||||
type UpdateMembershipPayload {
|
||||
membership: Membership!
|
||||
}
|
||||
|
||||
input VendorRiskAssessmentOrder {
|
||||
field: VendorRiskAssessmentOrderField!
|
||||
direction: OrderDirection!
|
||||
@@ -5213,7 +4828,7 @@ type Asset implements Node {
|
||||
before: CursorKey
|
||||
orderBy: VendorOrder
|
||||
): VendorConnection! @goField(forceResolver: true)
|
||||
assetType: AssetType! @goField(forceResolver: true)
|
||||
assetType: AssetType!
|
||||
dataTypesStored: String!
|
||||
organization: Organization! @goField(forceResolver: true)
|
||||
createdAt: Datetime!
|
||||
@@ -5513,162 +5128,3 @@ type CreateCustomDomainPayload {
|
||||
type DeleteCustomDomainPayload {
|
||||
deletedCustomDomainId: ID!
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# SAML Configuration Types
|
||||
# ============================================
|
||||
|
||||
type SAMLConfiguration implements Node {
|
||||
id: ID!
|
||||
organization: Organization! @goField(forceResolver: true)
|
||||
emailDomain: String!
|
||||
enabled: Boolean!
|
||||
enforcementPolicy: SAMLEnforcementPolicy!
|
||||
|
||||
# Domain verification (required before SAML can be configured)
|
||||
domainVerified: Boolean!
|
||||
domainVerificationToken: String
|
||||
domainVerifiedAt: Datetime
|
||||
|
||||
# Service Provider metadata (read-only, auto-generated)
|
||||
spEntityId: String!
|
||||
spAcsUrl: String!
|
||||
spMetadataUrl: String! @goField(forceResolver: true)
|
||||
|
||||
# Identity Provider configuration
|
||||
idpEntityId: String!
|
||||
idpSsoUrl: String!
|
||||
idpCertificate: String!
|
||||
idpMetadataUrl: String
|
||||
|
||||
# Attribute mapping
|
||||
attributeEmail: String!
|
||||
attributeFirstname: String!
|
||||
attributeLastname: String!
|
||||
attributeRole: String!
|
||||
|
||||
# Auto-signup
|
||||
autoSignupEnabled: Boolean!
|
||||
|
||||
# Test login URL for this configuration
|
||||
testLoginUrl: String! @goField(forceResolver: true)
|
||||
|
||||
createdAt: Datetime!
|
||||
updatedAt: Datetime!
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# SAML Configuration Inputs
|
||||
# ============================================
|
||||
|
||||
input CreateSAMLConfigurationInput {
|
||||
organizationId: ID!
|
||||
|
||||
# Email domain this config applies to
|
||||
emailDomain: String!
|
||||
|
||||
# Enforcement policy for this SAML configuration
|
||||
enforcementPolicy: SAMLEnforcementPolicy!
|
||||
|
||||
# SP configuration (optional - auto-generated if not provided)
|
||||
spCertificate: String
|
||||
spPrivateKey: String
|
||||
|
||||
# IdP configuration - Option 1: Provide metadata XML (recommended for Google Workspace)
|
||||
# This will automatically extract entityId, ssoUrl, and certificate from the metadata
|
||||
idpMetadataXml: String
|
||||
|
||||
# IdP configuration - Option 2: Provide individual fields manually
|
||||
# Required if idpMetadataXml is not provided
|
||||
idpEntityId: String
|
||||
idpSsoUrl: String
|
||||
idpCertificate: String
|
||||
idpMetadataUrl: String
|
||||
|
||||
# Attribute mapping (optional, defaults provided)
|
||||
attributeEmail: String
|
||||
attributeFirstname: String
|
||||
attributeLastname: String
|
||||
attributeRole: String
|
||||
|
||||
autoSignupEnabled: Boolean
|
||||
}
|
||||
|
||||
input UpdateSAMLConfigurationInput {
|
||||
id: ID!
|
||||
|
||||
enabled: Boolean
|
||||
enforcementPolicy: SAMLEnforcementPolicy
|
||||
spCertificate: String
|
||||
spPrivateKey: String
|
||||
idpEntityId: String
|
||||
idpSsoUrl: String
|
||||
idpCertificate: String
|
||||
idpMetadataUrl: String
|
||||
attributeEmail: String
|
||||
attributeFirstname: String
|
||||
attributeLastname: String
|
||||
attributeRole: String
|
||||
autoSignupEnabled: Boolean
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# Domain Verification Inputs
|
||||
# ============================================
|
||||
|
||||
input InitiateDomainVerificationInput {
|
||||
organizationId: ID!
|
||||
emailDomain: String!
|
||||
}
|
||||
|
||||
input VerifyDomainInput {
|
||||
id: ID!
|
||||
}
|
||||
|
||||
input DeleteSAMLConfigurationInput {
|
||||
id: ID!
|
||||
}
|
||||
|
||||
input EnableSAMLInput {
|
||||
id: ID!
|
||||
}
|
||||
|
||||
input DisableSAMLInput {
|
||||
id: ID!
|
||||
}
|
||||
|
||||
# ============================================
|
||||
# SAML Configuration Payloads
|
||||
# ============================================
|
||||
|
||||
type InitiateDomainVerificationPayload {
|
||||
samlConfiguration: SAMLConfiguration!
|
||||
# The TXT record value that needs to be added to DNS
|
||||
# Format: probo-verification={token}
|
||||
dnsRecord: String!
|
||||
}
|
||||
|
||||
type VerifyDomainPayload {
|
||||
samlConfiguration: SAMLConfiguration!
|
||||
verified: Boolean!
|
||||
}
|
||||
|
||||
type CreateSAMLConfigurationPayload {
|
||||
samlConfiguration: SAMLConfiguration!
|
||||
}
|
||||
|
||||
type UpdateSAMLConfigurationPayload {
|
||||
samlConfiguration: SAMLConfiguration!
|
||||
}
|
||||
|
||||
type DeleteSAMLConfigurationPayload {
|
||||
deletedSAMLConfigurationId: ID!
|
||||
}
|
||||
|
||||
type EnableSAMLPayload {
|
||||
samlConfiguration: SAMLConfiguration!
|
||||
}
|
||||
|
||||
type DisableSAMLPayload {
|
||||
samlConfiguration: SAMLConfiguration!
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,33 +16,8 @@ package types
|
||||
|
||||
import (
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/page"
|
||||
)
|
||||
|
||||
type (
|
||||
OrganizationOrderBy OrderBy[coredata.OrganizationOrderField]
|
||||
)
|
||||
|
||||
func NewOrganizationConnection(page *page.Page[*coredata.Organization, coredata.OrganizationOrderField]) *OrganizationConnection {
|
||||
var edges = make([]*OrganizationEdge, len(page.Data))
|
||||
|
||||
for i := range edges {
|
||||
edges[i] = NewOrganizationEdge(page.Data[i], page.Cursor.OrderBy.Field)
|
||||
}
|
||||
|
||||
return &OrganizationConnection{
|
||||
Edges: edges,
|
||||
PageInfo: NewPageInfo(page),
|
||||
}
|
||||
}
|
||||
|
||||
func NewOrganizationEdge(o *coredata.Organization, orderBy coredata.OrganizationOrderField) *OrganizationEdge {
|
||||
return &OrganizationEdge{
|
||||
Cursor: o.CursorKey(orderBy),
|
||||
Node: NewOrganization(o),
|
||||
}
|
||||
}
|
||||
|
||||
func NewOrganization(o *coredata.Organization) *Organization {
|
||||
return &Organization{
|
||||
ID: o.ID,
|
||||
|
||||
@@ -64,7 +64,6 @@ func NewPeopleEdge(p *coredata.People, orderBy coredata.PeopleOrderField) *Peopl
|
||||
}
|
||||
|
||||
func NewPeople(p *coredata.People) *People {
|
||||
|
||||
return &People{
|
||||
ID: p.ID,
|
||||
FullName: p.FullName,
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
// 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 NewSAMLConfigurationWithURLs(c *coredata.SAMLConfiguration, spEntityID, spAcsURL string) *SAMLConfiguration {
|
||||
return &SAMLConfiguration{
|
||||
ID: c.ID,
|
||||
EmailDomain: c.EmailDomain,
|
||||
Enabled: c.Enabled,
|
||||
EnforcementPolicy: c.EnforcementPolicy,
|
||||
DomainVerified: c.DomainVerified,
|
||||
DomainVerificationToken: c.DomainVerificationToken,
|
||||
DomainVerifiedAt: c.DomainVerifiedAt,
|
||||
SpEntityID: spEntityID,
|
||||
SpAcsURL: spAcsURL,
|
||||
IdpEntityID: c.IdPEntityID,
|
||||
IdpSsoURL: c.IdPSsoURL,
|
||||
IdpCertificate: c.IdPCertificate,
|
||||
IdpMetadataURL: c.IdPMetadataURL,
|
||||
AttributeEmail: c.AttributeEmail,
|
||||
AttributeFirstname: c.AttributeFirstname,
|
||||
AttributeLastname: c.AttributeLastname,
|
||||
AttributeRole: c.AttributeRole,
|
||||
AutoSignupEnabled: c.AutoSignupEnabled,
|
||||
CreatedAt: c.CreatedAt,
|
||||
UpdatedAt: c.UpdatedAt,
|
||||
}
|
||||
}
|
||||
@@ -65,7 +65,13 @@ func NewStateOfApplicabilityEdge(soa *coredata.StateOfApplicability, orderBy cor
|
||||
|
||||
func NewStateOfApplicability(soa *coredata.StateOfApplicability) *StateOfApplicability {
|
||||
return &StateOfApplicability{
|
||||
ID: soa.ID,
|
||||
ID: soa.ID,
|
||||
Organization: &Organization{
|
||||
ID: soa.OrganizationID,
|
||||
},
|
||||
Owner: &People{
|
||||
ID: soa.OwnerID,
|
||||
},
|
||||
Name: soa.Name,
|
||||
SourceID: soa.SourceID,
|
||||
SnapshotID: soa.SnapshotID,
|
||||
|
||||
@@ -61,7 +61,7 @@ func NewTaskEdge(t *coredata.Task, orderBy coredata.TaskOrderField) *TaskEdge {
|
||||
}
|
||||
|
||||
func NewTask(t *coredata.Task) *Task {
|
||||
return &Task{
|
||||
node := &Task{
|
||||
ID: t.ID,
|
||||
Name: t.Name,
|
||||
Description: t.Description,
|
||||
@@ -71,4 +71,12 @@ func NewTask(t *coredata.Task) *Task {
|
||||
UpdatedAt: t.UpdatedAt,
|
||||
Deadline: t.Deadline,
|
||||
}
|
||||
|
||||
if t.MeasureID != nil {
|
||||
node.Measure = &Measure{
|
||||
ID: *t.MeasureID,
|
||||
}
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
||||
|
||||
@@ -3,10 +3,6 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
@@ -21,14 +17,6 @@ type Node interface {
|
||||
GetID() gid.GID
|
||||
}
|
||||
|
||||
type AcceptInvitationInput struct {
|
||||
InvitationID gid.GID `json:"invitationId"`
|
||||
}
|
||||
|
||||
type AcceptInvitationPayload struct {
|
||||
Invitation *Invitation `json:"invitation"`
|
||||
}
|
||||
|
||||
type AssessVendorInput struct {
|
||||
ID gid.GID `json:"id"`
|
||||
WebsiteURL string `json:"websiteUrl"`
|
||||
@@ -150,14 +138,6 @@ type CancelSignatureRequestPayload struct {
|
||||
DeletedDocumentVersionSignatureID gid.GID `json:"deletedDocumentVersionSignatureId"`
|
||||
}
|
||||
|
||||
type ConfirmEmailInput struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type ConfirmEmailPayload struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
|
||||
type ContinualImprovement struct {
|
||||
ID gid.GID `json:"id"`
|
||||
SnapshotID *gid.GID `json:"snapshotId,omitempty"`
|
||||
@@ -441,14 +421,6 @@ type CreateObligationPayload struct {
|
||||
ObligationEdge *ObligationEdge `json:"obligationEdge"`
|
||||
}
|
||||
|
||||
type CreateOrganizationInput struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type CreateOrganizationPayload struct {
|
||||
OrganizationEdge *OrganizationEdge `json:"organizationEdge"`
|
||||
}
|
||||
|
||||
type CreatePeopleInput struct {
|
||||
OrganizationID gid.GID `json:"organizationId"`
|
||||
FullName string `json:"fullName"`
|
||||
@@ -555,28 +527,6 @@ type CreateRiskPayload struct {
|
||||
RiskEdge *RiskEdge `json:"riskEdge"`
|
||||
}
|
||||
|
||||
type CreateSAMLConfigurationInput struct {
|
||||
OrganizationID gid.GID `json:"organizationId"`
|
||||
EmailDomain string `json:"emailDomain"`
|
||||
EnforcementPolicy coredata.SAMLEnforcementPolicy `json:"enforcementPolicy"`
|
||||
SpCertificate *string `json:"spCertificate,omitempty"`
|
||||
SpPrivateKey *string `json:"spPrivateKey,omitempty"`
|
||||
IdpMetadataXML *string `json:"idpMetadataXml,omitempty"`
|
||||
IdpEntityID *string `json:"idpEntityId,omitempty"`
|
||||
IdpSsoURL *string `json:"idpSsoUrl,omitempty"`
|
||||
IdpCertificate *string `json:"idpCertificate,omitempty"`
|
||||
IdpMetadataURL *string `json:"idpMetadataUrl,omitempty"`
|
||||
AttributeEmail *string `json:"attributeEmail,omitempty"`
|
||||
AttributeFirstname *string `json:"attributeFirstname,omitempty"`
|
||||
AttributeLastname *string `json:"attributeLastname,omitempty"`
|
||||
AttributeRole *string `json:"attributeRole,omitempty"`
|
||||
AutoSignupEnabled *bool `json:"autoSignupEnabled,omitempty"`
|
||||
}
|
||||
|
||||
type CreateSAMLConfigurationPayload struct {
|
||||
SamlConfiguration *SAMLConfiguration `json:"samlConfiguration"`
|
||||
}
|
||||
|
||||
type CreateSnapshotInput struct {
|
||||
OrganizationID gid.GID `json:"organizationId"`
|
||||
Name string `json:"name"`
|
||||
@@ -936,14 +886,6 @@ type DeleteFrameworkPayload struct {
|
||||
DeletedFrameworkID gid.GID `json:"deletedFrameworkId"`
|
||||
}
|
||||
|
||||
type DeleteInvitationInput struct {
|
||||
InvitationID gid.GID `json:"invitationId"`
|
||||
}
|
||||
|
||||
type DeleteInvitationPayload struct {
|
||||
DeletedInvitationID gid.GID `json:"deletedInvitationId"`
|
||||
}
|
||||
|
||||
type DeleteMeasureInput struct {
|
||||
MeasureID gid.GID `json:"measureId"`
|
||||
}
|
||||
@@ -976,22 +918,6 @@ type DeleteObligationPayload struct {
|
||||
DeletedObligationID gid.GID `json:"deletedObligationId"`
|
||||
}
|
||||
|
||||
type DeleteOrganizationHorizontalLogoInput struct {
|
||||
OrganizationID gid.GID `json:"organizationId"`
|
||||
}
|
||||
|
||||
type DeleteOrganizationHorizontalLogoPayload struct {
|
||||
Organization *Organization `json:"organization"`
|
||||
}
|
||||
|
||||
type DeleteOrganizationInput struct {
|
||||
OrganizationID gid.GID `json:"organizationId"`
|
||||
}
|
||||
|
||||
type DeleteOrganizationPayload struct {
|
||||
DeletedOrganizationID gid.GID `json:"deletedOrganizationId"`
|
||||
}
|
||||
|
||||
type DeletePeopleInput struct {
|
||||
PeopleID gid.GID `json:"peopleId"`
|
||||
}
|
||||
@@ -1054,14 +980,6 @@ type DeleteRiskPayload struct {
|
||||
DeletedRiskID gid.GID `json:"deletedRiskId"`
|
||||
}
|
||||
|
||||
type DeleteSAMLConfigurationInput struct {
|
||||
ID gid.GID `json:"id"`
|
||||
}
|
||||
|
||||
type DeleteSAMLConfigurationPayload struct {
|
||||
DeletedSAMLConfigurationID gid.GID `json:"deletedSAMLConfigurationId"`
|
||||
}
|
||||
|
||||
type DeleteSnapshotInput struct {
|
||||
SnapshotID gid.GID `json:"snapshotId"`
|
||||
}
|
||||
@@ -1185,14 +1103,6 @@ type DeleteVendorServicePayload struct {
|
||||
DeletedVendorServiceID gid.GID `json:"deletedVendorServiceId"`
|
||||
}
|
||||
|
||||
type DisableSAMLInput struct {
|
||||
ID gid.GID `json:"id"`
|
||||
}
|
||||
|
||||
type DisableSAMLPayload struct {
|
||||
SamlConfiguration *SAMLConfiguration `json:"samlConfiguration"`
|
||||
}
|
||||
|
||||
type Document struct {
|
||||
ID gid.GID `json:"id"`
|
||||
Title string `json:"title"`
|
||||
@@ -1268,14 +1178,6 @@ type DocumentVersionSignatureOrder struct {
|
||||
Direction page.OrderDirection `json:"direction"`
|
||||
}
|
||||
|
||||
type EnableSAMLInput struct {
|
||||
ID gid.GID `json:"id"`
|
||||
}
|
||||
|
||||
type EnableSAMLPayload struct {
|
||||
SamlConfiguration *SAMLConfiguration `json:"samlConfiguration"`
|
||||
}
|
||||
|
||||
type Evidence struct {
|
||||
ID gid.GID `json:"id"`
|
||||
Size int `json:"size"`
|
||||
@@ -1443,57 +1345,6 @@ type ImportMeasurePayload struct {
|
||||
MeasureEdges []*MeasureEdge `json:"measureEdges"`
|
||||
}
|
||||
|
||||
type InitiateDomainVerificationInput struct {
|
||||
OrganizationID gid.GID `json:"organizationId"`
|
||||
EmailDomain string `json:"emailDomain"`
|
||||
}
|
||||
|
||||
type InitiateDomainVerificationPayload struct {
|
||||
SamlConfiguration *SAMLConfiguration `json:"samlConfiguration"`
|
||||
DNSRecord string `json:"dnsRecord"`
|
||||
}
|
||||
|
||||
type Invitation struct {
|
||||
ID gid.GID `json:"id"`
|
||||
Email mail.Addr `json:"email"`
|
||||
FullName string `json:"fullName"`
|
||||
Role coredata.MembershipRole `json:"role"`
|
||||
Status coredata.InvitationStatus `json:"status"`
|
||||
ExpiresAt time.Time `json:"expiresAt"`
|
||||
AcceptedAt *time.Time `json:"acceptedAt,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
Organization *Organization `json:"organization"`
|
||||
}
|
||||
|
||||
func (Invitation) IsNode() {}
|
||||
func (this Invitation) GetID() gid.GID { return this.ID }
|
||||
|
||||
type InvitationEdge struct {
|
||||
Cursor page.CursorKey `json:"cursor"`
|
||||
Node *Invitation `json:"node"`
|
||||
}
|
||||
|
||||
type InvitationFilter struct {
|
||||
Statuses []coredata.InvitationStatus `json:"statuses,omitempty"`
|
||||
}
|
||||
|
||||
type InvitationOrder struct {
|
||||
Direction page.OrderDirection `json:"direction"`
|
||||
Field coredata.InvitationOrderField `json:"field"`
|
||||
}
|
||||
|
||||
type InviteUserInput struct {
|
||||
OrganizationID gid.GID `json:"organizationId"`
|
||||
Email mail.Addr `json:"email"`
|
||||
FullName string `json:"fullName"`
|
||||
Role coredata.MembershipRole `json:"role"`
|
||||
CreatePeople bool `json:"createPeople"`
|
||||
}
|
||||
|
||||
type InviteUserPayload struct {
|
||||
InvitationEdge *InvitationEdge `json:"invitationEdge"`
|
||||
}
|
||||
|
||||
type Measure struct {
|
||||
ID gid.GID `json:"id"`
|
||||
Category string `json:"category"`
|
||||
@@ -1540,26 +1391,6 @@ type MeetingEdge struct {
|
||||
Node *Meeting `json:"node"`
|
||||
}
|
||||
|
||||
type Membership struct {
|
||||
ID gid.GID `json:"id"`
|
||||
UserID gid.GID `json:"userID"`
|
||||
OrganizationID gid.GID `json:"organizationID"`
|
||||
Role coredata.MembershipRole `json:"role"`
|
||||
FullName string `json:"fullName"`
|
||||
EmailAddress mail.Addr `json:"emailAddress"`
|
||||
AuthMethod coredata.UserAuthMethod `json:"authMethod"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
func (Membership) IsNode() {}
|
||||
func (this Membership) GetID() gid.GID { return this.ID }
|
||||
|
||||
type MembershipEdge struct {
|
||||
Cursor page.CursorKey `json:"cursor"`
|
||||
Node *Membership `json:"node"`
|
||||
}
|
||||
|
||||
type Mutation struct {
|
||||
}
|
||||
|
||||
@@ -1634,8 +1465,6 @@ type Organization struct {
|
||||
Email *string `json:"email,omitempty"`
|
||||
HeadquarterAddress *string `json:"headquarterAddress,omitempty"`
|
||||
Context *OrganizationContext `json:"context,omitempty"`
|
||||
Memberships *MembershipConnection `json:"memberships"`
|
||||
Invitations *InvitationConnection `json:"invitations"`
|
||||
SlackConnections *SlackConnectionConnection `json:"slackConnections"`
|
||||
Frameworks *FrameworkConnection `json:"frameworks"`
|
||||
Controls *ControlConnection `json:"controls"`
|
||||
@@ -1661,7 +1490,6 @@ type Organization struct {
|
||||
TrustCenterFiles *TrustCenterFileConnection `json:"trustCenterFiles"`
|
||||
TrustCenter *TrustCenter `json:"trustCenter,omitempty"`
|
||||
CustomDomain *CustomDomain `json:"customDomain,omitempty"`
|
||||
SamlConfigurations []*SAMLConfiguration `json:"samlConfigurations"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
@@ -1669,21 +1497,11 @@ type Organization struct {
|
||||
func (Organization) IsNode() {}
|
||||
func (this Organization) GetID() gid.GID { return this.ID }
|
||||
|
||||
type OrganizationConnection struct {
|
||||
Edges []*OrganizationEdge `json:"edges"`
|
||||
PageInfo *PageInfo `json:"pageInfo"`
|
||||
}
|
||||
|
||||
type OrganizationContext struct {
|
||||
OrganizationID gid.GID `json:"organizationId"`
|
||||
Summary *string `json:"summary,omitempty"`
|
||||
}
|
||||
|
||||
type OrganizationEdge struct {
|
||||
Cursor page.CursorKey `json:"cursor"`
|
||||
Node *Organization `json:"node"`
|
||||
}
|
||||
|
||||
type OrganizationOrder struct {
|
||||
Direction page.OrderDirection `json:"direction"`
|
||||
Field coredata.OrganizationOrderField `json:"field"`
|
||||
@@ -1777,15 +1595,6 @@ type PublishDocumentVersionPayload struct {
|
||||
type Query struct {
|
||||
}
|
||||
|
||||
type RemoveMemberInput struct {
|
||||
OrganizationID gid.GID `json:"organizationId"`
|
||||
MemberID gid.GID `json:"memberId"`
|
||||
}
|
||||
|
||||
type RemoveMemberPayload struct {
|
||||
DeletedMemberID gid.GID `json:"deletedMemberId"`
|
||||
}
|
||||
|
||||
type Report struct {
|
||||
ID gid.GID `json:"id"`
|
||||
ObjectKey string `json:"objectKey"`
|
||||
@@ -1880,35 +1689,6 @@ type RiskFilter struct {
|
||||
SnapshotID *gid.GID `json:"snapshotId,omitempty"`
|
||||
}
|
||||
|
||||
type SAMLConfiguration struct {
|
||||
ID gid.GID `json:"id"`
|
||||
Organization *Organization `json:"organization"`
|
||||
EmailDomain string `json:"emailDomain"`
|
||||
Enabled bool `json:"enabled"`
|
||||
EnforcementPolicy coredata.SAMLEnforcementPolicy `json:"enforcementPolicy"`
|
||||
DomainVerified bool `json:"domainVerified"`
|
||||
DomainVerificationToken *string `json:"domainVerificationToken,omitempty"`
|
||||
DomainVerifiedAt *time.Time `json:"domainVerifiedAt,omitempty"`
|
||||
SpEntityID string `json:"spEntityId"`
|
||||
SpAcsURL string `json:"spAcsUrl"`
|
||||
SpMetadataURL string `json:"spMetadataUrl"`
|
||||
IdpEntityID string `json:"idpEntityId"`
|
||||
IdpSsoURL string `json:"idpSsoUrl"`
|
||||
IdpCertificate string `json:"idpCertificate"`
|
||||
IdpMetadataURL *string `json:"idpMetadataUrl,omitempty"`
|
||||
AttributeEmail string `json:"attributeEmail"`
|
||||
AttributeFirstname string `json:"attributeFirstname"`
|
||||
AttributeLastname string `json:"attributeLastname"`
|
||||
AttributeRole string `json:"attributeRole"`
|
||||
AutoSignupEnabled bool `json:"autoSignupEnabled"`
|
||||
TestLoginURL string `json:"testLoginUrl"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
func (SAMLConfiguration) IsNode() {}
|
||||
func (this SAMLConfiguration) GetID() gid.GID { return this.ID }
|
||||
|
||||
type SendSigningNotificationsInput struct {
|
||||
OrganizationID gid.GID `json:"organizationId"`
|
||||
}
|
||||
@@ -2289,16 +2069,6 @@ type UpdateMeetingPayload struct {
|
||||
Meeting *Meeting `json:"meeting"`
|
||||
}
|
||||
|
||||
type UpdateMembershipInput struct {
|
||||
OrganizationID gid.GID `json:"organizationId"`
|
||||
MemberID gid.GID `json:"memberId"`
|
||||
Role coredata.MembershipRole `json:"role"`
|
||||
}
|
||||
|
||||
type UpdateMembershipPayload struct {
|
||||
Membership *Membership `json:"membership"`
|
||||
}
|
||||
|
||||
type UpdateNonconformityInput struct {
|
||||
ID gid.GID `json:"id"`
|
||||
ReferenceID *string `json:"referenceId,omitempty"`
|
||||
@@ -2344,30 +2114,15 @@ type UpdateOrganizationContextPayload struct {
|
||||
Context *OrganizationContext `json:"context"`
|
||||
}
|
||||
|
||||
type UpdateOrganizationInput struct {
|
||||
OrganizationID gid.GID `json:"organizationId"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
Description graphql.Omittable[*string] `json:"description,omitempty"`
|
||||
WebsiteURL graphql.Omittable[*string] `json:"websiteUrl,omitempty"`
|
||||
Email graphql.Omittable[*string] `json:"email,omitempty"`
|
||||
HeadquarterAddress graphql.Omittable[*string] `json:"headquarterAddress,omitempty"`
|
||||
LogoFile *graphql.Upload `json:"logoFile,omitempty"`
|
||||
HorizontalLogoFile *graphql.Upload `json:"horizontalLogoFile,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateOrganizationPayload struct {
|
||||
Organization *Organization `json:"organization"`
|
||||
}
|
||||
|
||||
type UpdatePeopleInput struct {
|
||||
ID gid.GID `json:"id"`
|
||||
FullName *string `json:"fullName,omitempty"`
|
||||
PrimaryEmailAddress *mail.Addr `json:"primaryEmailAddress,omitempty"`
|
||||
AdditionalEmailAddresses []mail.Addr `json:"additionalEmailAddresses,omitempty"`
|
||||
Kind *coredata.PeopleKind `json:"kind,omitempty"`
|
||||
Position graphql.Omittable[*string] `json:"position,omitempty"`
|
||||
ContractStartDate graphql.Omittable[*time.Time] `json:"contractStartDate,omitempty"`
|
||||
ContractEndDate graphql.Omittable[*time.Time] `json:"contractEndDate,omitempty"`
|
||||
ID gid.GID `json:"id"`
|
||||
FullName *string `json:"fullName,omitempty"`
|
||||
PrimaryEmailAddress *mail.Addr `json:"primaryEmailAddress,omitempty"`
|
||||
AdditionalEmailAddresses graphql.Omittable[[]mail.Addr] `json:"additionalEmailAddresses,omitempty"`
|
||||
Kind *coredata.PeopleKind `json:"kind,omitempty"`
|
||||
Position graphql.Omittable[*string] `json:"position,omitempty"`
|
||||
ContractStartDate graphql.Omittable[*time.Time] `json:"contractStartDate,omitempty"`
|
||||
ContractEndDate graphql.Omittable[*time.Time] `json:"contractEndDate,omitempty"`
|
||||
}
|
||||
|
||||
type UpdatePeoplePayload struct {
|
||||
@@ -2435,27 +2190,6 @@ type UpdateRiskPayload struct {
|
||||
Risk *Risk `json:"risk"`
|
||||
}
|
||||
|
||||
type UpdateSAMLConfigurationInput struct {
|
||||
ID gid.GID `json:"id"`
|
||||
Enabled *bool `json:"enabled,omitempty"`
|
||||
EnforcementPolicy *coredata.SAMLEnforcementPolicy `json:"enforcementPolicy,omitempty"`
|
||||
SpCertificate *string `json:"spCertificate,omitempty"`
|
||||
SpPrivateKey *string `json:"spPrivateKey,omitempty"`
|
||||
IdpEntityID *string `json:"idpEntityId,omitempty"`
|
||||
IdpSsoURL *string `json:"idpSsoUrl,omitempty"`
|
||||
IdpCertificate *string `json:"idpCertificate,omitempty"`
|
||||
IdpMetadataURL *string `json:"idpMetadataUrl,omitempty"`
|
||||
AttributeEmail *string `json:"attributeEmail,omitempty"`
|
||||
AttributeFirstname *string `json:"attributeFirstname,omitempty"`
|
||||
AttributeLastname *string `json:"attributeLastname,omitempty"`
|
||||
AttributeRole *string `json:"attributeRole,omitempty"`
|
||||
AutoSignupEnabled *bool `json:"autoSignupEnabled,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateSAMLConfigurationPayload struct {
|
||||
SamlConfiguration *SAMLConfiguration `json:"samlConfiguration"`
|
||||
}
|
||||
|
||||
type UpdateStateOfApplicabilityInput struct {
|
||||
ID gid.GID `json:"id"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
@@ -2676,22 +2410,6 @@ type UploadVendorDataPrivacyAgreementPayload struct {
|
||||
VendorDataPrivacyAgreement *VendorDataPrivacyAgreement `json:"vendorDataPrivacyAgreement"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID gid.GID `json:"id"`
|
||||
FullName string `json:"fullName"`
|
||||
Email mail.Addr `json:"email"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
func (User) IsNode() {}
|
||||
func (this User) GetID() gid.GID { return this.ID }
|
||||
|
||||
type UserEdge struct {
|
||||
Cursor page.CursorKey `json:"cursor"`
|
||||
Node *User `json:"node"`
|
||||
}
|
||||
|
||||
type Vendor struct {
|
||||
ID gid.GID `json:"id"`
|
||||
SnapshotID *gid.GID `json:"snapshotId,omitempty"`
|
||||
@@ -2867,80 +2585,8 @@ type VendorServiceEdge struct {
|
||||
Node *VendorService `json:"node"`
|
||||
}
|
||||
|
||||
type VerifyDomainInput struct {
|
||||
ID gid.GID `json:"id"`
|
||||
}
|
||||
|
||||
type VerifyDomainPayload struct {
|
||||
SamlConfiguration *SAMLConfiguration `json:"samlConfiguration"`
|
||||
Verified bool `json:"verified"`
|
||||
}
|
||||
|
||||
type Viewer struct {
|
||||
ID gid.GID `json:"id"`
|
||||
User *User `json:"user"`
|
||||
Organizations *OrganizationConnection `json:"organizations"`
|
||||
SignableDocuments *SignableDocumentConnection `json:"signableDocuments"`
|
||||
SignableDocument *SignableDocument `json:"signableDocument,omitempty"`
|
||||
}
|
||||
|
||||
type Role string
|
||||
|
||||
const (
|
||||
RoleOwner Role = "OWNER"
|
||||
RoleAdmin Role = "ADMIN"
|
||||
RoleViewer Role = "VIEWER"
|
||||
RoleAuditor Role = "AUDITOR"
|
||||
RoleFull Role = "FULL"
|
||||
)
|
||||
|
||||
var AllRole = []Role{
|
||||
RoleOwner,
|
||||
RoleAdmin,
|
||||
RoleViewer,
|
||||
RoleAuditor,
|
||||
RoleFull,
|
||||
}
|
||||
|
||||
func (e Role) IsValid() bool {
|
||||
switch e {
|
||||
case RoleOwner, RoleAdmin, RoleViewer, RoleAuditor, RoleFull:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (e Role) String() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
func (e *Role) UnmarshalGQL(v any) error {
|
||||
str, ok := v.(string)
|
||||
if !ok {
|
||||
return fmt.Errorf("enums must be strings")
|
||||
}
|
||||
|
||||
*e = Role(str)
|
||||
if !e.IsValid() {
|
||||
return fmt.Errorf("%s is not a valid Role", str)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e Role) MarshalGQL(w io.Writer) {
|
||||
fmt.Fprint(w, strconv.Quote(e.String()))
|
||||
}
|
||||
|
||||
func (e *Role) UnmarshalJSON(b []byte) error {
|
||||
s, err := strconv.Unquote(string(b))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return e.UnmarshalGQL(s)
|
||||
}
|
||||
|
||||
func (e Role) MarshalJSON() ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
e.MarshalGQL(&buf)
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -22,7 +22,7 @@ import (
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
"go.gearno.de/kit/log"
|
||||
"go.probo.inc/probo/pkg/authz"
|
||||
"go.probo.inc/probo/pkg/iam"
|
||||
)
|
||||
|
||||
func RecoveryMiddleware(logger *log.Logger) func(mcp.MethodHandler) mcp.MethodHandler {
|
||||
@@ -47,14 +47,14 @@ func convertPanicToError(ctx context.Context, logger *log.Logger, panicValue any
|
||||
return fmt.Errorf("internal server error")
|
||||
}
|
||||
|
||||
var tenantAccessErr *authz.TenantAccessError
|
||||
var tenantAccessErr *iam.TenantAccessError
|
||||
if errTyped, ok := panicValue.(error); ok && errors.As(errTyped, &tenantAccessErr) {
|
||||
return fmt.Errorf("not authorized: %s", tenantAccessErr.Message)
|
||||
}
|
||||
|
||||
var permissionDeniedErr *authz.PermissionDeniedError
|
||||
var permissionDeniedErr *iam.ErrInsufficientPermissions
|
||||
if errTyped, ok := panicValue.(error); ok && errors.As(errTyped, &permissionDeniedErr) {
|
||||
return fmt.Errorf("permission denied: %s", permissionDeniedErr.Message)
|
||||
return fmt.Errorf("permission denied: %s", permissionDeniedErr.Error())
|
||||
}
|
||||
|
||||
if err, ok := panicValue.(error); ok {
|
||||
|
||||
@@ -17,18 +17,15 @@ package mcp_v1
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"errors"
|
||||
|
||||
"go.gearno.de/kit/httpserver"
|
||||
"go.gearno.de/kit/log"
|
||||
"go.probo.inc/probo/pkg/auth"
|
||||
"go.probo.inc/probo/pkg/authz"
|
||||
serverauth "go.probo.inc/probo/pkg/server/auth"
|
||||
connect_v1 "go.probo.inc/probo/pkg/server/api/connect/v1"
|
||||
)
|
||||
|
||||
// WithMCPAuth wraps an HTTP handler with MCP authentication middleware
|
||||
// It authenticates using API keys from the Authorization header
|
||||
func WithMCPAuth(
|
||||
func RequireAPIKeyHandler(
|
||||
logger *log.Logger,
|
||||
authSvc *auth.Service,
|
||||
authzSvc *authz.Service,
|
||||
next http.Handler,
|
||||
) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -43,27 +40,19 @@ func WithMCPAuth(
|
||||
log.String("path", r.URL.Path),
|
||||
)
|
||||
|
||||
// Authenticate using API key from shared function
|
||||
authCtx := serverauth.AuthenticateWithAPIKey(ctx, r, authSvc, authzSvc)
|
||||
if authCtx == nil {
|
||||
logger.WarnCtx(ctx, "MCP auth: authentication required",
|
||||
log.String("correlation_id", correlationID),
|
||||
)
|
||||
http.Error(w, "authentication required", http.StatusUnauthorized)
|
||||
apiKey := connect_v1.APIKeyFromContext(ctx)
|
||||
identity := connect_v1.UserFromContext(ctx)
|
||||
if identity == nil {
|
||||
httpserver.RenderError(w, http.StatusUnauthorized, errors.New("authentication required"))
|
||||
return
|
||||
}
|
||||
|
||||
user := serverauth.UserFromContext(authCtx)
|
||||
userAPIKey := serverauth.UserAPIKeyFromContext(authCtx)
|
||||
tenantAccess := serverauth.UserTenantAccessFromContext(authCtx)
|
||||
|
||||
logger.InfoCtx(authCtx, "MCP authentication successful",
|
||||
logger.InfoCtx(ctx, "MCP authentication successful",
|
||||
log.String("correlation_id", correlationID),
|
||||
log.String("user_id", user.ID.String()),
|
||||
log.String("api_key_id", userAPIKey.ID.String()),
|
||||
log.Int("accessible_tenants", len(tenantAccess.TenantIDs)),
|
||||
log.String("identity_id", identity.ID.String()),
|
||||
log.String("api_key_id", apiKey.ID.String()),
|
||||
)
|
||||
|
||||
next.ServeHTTP(w, r.WithContext(authCtx))
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,39 +6,32 @@ import (
|
||||
"context"
|
||||
|
||||
"go.gearno.de/kit/log"
|
||||
"go.probo.inc/probo/pkg/auth"
|
||||
"go.probo.inc/probo/pkg/authz"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/iam"
|
||||
"go.probo.inc/probo/pkg/probo"
|
||||
serverauth "go.probo.inc/probo/pkg/server/auth"
|
||||
connect_v1 "go.probo.inc/probo/pkg/server/api/connect/v1"
|
||||
)
|
||||
|
||||
type Resolver struct {
|
||||
proboSvc *probo.Service
|
||||
authSvc *auth.Service
|
||||
authzSvc *authz.Service
|
||||
iamSvc *iam.Service
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
func (r *Resolver) MustBeAuthorized(ctx context.Context, entityID gid.GID, action authz.Action) {
|
||||
user := serverauth.UserFromContext(ctx)
|
||||
apiKey := serverauth.UserAPIKeyFromContext(ctx)
|
||||
func (r *Resolver) MustBeAuthorized(ctx context.Context, entityID gid.GID, action iam.Action) {
|
||||
user := connect_v1.UserFromContext(ctx)
|
||||
apiKey := connect_v1.APIKeyFromContext(ctx)
|
||||
if user == nil {
|
||||
panic(&authz.TenantAccessError{Message: "authentication required"})
|
||||
panic(&iam.TenantAccessError{Message: "authentication required"})
|
||||
}
|
||||
|
||||
authzSvc := r.AuthzService(ctx, entityID.TenantID())
|
||||
err := authzSvc.Authorize(ctx, user, apiKey, entityID, action)
|
||||
var credentialID *gid.GID
|
||||
if apiKey != nil {
|
||||
credentialID = &apiKey.ID
|
||||
}
|
||||
|
||||
err := r.iamSvc.AccessManagementService.Authorize(ctx, user.ID, credentialID, entityID, action)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Resolver) AuthzService(ctx context.Context, tenantID gid.TenantID) *authz.TenantAuthzService {
|
||||
return GetTenantAuthzService(ctx, r.authzSvc, tenantID)
|
||||
}
|
||||
|
||||
func GetTenantAuthzService(ctx context.Context, authzSvc *authz.Service, tenantID gid.TenantID) *authz.TenantAuthzService {
|
||||
serverauth.RequireTenantAccess(ctx, tenantID)
|
||||
return authzSvc.WithTenant(tenantID)
|
||||
}
|
||||
|
||||
@@ -9,24 +9,21 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
"go.probo.inc/probo/pkg/authz"
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/iam"
|
||||
"go.probo.inc/probo/pkg/mail"
|
||||
"go.probo.inc/probo/pkg/page"
|
||||
"go.probo.inc/probo/pkg/probo"
|
||||
connect_v1 "go.probo.inc/probo/pkg/server/api/connect/v1"
|
||||
"go.probo.inc/probo/pkg/server/api/mcp/v1/types"
|
||||
serverauth "go.probo.inc/probo/pkg/server/auth"
|
||||
)
|
||||
|
||||
// ListOrganizationsTool handles the listOrganizations tool
|
||||
// List all organizations the user has access to
|
||||
func (r *Resolver) ListOrganizationsTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListOrganizationsInput) (*mcp.CallToolResult, types.ListOrganizationsOutput, error) {
|
||||
user := serverauth.UserFromContext(ctx)
|
||||
if user == nil {
|
||||
return nil, types.ListOrganizationsOutput{}, fmt.Errorf("authentication required")
|
||||
}
|
||||
user := connect_v1.UserFromContext(ctx)
|
||||
|
||||
organizations, err := r.authzSvc.GetAllUserOrganizations(ctx, user.ID)
|
||||
organizations, err := r.iamSvc.AccountService.ListOrganizations(ctx, user.ID)
|
||||
if err != nil {
|
||||
return nil, types.ListOrganizationsOutput{}, fmt.Errorf("failed to list organizations: %w", err)
|
||||
}
|
||||
@@ -45,7 +42,7 @@ func (r *Resolver) ListOrganizationsTool(ctx context.Context, req *mcp.CallToolR
|
||||
// ListVendorsTool handles the listVendors tool
|
||||
// List all vendors for the organization
|
||||
func (r *Resolver) ListVendorsTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListVendorsInput) (*mcp.CallToolResult, types.ListVendorsOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, authz.ActionListVendors)
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, iam.ActionListVendors)
|
||||
|
||||
prb := r.ProboService(ctx, input.OrganizationID)
|
||||
|
||||
@@ -78,7 +75,7 @@ func (r *Resolver) ListVendorsTool(ctx context.Context, req *mcp.CallToolRequest
|
||||
// AddVendorTool handles the addVendor tool
|
||||
// Add a new vendor to the organization
|
||||
func (r *Resolver) AddVendorTool(ctx context.Context, req *mcp.CallToolRequest, input *types.AddVendorInput) (*mcp.CallToolResult, types.AddVendorOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, authz.ActionCreateAsset)
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, iam.ActionCreateAsset)
|
||||
|
||||
svc := r.ProboService(ctx, input.OrganizationID)
|
||||
|
||||
@@ -122,7 +119,7 @@ func (r *Resolver) UpdateVendorTool(ctx context.Context, req *mcp.CallToolReques
|
||||
}
|
||||
|
||||
func (r *Resolver) ListPeopleTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListPeopleInput) (*mcp.CallToolResult, types.ListPeopleOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, authz.ActionListPeople)
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, iam.ActionListPeople)
|
||||
|
||||
prb := r.ProboService(ctx, input.OrganizationID)
|
||||
|
||||
@@ -153,7 +150,7 @@ func (r *Resolver) ListPeopleTool(ctx context.Context, req *mcp.CallToolRequest,
|
||||
}
|
||||
|
||||
func (r *Resolver) GetPeopleTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetPeopleInput) (*mcp.CallToolResult, types.GetPeopleOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.ID, authz.ActionGet)
|
||||
r.MustBeAuthorized(ctx, input.ID, iam.ActionGet)
|
||||
|
||||
prb := r.ProboService(ctx, input.ID)
|
||||
|
||||
@@ -168,7 +165,7 @@ func (r *Resolver) GetPeopleTool(ctx context.Context, req *mcp.CallToolRequest,
|
||||
}
|
||||
|
||||
func (r *Resolver) AddPeopleTool(ctx context.Context, req *mcp.CallToolRequest, input *types.AddPeopleInput) (*mcp.CallToolResult, types.AddPeopleOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, authz.ActionCreatePeople)
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, iam.ActionCreatePeople)
|
||||
|
||||
svc := r.ProboService(ctx, input.OrganizationID)
|
||||
|
||||
@@ -200,7 +197,7 @@ func (r *Resolver) AddPeopleTool(ctx context.Context, req *mcp.CallToolRequest,
|
||||
}
|
||||
|
||||
func (r *Resolver) UpdatePeopleTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdatePeopleInput) (*mcp.CallToolResult, types.UpdatePeopleOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.ID, authz.ActionUpdatePeople)
|
||||
r.MustBeAuthorized(ctx, input.ID, iam.ActionUpdatePeople)
|
||||
|
||||
svc := r.ProboService(ctx, input.ID)
|
||||
|
||||
@@ -227,7 +224,7 @@ func (r *Resolver) UpdatePeopleTool(ctx context.Context, req *mcp.CallToolReques
|
||||
}
|
||||
|
||||
func (r *Resolver) ListRisksTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListRisksInput) (*mcp.CallToolResult, types.ListRisksOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, authz.ActionListRisks)
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, iam.ActionListRisks)
|
||||
|
||||
prb := r.ProboService(ctx, input.OrganizationID)
|
||||
|
||||
@@ -258,7 +255,7 @@ func (r *Resolver) ListRisksTool(ctx context.Context, req *mcp.CallToolRequest,
|
||||
}
|
||||
|
||||
func (r *Resolver) GetRiskTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetRiskInput) (*mcp.CallToolResult, types.GetRiskOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.ID, authz.ActionGet)
|
||||
r.MustBeAuthorized(ctx, input.ID, iam.ActionGet)
|
||||
|
||||
prb := r.ProboService(ctx, input.ID)
|
||||
|
||||
@@ -273,7 +270,7 @@ func (r *Resolver) GetRiskTool(ctx context.Context, req *mcp.CallToolRequest, in
|
||||
}
|
||||
|
||||
func (r *Resolver) AddRiskTool(ctx context.Context, req *mcp.CallToolRequest, input *types.AddRiskInput) (*mcp.CallToolResult, types.AddRiskOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, authz.ActionCreateRisk)
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, iam.ActionCreateRisk)
|
||||
|
||||
svc := r.ProboService(ctx, input.OrganizationID)
|
||||
|
||||
@@ -301,7 +298,7 @@ func (r *Resolver) AddRiskTool(ctx context.Context, req *mcp.CallToolRequest, in
|
||||
}
|
||||
|
||||
func (r *Resolver) UpdateRiskTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateRiskInput) (*mcp.CallToolResult, types.UpdateRiskOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.ID, authz.ActionUpdateRisk)
|
||||
r.MustBeAuthorized(ctx, input.ID, iam.ActionUpdateRisk)
|
||||
|
||||
svc := r.ProboService(ctx, input.ID)
|
||||
|
||||
@@ -330,7 +327,7 @@ func (r *Resolver) UpdateRiskTool(ctx context.Context, req *mcp.CallToolRequest,
|
||||
}
|
||||
|
||||
func (r *Resolver) ListMeasuresTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListMeasuresInput) (*mcp.CallToolResult, types.ListMeasuresOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, authz.ActionListMeasures)
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, iam.ActionListMeasures)
|
||||
|
||||
prb := r.ProboService(ctx, input.OrganizationID)
|
||||
|
||||
@@ -361,7 +358,7 @@ func (r *Resolver) ListMeasuresTool(ctx context.Context, req *mcp.CallToolReques
|
||||
}
|
||||
|
||||
func (r *Resolver) GetMeasureTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetMeasureInput) (*mcp.CallToolResult, types.GetMeasureOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.ID, authz.ActionGet)
|
||||
r.MustBeAuthorized(ctx, input.ID, iam.ActionGet)
|
||||
|
||||
prb := r.ProboService(ctx, input.ID)
|
||||
|
||||
@@ -376,7 +373,7 @@ func (r *Resolver) GetMeasureTool(ctx context.Context, req *mcp.CallToolRequest,
|
||||
}
|
||||
|
||||
func (r *Resolver) AddMeasureTool(ctx context.Context, req *mcp.CallToolRequest, input *types.AddMeasureInput) (*mcp.CallToolResult, types.AddMeasureOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, authz.ActionCreateMeasure)
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, iam.ActionCreateMeasure)
|
||||
|
||||
svc := r.ProboService(ctx, input.OrganizationID)
|
||||
|
||||
@@ -399,7 +396,7 @@ func (r *Resolver) AddMeasureTool(ctx context.Context, req *mcp.CallToolRequest,
|
||||
}
|
||||
|
||||
func (r *Resolver) UpdateMeasureTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateMeasureInput) (*mcp.CallToolResult, types.UpdateMeasureOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.ID, authz.ActionUpdateMeasure)
|
||||
r.MustBeAuthorized(ctx, input.ID, iam.ActionUpdateMeasure)
|
||||
|
||||
svc := r.ProboService(ctx, input.ID)
|
||||
|
||||
@@ -423,7 +420,7 @@ func (r *Resolver) UpdateMeasureTool(ctx context.Context, req *mcp.CallToolReque
|
||||
}
|
||||
|
||||
func (r *Resolver) ListFrameworksTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListFrameworksInput) (*mcp.CallToolResult, types.ListFrameworksOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, authz.ActionListFrameworks)
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, iam.ActionListFrameworks)
|
||||
|
||||
prb := r.ProboService(ctx, input.OrganizationID)
|
||||
|
||||
@@ -449,7 +446,7 @@ func (r *Resolver) ListFrameworksTool(ctx context.Context, req *mcp.CallToolRequ
|
||||
}
|
||||
|
||||
func (r *Resolver) GetFrameworkTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetFrameworkInput) (*mcp.CallToolResult, types.GetFrameworkOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.ID, authz.ActionGet)
|
||||
r.MustBeAuthorized(ctx, input.ID, iam.ActionGet)
|
||||
|
||||
prb := r.ProboService(ctx, input.ID)
|
||||
|
||||
@@ -464,7 +461,7 @@ func (r *Resolver) GetFrameworkTool(ctx context.Context, req *mcp.CallToolReques
|
||||
}
|
||||
|
||||
func (r *Resolver) AddFrameworkTool(ctx context.Context, req *mcp.CallToolRequest, input *types.AddFrameworkInput) (*mcp.CallToolResult, types.AddFrameworkOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, authz.ActionCreateFramework)
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, iam.ActionCreateFramework)
|
||||
|
||||
svc := r.ProboService(ctx, input.OrganizationID)
|
||||
|
||||
@@ -486,7 +483,7 @@ func (r *Resolver) AddFrameworkTool(ctx context.Context, req *mcp.CallToolReques
|
||||
}
|
||||
|
||||
func (r *Resolver) UpdateFrameworkTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateFrameworkInput) (*mcp.CallToolResult, types.UpdateFrameworkOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.ID, authz.ActionUpdateFramework)
|
||||
r.MustBeAuthorized(ctx, input.ID, iam.ActionUpdateFramework)
|
||||
|
||||
svc := r.ProboService(ctx, input.ID)
|
||||
|
||||
@@ -508,7 +505,7 @@ func (r *Resolver) UpdateFrameworkTool(ctx context.Context, req *mcp.CallToolReq
|
||||
}
|
||||
|
||||
func (r *Resolver) ListAssetsTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListAssetsInput) (*mcp.CallToolResult, types.ListAssetsOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, authz.ActionListAssets)
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, iam.ActionListAssets)
|
||||
|
||||
prb := r.ProboService(ctx, input.OrganizationID)
|
||||
|
||||
@@ -539,7 +536,7 @@ func (r *Resolver) ListAssetsTool(ctx context.Context, req *mcp.CallToolRequest,
|
||||
}
|
||||
|
||||
func (r *Resolver) GetAssetTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetAssetInput) (*mcp.CallToolResult, types.GetAssetOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.ID, authz.ActionGet)
|
||||
r.MustBeAuthorized(ctx, input.ID, iam.ActionGet)
|
||||
|
||||
prb := r.ProboService(ctx, input.ID)
|
||||
|
||||
@@ -554,7 +551,7 @@ func (r *Resolver) GetAssetTool(ctx context.Context, req *mcp.CallToolRequest, i
|
||||
}
|
||||
|
||||
func (r *Resolver) AddAssetTool(ctx context.Context, req *mcp.CallToolRequest, input *types.AddAssetInput) (*mcp.CallToolResult, types.AddAssetOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, authz.ActionCreateAsset)
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, iam.ActionCreateAsset)
|
||||
|
||||
svc := r.ProboService(ctx, input.OrganizationID)
|
||||
|
||||
@@ -580,7 +577,7 @@ func (r *Resolver) AddAssetTool(ctx context.Context, req *mcp.CallToolRequest, i
|
||||
}
|
||||
|
||||
func (r *Resolver) UpdateAssetTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateAssetInput) (*mcp.CallToolResult, types.UpdateAssetOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.ID, authz.ActionUpdateAsset)
|
||||
r.MustBeAuthorized(ctx, input.ID, iam.ActionUpdateAsset)
|
||||
|
||||
svc := r.ProboService(ctx, input.ID)
|
||||
|
||||
@@ -606,7 +603,7 @@ func (r *Resolver) UpdateAssetTool(ctx context.Context, req *mcp.CallToolRequest
|
||||
}
|
||||
|
||||
func (r *Resolver) ListDataTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListDataInput) (*mcp.CallToolResult, types.ListDataOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, authz.ActionListData)
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, iam.ActionListData)
|
||||
|
||||
prb := r.ProboService(ctx, input.OrganizationID)
|
||||
|
||||
@@ -637,7 +634,7 @@ func (r *Resolver) ListDataTool(ctx context.Context, req *mcp.CallToolRequest, i
|
||||
}
|
||||
|
||||
func (r *Resolver) GetDatumTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetDatumInput) (*mcp.CallToolResult, types.GetDatumOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.ID, authz.ActionGet)
|
||||
r.MustBeAuthorized(ctx, input.ID, iam.ActionGet)
|
||||
|
||||
prb := r.ProboService(ctx, input.ID)
|
||||
|
||||
@@ -652,7 +649,7 @@ func (r *Resolver) GetDatumTool(ctx context.Context, req *mcp.CallToolRequest, i
|
||||
}
|
||||
|
||||
func (r *Resolver) AddDatumTool(ctx context.Context, req *mcp.CallToolRequest, input *types.AddDatumInput) (*mcp.CallToolResult, types.AddDatumOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, authz.ActionCreateDatum)
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, iam.ActionCreateDatum)
|
||||
|
||||
svc := r.ProboService(ctx, input.OrganizationID)
|
||||
|
||||
@@ -676,7 +673,7 @@ func (r *Resolver) AddDatumTool(ctx context.Context, req *mcp.CallToolRequest, i
|
||||
}
|
||||
|
||||
func (r *Resolver) UpdateDatumTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateDatumInput) (*mcp.CallToolResult, types.UpdateDatumOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.ID, authz.ActionUpdateDatum)
|
||||
r.MustBeAuthorized(ctx, input.ID, iam.ActionUpdateDatum)
|
||||
|
||||
svc := r.ProboService(ctx, input.ID)
|
||||
|
||||
@@ -700,7 +697,7 @@ func (r *Resolver) UpdateDatumTool(ctx context.Context, req *mcp.CallToolRequest
|
||||
}
|
||||
|
||||
func (r *Resolver) ListNonconformitiesTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListNonconformitiesInput) (*mcp.CallToolResult, types.ListNonconformitiesOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, authz.ActionListNonconformities)
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, iam.ActionListNonconformities)
|
||||
|
||||
prb := r.ProboService(ctx, input.OrganizationID)
|
||||
|
||||
@@ -731,7 +728,7 @@ func (r *Resolver) ListNonconformitiesTool(ctx context.Context, req *mcp.CallToo
|
||||
}
|
||||
|
||||
func (r *Resolver) GetNonconformityTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetNonconformityInput) (*mcp.CallToolResult, types.GetNonconformityOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.ID, authz.ActionGet)
|
||||
r.MustBeAuthorized(ctx, input.ID, iam.ActionGet)
|
||||
|
||||
prb := r.ProboService(ctx, input.ID)
|
||||
|
||||
@@ -746,7 +743,7 @@ func (r *Resolver) GetNonconformityTool(ctx context.Context, req *mcp.CallToolRe
|
||||
}
|
||||
|
||||
func (r *Resolver) AddNonconformityTool(ctx context.Context, req *mcp.CallToolRequest, input *types.AddNonconformityInput) (*mcp.CallToolResult, types.AddNonconformityOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, authz.ActionCreateNonconformity)
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, iam.ActionCreateNonconformity)
|
||||
|
||||
svc := r.ProboService(ctx, input.OrganizationID)
|
||||
|
||||
@@ -776,7 +773,7 @@ func (r *Resolver) AddNonconformityTool(ctx context.Context, req *mcp.CallToolRe
|
||||
}
|
||||
|
||||
func (r *Resolver) UpdateNonconformityTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateNonconformityInput) (*mcp.CallToolResult, types.UpdateNonconformityOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.ID, authz.ActionUpdateNonconformity)
|
||||
r.MustBeAuthorized(ctx, input.ID, iam.ActionUpdateNonconformity)
|
||||
|
||||
svc := r.ProboService(ctx, input.ID)
|
||||
|
||||
@@ -806,7 +803,7 @@ func (r *Resolver) UpdateNonconformityTool(ctx context.Context, req *mcp.CallToo
|
||||
}
|
||||
|
||||
func (r *Resolver) ListObligationsTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListObligationsInput) (*mcp.CallToolResult, types.ListObligationsOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, authz.ActionListObligations)
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, iam.ActionListObligations)
|
||||
|
||||
prb := r.ProboService(ctx, input.OrganizationID)
|
||||
|
||||
@@ -837,7 +834,7 @@ func (r *Resolver) ListObligationsTool(ctx context.Context, req *mcp.CallToolReq
|
||||
}
|
||||
|
||||
func (r *Resolver) GetObligationTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetObligationInput) (*mcp.CallToolResult, types.GetObligationOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.ID, authz.ActionGet)
|
||||
r.MustBeAuthorized(ctx, input.ID, iam.ActionGet)
|
||||
|
||||
prb := r.ProboService(ctx, input.ID)
|
||||
|
||||
@@ -852,7 +849,7 @@ func (r *Resolver) GetObligationTool(ctx context.Context, req *mcp.CallToolReque
|
||||
}
|
||||
|
||||
func (r *Resolver) AddObligationTool(ctx context.Context, req *mcp.CallToolRequest, input *types.AddObligationInput) (*mcp.CallToolResult, types.AddObligationOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, authz.ActionCreateObligation)
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, iam.ActionCreateObligation)
|
||||
|
||||
svc := r.ProboService(ctx, input.OrganizationID)
|
||||
|
||||
@@ -882,7 +879,7 @@ func (r *Resolver) AddObligationTool(ctx context.Context, req *mcp.CallToolReque
|
||||
}
|
||||
|
||||
func (r *Resolver) UpdateObligationTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateObligationInput) (*mcp.CallToolResult, types.UpdateObligationOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.ID, authz.ActionUpdateObligation)
|
||||
r.MustBeAuthorized(ctx, input.ID, iam.ActionUpdateObligation)
|
||||
|
||||
svc := r.ProboService(ctx, input.ID)
|
||||
|
||||
@@ -912,7 +909,7 @@ func (r *Resolver) UpdateObligationTool(ctx context.Context, req *mcp.CallToolRe
|
||||
}
|
||||
|
||||
func (r *Resolver) ListContinualImprovementsTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListContinualImprovementsInput) (*mcp.CallToolResult, types.ListContinualImprovementsOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, authz.ActionListContinualImprovements)
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, iam.ActionListContinualImprovements)
|
||||
|
||||
prb := r.ProboService(ctx, input.OrganizationID)
|
||||
|
||||
@@ -943,7 +940,7 @@ func (r *Resolver) ListContinualImprovementsTool(ctx context.Context, req *mcp.C
|
||||
}
|
||||
|
||||
func (r *Resolver) GetContinualImprovementTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetContinualImprovementInput) (*mcp.CallToolResult, types.GetContinualImprovementOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.ID, authz.ActionGet)
|
||||
r.MustBeAuthorized(ctx, input.ID, iam.ActionGet)
|
||||
|
||||
prb := r.ProboService(ctx, input.ID)
|
||||
|
||||
@@ -958,7 +955,7 @@ func (r *Resolver) GetContinualImprovementTool(ctx context.Context, req *mcp.Cal
|
||||
}
|
||||
|
||||
func (r *Resolver) AddContinualImprovementTool(ctx context.Context, req *mcp.CallToolRequest, input *types.AddContinualImprovementInput) (*mcp.CallToolResult, types.AddContinualImprovementOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, authz.ActionCreateContinualImprovement)
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, iam.ActionCreateContinualImprovement)
|
||||
|
||||
svc := r.ProboService(ctx, input.OrganizationID)
|
||||
|
||||
@@ -985,7 +982,7 @@ func (r *Resolver) AddContinualImprovementTool(ctx context.Context, req *mcp.Cal
|
||||
}
|
||||
|
||||
func (r *Resolver) UpdateContinualImprovementTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateContinualImprovementInput) (*mcp.CallToolResult, types.UpdateContinualImprovementOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.ID, authz.ActionUpdateContinualImprovement)
|
||||
r.MustBeAuthorized(ctx, input.ID, iam.ActionUpdateContinualImprovement)
|
||||
|
||||
svc := r.ProboService(ctx, input.ID)
|
||||
|
||||
@@ -1012,7 +1009,7 @@ func (r *Resolver) UpdateContinualImprovementTool(ctx context.Context, req *mcp.
|
||||
}
|
||||
|
||||
func (r *Resolver) ListAuditsTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListAuditsInput) (*mcp.CallToolResult, types.ListAuditsOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, authz.ActionListAudits)
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, iam.ActionListAudits)
|
||||
|
||||
prb := r.ProboService(ctx, input.OrganizationID)
|
||||
|
||||
@@ -1038,7 +1035,7 @@ func (r *Resolver) ListAuditsTool(ctx context.Context, req *mcp.CallToolRequest,
|
||||
}
|
||||
|
||||
func (r *Resolver) GetAuditTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetAuditInput) (*mcp.CallToolResult, types.GetAuditOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.ID, authz.ActionGet)
|
||||
r.MustBeAuthorized(ctx, input.ID, iam.ActionGet)
|
||||
|
||||
prb := r.ProboService(ctx, input.ID)
|
||||
|
||||
@@ -1053,7 +1050,7 @@ func (r *Resolver) GetAuditTool(ctx context.Context, req *mcp.CallToolRequest, i
|
||||
}
|
||||
|
||||
func (r *Resolver) AddAuditTool(ctx context.Context, req *mcp.CallToolRequest, input *types.AddAuditInput) (*mcp.CallToolResult, types.AddAuditOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, authz.ActionCreateAudit)
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, iam.ActionCreateAudit)
|
||||
|
||||
svc := r.ProboService(ctx, input.OrganizationID)
|
||||
|
||||
@@ -1078,7 +1075,7 @@ func (r *Resolver) AddAuditTool(ctx context.Context, req *mcp.CallToolRequest, i
|
||||
}
|
||||
|
||||
func (r *Resolver) UpdateAuditTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateAuditInput) (*mcp.CallToolResult, types.UpdateAuditOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.ID, authz.ActionUpdateAudit)
|
||||
r.MustBeAuthorized(ctx, input.ID, iam.ActionUpdateAudit)
|
||||
|
||||
svc := r.ProboService(ctx, input.ID)
|
||||
|
||||
@@ -1103,7 +1100,7 @@ func (r *Resolver) UpdateAuditTool(ctx context.Context, req *mcp.CallToolRequest
|
||||
}
|
||||
|
||||
func (r *Resolver) ListControlsTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListControlsInput) (*mcp.CallToolResult, types.ListControlsOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, authz.ActionListControls)
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, iam.ActionListControls)
|
||||
|
||||
prb := r.ProboService(ctx, input.OrganizationID)
|
||||
|
||||
@@ -1134,7 +1131,7 @@ func (r *Resolver) ListControlsTool(ctx context.Context, req *mcp.CallToolReques
|
||||
}
|
||||
|
||||
func (r *Resolver) GetControlTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetControlInput) (*mcp.CallToolResult, types.GetControlOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.ID, authz.ActionGet)
|
||||
r.MustBeAuthorized(ctx, input.ID, iam.ActionGet)
|
||||
|
||||
prb := r.ProboService(ctx, input.ID)
|
||||
|
||||
@@ -1149,7 +1146,7 @@ func (r *Resolver) GetControlTool(ctx context.Context, req *mcp.CallToolRequest,
|
||||
}
|
||||
|
||||
func (r *Resolver) AddControlTool(ctx context.Context, req *mcp.CallToolRequest, input *types.AddControlInput) (*mcp.CallToolResult, types.AddControlOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.FrameworkID, authz.ActionCreateControl)
|
||||
r.MustBeAuthorized(ctx, input.FrameworkID, iam.ActionCreateControl)
|
||||
|
||||
svc := r.ProboService(ctx, input.FrameworkID)
|
||||
|
||||
@@ -1174,7 +1171,7 @@ func (r *Resolver) AddControlTool(ctx context.Context, req *mcp.CallToolRequest,
|
||||
}
|
||||
|
||||
func (r *Resolver) UpdateControlTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateControlInput) (*mcp.CallToolResult, types.UpdateControlOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.ID, authz.ActionUpdateControl)
|
||||
r.MustBeAuthorized(ctx, input.ID, iam.ActionUpdateControl)
|
||||
|
||||
svc := r.ProboService(ctx, input.ID)
|
||||
|
||||
@@ -1199,7 +1196,7 @@ func (r *Resolver) UpdateControlTool(ctx context.Context, req *mcp.CallToolReque
|
||||
}
|
||||
|
||||
func (r *Resolver) LinkControlMeasureTool(ctx context.Context, req *mcp.CallToolRequest, input *types.LinkControlMeasureInput) (*mcp.CallToolResult, types.LinkControlMeasureOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.ControlID, authz.ActionCreateControlMeasureMapping)
|
||||
r.MustBeAuthorized(ctx, input.ControlID, iam.ActionCreateControlMeasureMapping)
|
||||
|
||||
svc := r.ProboService(ctx, input.ControlID)
|
||||
|
||||
@@ -1212,7 +1209,7 @@ func (r *Resolver) LinkControlMeasureTool(ctx context.Context, req *mcp.CallTool
|
||||
}
|
||||
|
||||
func (r *Resolver) UnlinkControlMeasureTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UnlinkControlMeasureInput) (*mcp.CallToolResult, types.UnlinkControlMeasureOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.ControlID, authz.ActionDeleteControlMeasureMapping)
|
||||
r.MustBeAuthorized(ctx, input.ControlID, iam.ActionDeleteControlMeasureMapping)
|
||||
|
||||
svc := r.ProboService(ctx, input.ControlID)
|
||||
|
||||
@@ -1225,7 +1222,7 @@ func (r *Resolver) UnlinkControlMeasureTool(ctx context.Context, req *mcp.CallTo
|
||||
}
|
||||
|
||||
func (r *Resolver) LinkControlDocumentTool(ctx context.Context, req *mcp.CallToolRequest, input *types.LinkControlDocumentInput) (*mcp.CallToolResult, types.LinkControlDocumentOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.ControlID, authz.ActionCreateControlDocumentMapping)
|
||||
r.MustBeAuthorized(ctx, input.ControlID, iam.ActionCreateControlDocumentMapping)
|
||||
|
||||
svc := r.ProboService(ctx, input.ControlID)
|
||||
|
||||
@@ -1238,7 +1235,7 @@ func (r *Resolver) LinkControlDocumentTool(ctx context.Context, req *mcp.CallToo
|
||||
}
|
||||
|
||||
func (r *Resolver) UnlinkControlDocumentTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UnlinkControlDocumentInput) (*mcp.CallToolResult, types.UnlinkControlDocumentOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.ControlID, authz.ActionDeleteControlDocumentMapping)
|
||||
r.MustBeAuthorized(ctx, input.ControlID, iam.ActionDeleteControlDocumentMapping)
|
||||
|
||||
svc := r.ProboService(ctx, input.ControlID)
|
||||
|
||||
@@ -1251,7 +1248,7 @@ func (r *Resolver) UnlinkControlDocumentTool(ctx context.Context, req *mcp.CallT
|
||||
}
|
||||
|
||||
func (r *Resolver) LinkControlAuditTool(ctx context.Context, req *mcp.CallToolRequest, input *types.LinkControlAuditInput) (*mcp.CallToolResult, types.LinkControlAuditOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.ControlID, authz.ActionCreateControlAuditMapping)
|
||||
r.MustBeAuthorized(ctx, input.ControlID, iam.ActionCreateControlAuditMapping)
|
||||
|
||||
svc := r.ProboService(ctx, input.ControlID)
|
||||
|
||||
@@ -1264,7 +1261,7 @@ func (r *Resolver) LinkControlAuditTool(ctx context.Context, req *mcp.CallToolRe
|
||||
}
|
||||
|
||||
func (r *Resolver) UnlinkControlAuditTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UnlinkControlAuditInput) (*mcp.CallToolResult, types.UnlinkControlAuditOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.ControlID, authz.ActionDeleteControlAuditMapping)
|
||||
r.MustBeAuthorized(ctx, input.ControlID, iam.ActionDeleteControlAuditMapping)
|
||||
|
||||
svc := r.ProboService(ctx, input.ControlID)
|
||||
|
||||
@@ -1277,7 +1274,7 @@ func (r *Resolver) UnlinkControlAuditTool(ctx context.Context, req *mcp.CallTool
|
||||
}
|
||||
|
||||
func (r *Resolver) LinkControlSnapshotTool(ctx context.Context, req *mcp.CallToolRequest, input *types.LinkControlSnapshotInput) (*mcp.CallToolResult, types.LinkControlSnapshotOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.ControlID, authz.ActionCreateControlSnapshotMapping)
|
||||
r.MustBeAuthorized(ctx, input.ControlID, iam.ActionCreateControlSnapshotMapping)
|
||||
|
||||
svc := r.ProboService(ctx, input.ControlID)
|
||||
|
||||
@@ -1290,7 +1287,7 @@ func (r *Resolver) LinkControlSnapshotTool(ctx context.Context, req *mcp.CallToo
|
||||
}
|
||||
|
||||
func (r *Resolver) UnlinkControlSnapshotTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UnlinkControlSnapshotInput) (*mcp.CallToolResult, types.UnlinkControlSnapshotOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.ControlID, authz.ActionDeleteControlSnapshotMapping)
|
||||
r.MustBeAuthorized(ctx, input.ControlID, iam.ActionDeleteControlSnapshotMapping)
|
||||
|
||||
svc := r.ProboService(ctx, input.ControlID)
|
||||
|
||||
@@ -1303,7 +1300,7 @@ func (r *Resolver) UnlinkControlSnapshotTool(ctx context.Context, req *mcp.CallT
|
||||
}
|
||||
|
||||
func (r *Resolver) ListTasksTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListTasksInput) (*mcp.CallToolResult, types.ListTasksOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, authz.ActionListTasks)
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, iam.ActionListTasks)
|
||||
|
||||
prb := r.ProboService(ctx, input.OrganizationID)
|
||||
|
||||
@@ -1329,7 +1326,7 @@ func (r *Resolver) ListTasksTool(ctx context.Context, req *mcp.CallToolRequest,
|
||||
}
|
||||
|
||||
func (r *Resolver) GetTaskTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetTaskInput) (*mcp.CallToolResult, types.GetTaskOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.ID, authz.ActionGet)
|
||||
r.MustBeAuthorized(ctx, input.ID, iam.ActionGet)
|
||||
|
||||
prb := r.ProboService(ctx, input.ID)
|
||||
|
||||
@@ -1343,7 +1340,7 @@ func (r *Resolver) GetTaskTool(ctx context.Context, req *mcp.CallToolRequest, in
|
||||
}
|
||||
|
||||
func (r *Resolver) AddTaskTool(ctx context.Context, req *mcp.CallToolRequest, input *types.AddTaskInput) (*mcp.CallToolResult, types.AddTaskOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, authz.ActionCreateTask)
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, iam.ActionCreateTask)
|
||||
|
||||
svc := r.ProboService(ctx, input.OrganizationID)
|
||||
|
||||
@@ -1368,7 +1365,7 @@ func (r *Resolver) AddTaskTool(ctx context.Context, req *mcp.CallToolRequest, in
|
||||
}
|
||||
|
||||
func (r *Resolver) UpdateTaskTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateTaskInput) (*mcp.CallToolResult, types.UpdateTaskOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.ID, authz.ActionUpdateTask)
|
||||
r.MustBeAuthorized(ctx, input.ID, iam.ActionUpdateTask)
|
||||
|
||||
svc := r.ProboService(ctx, input.ID)
|
||||
|
||||
@@ -1394,7 +1391,7 @@ func (r *Resolver) UpdateTaskTool(ctx context.Context, req *mcp.CallToolRequest,
|
||||
}
|
||||
|
||||
func (r *Resolver) AssignTaskTool(ctx context.Context, req *mcp.CallToolRequest, input *types.AssignTaskInput) (*mcp.CallToolResult, types.AssignTaskOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.ID, authz.ActionAssignTask)
|
||||
r.MustBeAuthorized(ctx, input.ID, iam.ActionAssignTask)
|
||||
|
||||
svc := r.ProboService(ctx, input.ID)
|
||||
|
||||
@@ -1409,7 +1406,7 @@ func (r *Resolver) AssignTaskTool(ctx context.Context, req *mcp.CallToolRequest,
|
||||
}
|
||||
|
||||
func (r *Resolver) UnassignTaskTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UnassignTaskInput) (*mcp.CallToolResult, types.UnassignTaskOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.ID, authz.ActionUnassignTask)
|
||||
r.MustBeAuthorized(ctx, input.ID, iam.ActionUnassignTask)
|
||||
|
||||
svc := r.ProboService(ctx, input.ID)
|
||||
|
||||
@@ -1423,7 +1420,7 @@ func (r *Resolver) UnassignTaskTool(ctx context.Context, req *mcp.CallToolReques
|
||||
}
|
||||
|
||||
func (r *Resolver) ListSnapshotsTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListSnapshotsInput) (*mcp.CallToolResult, types.ListSnapshotsOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, authz.ActionListSnapshots)
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, iam.ActionListSnapshots)
|
||||
|
||||
prb := r.ProboService(ctx, input.OrganizationID)
|
||||
|
||||
@@ -1449,7 +1446,7 @@ func (r *Resolver) ListSnapshotsTool(ctx context.Context, req *mcp.CallToolReque
|
||||
}
|
||||
|
||||
func (r *Resolver) GetSnapshotTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetSnapshotInput) (*mcp.CallToolResult, types.GetSnapshotOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.ID, authz.ActionGet)
|
||||
r.MustBeAuthorized(ctx, input.ID, iam.ActionGet)
|
||||
|
||||
prb := r.ProboService(ctx, input.ID)
|
||||
|
||||
@@ -1463,7 +1460,7 @@ func (r *Resolver) GetSnapshotTool(ctx context.Context, req *mcp.CallToolRequest
|
||||
}
|
||||
|
||||
func (r *Resolver) TakeSnapshotTool(ctx context.Context, req *mcp.CallToolRequest, input *types.TakeSnapshotInput) (*mcp.CallToolResult, types.TakeSnapshotOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, authz.ActionCreateSnapshot)
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, iam.ActionCreateSnapshot)
|
||||
|
||||
prb := r.ProboService(ctx, input.OrganizationID)
|
||||
|
||||
@@ -1485,7 +1482,7 @@ func (r *Resolver) TakeSnapshotTool(ctx context.Context, req *mcp.CallToolReques
|
||||
}
|
||||
|
||||
func (r *Resolver) ListDocumentsTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListDocumentsInput) (*mcp.CallToolResult, types.ListDocumentsOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, authz.ActionListDocuments)
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, iam.ActionListDocuments)
|
||||
|
||||
prb := r.ProboService(ctx, input.OrganizationID)
|
||||
|
||||
@@ -1521,7 +1518,7 @@ func (r *Resolver) ListDocumentsTool(ctx context.Context, req *mcp.CallToolReque
|
||||
}
|
||||
|
||||
func (r *Resolver) GetDocumentTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetDocumentInput) (*mcp.CallToolResult, types.GetDocumentOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.ID, authz.ActionGet)
|
||||
r.MustBeAuthorized(ctx, input.ID, iam.ActionGet)
|
||||
|
||||
prb := r.ProboService(ctx, input.ID)
|
||||
|
||||
@@ -1536,7 +1533,7 @@ func (r *Resolver) GetDocumentTool(ctx context.Context, req *mcp.CallToolRequest
|
||||
}
|
||||
|
||||
func (r *Resolver) AddDocumentTool(ctx context.Context, req *mcp.CallToolRequest, input *types.AddDocumentInput) (*mcp.CallToolResult, types.AddDocumentOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, authz.ActionCreateDocument)
|
||||
r.MustBeAuthorized(ctx, input.OrganizationID, iam.ActionCreateDocument)
|
||||
|
||||
svc := r.ProboService(ctx, input.OrganizationID)
|
||||
|
||||
@@ -1565,7 +1562,7 @@ func (r *Resolver) AddDocumentTool(ctx context.Context, req *mcp.CallToolRequest
|
||||
}
|
||||
|
||||
func (r *Resolver) UpdateDocumentTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateDocumentInput) (*mcp.CallToolResult, types.UpdateDocumentOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.ID, authz.ActionUpdateDocument)
|
||||
r.MustBeAuthorized(ctx, input.ID, iam.ActionUpdateDocument)
|
||||
|
||||
svc := r.ProboService(ctx, input.ID)
|
||||
|
||||
@@ -1590,7 +1587,7 @@ func (r *Resolver) UpdateDocumentTool(ctx context.Context, req *mcp.CallToolRequ
|
||||
}
|
||||
|
||||
func (r *Resolver) ListDocumentVersionsTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListDocumentVersionsInput) (*mcp.CallToolResult, types.ListDocumentVersionsOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.DocumentID, authz.ActionDocumentVersion)
|
||||
r.MustBeAuthorized(ctx, input.DocumentID, iam.ActionDocumentVersion)
|
||||
|
||||
pageOrderBy := page.OrderBy[coredata.DocumentVersionOrderField]{
|
||||
Field: coredata.DocumentVersionOrderFieldCreatedAt,
|
||||
@@ -1615,7 +1612,7 @@ func (r *Resolver) ListDocumentVersionsTool(ctx context.Context, req *mcp.CallTo
|
||||
}
|
||||
|
||||
func (r *Resolver) GetDocumentVersionTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetDocumentVersionInput) (*mcp.CallToolResult, types.GetDocumentVersionOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.ID, authz.ActionGet)
|
||||
r.MustBeAuthorized(ctx, input.ID, iam.ActionGet)
|
||||
|
||||
svc := r.ProboService(ctx, input.ID)
|
||||
|
||||
@@ -1630,7 +1627,7 @@ func (r *Resolver) GetDocumentVersionTool(ctx context.Context, req *mcp.CallTool
|
||||
}
|
||||
|
||||
func (r *Resolver) CreateDraftDocumentVersionTool(ctx context.Context, req *mcp.CallToolRequest, input *types.CreateDraftDocumentVersionInput) (*mcp.CallToolResult, types.CreateDraftDocumentVersionOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.DocumentID, authz.ActionCreateDraftDocumentVersion)
|
||||
r.MustBeAuthorized(ctx, input.DocumentID, iam.ActionCreateDraftDocumentVersion)
|
||||
|
||||
svc := r.ProboService(ctx, input.DocumentID)
|
||||
|
||||
@@ -1645,7 +1642,7 @@ func (r *Resolver) CreateDraftDocumentVersionTool(ctx context.Context, req *mcp.
|
||||
}
|
||||
|
||||
func (r *Resolver) UpdateDocumentVersionTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateDocumentVersionInput) (*mcp.CallToolResult, types.UpdateDocumentVersionOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.DocumentVersionID, authz.ActionUpdateDocumentVersion)
|
||||
r.MustBeAuthorized(ctx, input.DocumentVersionID, iam.ActionUpdateDocumentVersion)
|
||||
|
||||
svc := r.ProboService(ctx, input.DocumentVersionID)
|
||||
|
||||
@@ -1666,11 +1663,11 @@ func (r *Resolver) UpdateDocumentVersionTool(ctx context.Context, req *mcp.CallT
|
||||
}
|
||||
|
||||
func (r *Resolver) PublishDocumentVersionTool(ctx context.Context, req *mcp.CallToolRequest, input *types.PublishDocumentVersionInput) (*mcp.CallToolResult, types.PublishDocumentVersionOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.DocumentID, authz.ActionPublishDocumentVersion)
|
||||
r.MustBeAuthorized(ctx, input.DocumentID, iam.ActionPublishDocumentVersion)
|
||||
|
||||
svc := r.ProboService(ctx, input.DocumentID)
|
||||
|
||||
user := serverauth.UserFromContext(ctx)
|
||||
user := connect_v1.UserFromContext(ctx)
|
||||
|
||||
document, documentVersion, err := svc.Documents.PublishVersion(ctx, input.DocumentID, user.ID, input.Changelog)
|
||||
if err != nil {
|
||||
@@ -1684,7 +1681,7 @@ func (r *Resolver) PublishDocumentVersionTool(ctx context.Context, req *mcp.Call
|
||||
}
|
||||
|
||||
func (r *Resolver) ListDocumentVersionSignaturesTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListDocumentVersionSignaturesInput) (*mcp.CallToolResult, types.ListDocumentVersionSignaturesOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.DocumentVersionID, authz.ActionDocumentVersion)
|
||||
r.MustBeAuthorized(ctx, input.DocumentVersionID, iam.ActionDocumentVersion)
|
||||
|
||||
prb := r.ProboService(ctx, input.DocumentVersionID)
|
||||
|
||||
@@ -1717,7 +1714,7 @@ func (r *Resolver) ListDocumentVersionSignaturesTool(ctx context.Context, req *m
|
||||
}
|
||||
|
||||
func (r *Resolver) GetDocumentVersionSignatureTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetDocumentVersionSignatureInput) (*mcp.CallToolResult, types.GetDocumentVersionSignatureOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.ID, authz.ActionGet)
|
||||
r.MustBeAuthorized(ctx, input.ID, iam.ActionGet)
|
||||
|
||||
prb := r.ProboService(ctx, input.ID)
|
||||
|
||||
@@ -1732,7 +1729,7 @@ func (r *Resolver) GetDocumentVersionSignatureTool(ctx context.Context, req *mcp
|
||||
}
|
||||
|
||||
func (r *Resolver) RequestDocumentVersionSignatureTool(ctx context.Context, req *mcp.CallToolRequest, input *types.RequestDocumentVersionSignatureInput) (*mcp.CallToolResult, types.RequestDocumentVersionSignatureOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.DocumentVersionID, authz.ActionRequestSignature)
|
||||
r.MustBeAuthorized(ctx, input.DocumentVersionID, iam.ActionRequestSignature)
|
||||
|
||||
svc := r.ProboService(ctx, input.DocumentVersionID)
|
||||
|
||||
@@ -1753,7 +1750,7 @@ func (r *Resolver) RequestDocumentVersionSignatureTool(ctx context.Context, req
|
||||
}
|
||||
|
||||
func (r *Resolver) DeleteDraftDocumentVersionTool(ctx context.Context, req *mcp.CallToolRequest, input *types.DeleteDraftDocumentVersionInput) (*mcp.CallToolResult, types.DeleteDraftDocumentVersionOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.DocumentVersionID, authz.ActionDeleteDraftDocumentVersion)
|
||||
r.MustBeAuthorized(ctx, input.DocumentVersionID, iam.ActionDeleteDraftDocumentVersion)
|
||||
|
||||
svc := r.ProboService(ctx, input.DocumentVersionID)
|
||||
|
||||
@@ -1768,7 +1765,7 @@ func (r *Resolver) DeleteDraftDocumentVersionTool(ctx context.Context, req *mcp.
|
||||
}
|
||||
|
||||
func (r *Resolver) DeleteDocumentTool(ctx context.Context, req *mcp.CallToolRequest, input *types.DeleteDocumentInput) (*mcp.CallToolResult, types.DeleteDocumentOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.DocumentID, authz.ActionDeleteDocument)
|
||||
r.MustBeAuthorized(ctx, input.DocumentID, iam.ActionDeleteDocument)
|
||||
|
||||
svc := r.ProboService(ctx, input.DocumentID)
|
||||
|
||||
@@ -1783,7 +1780,7 @@ func (r *Resolver) DeleteDocumentTool(ctx context.Context, req *mcp.CallToolRequ
|
||||
}
|
||||
|
||||
func (r *Resolver) CancelSignatureRequestTool(ctx context.Context, req *mcp.CallToolRequest, input *types.CancelSignatureRequestInput) (*mcp.CallToolResult, types.CancelSignatureRequestOutput, error) {
|
||||
r.MustBeAuthorized(ctx, input.DocumentVersionSignatureID, authz.ActionCancelSignatureRequest)
|
||||
r.MustBeAuthorized(ctx, input.DocumentVersionSignatureID, iam.ActionCancelSignatureRequest)
|
||||
|
||||
svc := r.ProboService(ctx, input.DocumentVersionSignatureID)
|
||||
|
||||
|
||||
@@ -8,33 +8,27 @@ import (
|
||||
"github.com/modelcontextprotocol/go-sdk/mcp"
|
||||
"go.gearno.de/kit/log"
|
||||
mcpgenmcp "go.probo.inc/mcpgen/mcp"
|
||||
"go.probo.inc/probo/pkg/auth"
|
||||
"go.probo.inc/probo/pkg/authz"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/iam"
|
||||
"go.probo.inc/probo/pkg/probo"
|
||||
connect_v1 "go.probo.inc/probo/pkg/server/api/connect/v1"
|
||||
"go.probo.inc/probo/pkg/server/api/mcp/mcputils"
|
||||
"go.probo.inc/probo/pkg/server/api/mcp/v1/server"
|
||||
serverauth "go.probo.inc/probo/pkg/server/auth"
|
||||
)
|
||||
|
||||
func (r *Resolver) ProboService(ctx context.Context, objectID gid.GID) *probo.TenantService {
|
||||
serverauth.RequireTenantAccess(ctx, objectID.TenantID())
|
||||
return r.proboSvc.WithTenant(objectID.TenantID())
|
||||
}
|
||||
|
||||
func NewMux(logger *log.Logger, proboSvc *probo.Service, authSvc *auth.Service, authzSvc *authz.Service, cfg Config) *chi.Mux {
|
||||
func NewMux(logger *log.Logger, proboSvc *probo.Service, iamSvc *iam.Service) *chi.Mux {
|
||||
logger = logger.Named("mcp.v1")
|
||||
|
||||
logger.Info("initializing MCP server",
|
||||
log.String("version", cfg.Version),
|
||||
log.String("request_timeout", cfg.RequestTimeout.String()),
|
||||
)
|
||||
logger.Info("initializing MCP server")
|
||||
// server.AddReceivingMiddleware(mcputils.LoggingMiddleware(logger))
|
||||
|
||||
resolver := &Resolver{
|
||||
proboSvc: proboSvc,
|
||||
authSvc: authSvc,
|
||||
authzSvc: authzSvc,
|
||||
iamSvc: iamSvc,
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
@@ -57,10 +51,9 @@ func NewMux(logger *log.Logger, proboSvc *probo.Service, authSvc *auth.Service,
|
||||
},
|
||||
)
|
||||
|
||||
authHandler := WithMCPAuth(logger, authSvc, authzSvc, handler)
|
||||
|
||||
r := chi.NewMux()
|
||||
r.Handle("/", authHandler)
|
||||
r.Use(connect_v1.NewAPIKeyMiddleware(iamSvc))
|
||||
r.Handle("/", RequireAPIKeyHandler(logger, handler))
|
||||
|
||||
logger.Info("MCP server initialized successfully")
|
||||
|
||||
|
||||
@@ -18,29 +18,16 @@ package trust_v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/99designs/gqlgen/graphql/handler"
|
||||
"github.com/99designs/gqlgen/graphql/handler/extension"
|
||||
"github.com/99designs/gqlgen/graphql/handler/transport"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"go.gearno.de/kit/log"
|
||||
"go.probo.inc/probo/pkg/auth"
|
||||
"go.probo.inc/probo/pkg/authz"
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/probo"
|
||||
console_v1 "go.probo.inc/probo/pkg/server/api/console/v1"
|
||||
slack_v1 "go.probo.inc/probo/pkg/server/api/slack/v1"
|
||||
"go.probo.inc/probo/pkg/iam"
|
||||
"go.probo.inc/probo/pkg/securecookie"
|
||||
connect_v1 "go.probo.inc/probo/pkg/server/api/connect/v1"
|
||||
"go.probo.inc/probo/pkg/server/api/trust/v1/schema"
|
||||
"go.probo.inc/probo/pkg/server/api/trust/v1/trustauth"
|
||||
"go.probo.inc/probo/pkg/server/gqlutils"
|
||||
"go.probo.inc/probo/pkg/server/session"
|
||||
"go.probo.inc/probo/pkg/slack"
|
||||
"go.probo.inc/probo/pkg/statelesstoken"
|
||||
"go.probo.inc/probo/pkg/trust"
|
||||
)
|
||||
|
||||
@@ -58,242 +45,42 @@ type (
|
||||
}
|
||||
|
||||
Resolver struct {
|
||||
trustCenterSvc *trust.Service
|
||||
authCfg console_v1.AuthConfig
|
||||
trustAuthCfg TrustAuthConfig
|
||||
trust *trust.Service
|
||||
}
|
||||
|
||||
ctxKey struct{ name string }
|
||||
)
|
||||
|
||||
var (
|
||||
sessionContextKey = &ctxKey{name: "session"}
|
||||
userContextKey = &ctxKey{name: "user"}
|
||||
userTenantContextKey = &ctxKey{name: "user_tenants"}
|
||||
tokenAccessContextKey = &ctxKey{name: "token_access"}
|
||||
)
|
||||
|
||||
func SessionFromContext(ctx context.Context) *coredata.Session {
|
||||
session, _ := ctx.Value(sessionContextKey).(*coredata.Session)
|
||||
return session
|
||||
}
|
||||
|
||||
func UserFromContext(ctx context.Context) *coredata.User {
|
||||
user, _ := ctx.Value(userContextKey).(*coredata.User)
|
||||
return user
|
||||
}
|
||||
|
||||
func TokenAccessFromContext(ctx context.Context) *trustauth.TokenAccessData {
|
||||
tokenAccess, _ := ctx.Value(tokenAccessContextKey).(*trustauth.TokenAccessData)
|
||||
return tokenAccess
|
||||
}
|
||||
|
||||
// UserFromContext implements trustauth.ContextAccessor interface
|
||||
func (r *Resolver) UserFromContext(ctx context.Context) *coredata.User {
|
||||
return UserFromContext(ctx)
|
||||
}
|
||||
|
||||
// TokenAccessFromContext implements trustauth.ContextAccessor interface
|
||||
func (r *Resolver) TokenAccessFromContext(ctx context.Context) *trustauth.TokenAccessData {
|
||||
return TokenAccessFromContext(ctx)
|
||||
}
|
||||
|
||||
func NewMux(
|
||||
logger *log.Logger,
|
||||
authSvc *auth.Service,
|
||||
authzSvc *authz.Service,
|
||||
iamSvc *iam.Service,
|
||||
trustSvc *trust.Service,
|
||||
authCfg console_v1.AuthConfig,
|
||||
trustAuthCfg TrustAuthConfig,
|
||||
|
||||
// TODO: Remove this after successful migration to /slack/v1.
|
||||
slackSvc *slack.Service,
|
||||
cookieConfig securecookie.Config,
|
||||
) *chi.Mux {
|
||||
r := chi.NewMux()
|
||||
|
||||
r.Handle("/graphql", graphqlHandler(logger, authSvc, authzSvc, trustSvc, authCfg, trustAuthCfg))
|
||||
sessionMiddleware := connect_v1.NewSessionMiddleware(iamSvc, cookieConfig)
|
||||
r.Use(sessionMiddleware)
|
||||
|
||||
r.Post("/auth/authenticate", authTokenHandler(trustSvc, trustAuthCfg))
|
||||
r.Delete("/auth/logout", trustCenterLogoutHandler(authCfg, trustAuthCfg))
|
||||
config := schema.Config{Resolvers: &Resolver{trust: trustSvc}}
|
||||
es := schema.NewExecutableSchema(config)
|
||||
graphqlHandler := gqlutils.NewHandler(es, logger)
|
||||
|
||||
// Backward compatibility: support old /trust/v1/slack endpoint
|
||||
// TODO: Remove this after successful migration to /slack/v1 and then make SlackHandler PRIVATE in slack_v1 package.
|
||||
r.Post("/slack", slack_v1.SlackHandler(slackSvc, slackSvc.GetSlackSigningSecret(), logger, trustSvc))
|
||||
r.Handle("/graphql", graphqlHandler)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func graphqlHandler(logger *log.Logger, authSvc *auth.Service, authzSvc *authz.Service, trustSvc *trust.Service, authCfg console_v1.AuthConfig, trustAuthCfg TrustAuthConfig) http.HandlerFunc {
|
||||
resolver := &Resolver{
|
||||
trustCenterSvc: trustSvc,
|
||||
authCfg: authCfg,
|
||||
trustAuthCfg: trustAuthCfg,
|
||||
}
|
||||
|
||||
c := schema.Config{
|
||||
Resolvers: resolver,
|
||||
}
|
||||
|
||||
c.Directives.MustBeAuthenticated = trustauth.MustBeAuthenticatedDirective(resolver)
|
||||
|
||||
es := schema.NewExecutableSchema(c)
|
||||
|
||||
srv := handler.New(es)
|
||||
|
||||
srv.AddTransport(transport.POST{})
|
||||
srv.AddTransport(transport.GET{})
|
||||
srv.AddTransport(transport.Options{})
|
||||
|
||||
srv.Use(extension.Introspection{})
|
||||
srv.Use(gqlutils.NewTracingExtension(logger))
|
||||
|
||||
srv.SetRecoverFunc(gqlutils.RecoverFunc)
|
||||
|
||||
return WithSession(authSvc, authzSvc, trustSvc, authCfg, trustAuthCfg, srv.ServeHTTP)
|
||||
}
|
||||
|
||||
func (r *Resolver) RootTrustService(ctx context.Context) *trust.TenantService {
|
||||
return r.trustCenterSvc.WithTenant(gid.NewTenantID())
|
||||
return r.trust.WithTenant(gid.NewTenantID())
|
||||
}
|
||||
|
||||
func (r *Resolver) PublicTrustService(ctx context.Context, tenantID gid.TenantID) *trust.TenantService {
|
||||
return r.trustCenterSvc.WithTenant(tenantID)
|
||||
return r.trust.WithTenant(tenantID)
|
||||
}
|
||||
|
||||
func (r *Resolver) PrivateTrustService(ctx context.Context, tenantID gid.TenantID) (*trust.TenantService, error) {
|
||||
if err := trustauth.ValidateTenantAccess(ctx, r, userTenantContextKey, tenantID); err != nil {
|
||||
return nil, fmt.Errorf("cannot access trust center: %w", err)
|
||||
}
|
||||
// if err := trustauth.ValidateTenantAccess(ctx, r, userTenantContextKey, tenantID); err != nil {
|
||||
// return nil, fmt.Errorf("cannot access trust center: %w", err)
|
||||
// }
|
||||
|
||||
return r.trustCenterSvc.WithTenant(tenantID), nil
|
||||
}
|
||||
|
||||
func WithSession(authSvc *auth.Service, authzSvc *authz.Service, trustSvc *trust.Service, authCfg console_v1.AuthConfig, trustAuthCfg TrustAuthConfig, next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
ip := extractIPAddress(r)
|
||||
ctx = context.WithValue(ctx, coredata.ContextKeyIPAddress, ip)
|
||||
|
||||
if authCtx := tryTokenAuth(ctx, w, r, trustSvc, trustAuthCfg); authCtx != nil {
|
||||
next(w, r.WithContext(authCtx))
|
||||
return
|
||||
}
|
||||
|
||||
if authCtx := trySessionAuth(ctx, w, r, authSvc, authzSvc, authCfg); authCtx != nil {
|
||||
next(w, r.WithContext(authCtx))
|
||||
updateSessionIfNeeded(authCtx, authSvc)
|
||||
return
|
||||
}
|
||||
|
||||
next(w, r.WithContext(ctx))
|
||||
}
|
||||
}
|
||||
|
||||
func trySessionAuth(ctx context.Context, w http.ResponseWriter, r *http.Request, authSvc *auth.Service, authzSvc *authz.Service, authCfg console_v1.AuthConfig) context.Context {
|
||||
sessionAuthCfg := session.AuthConfig{
|
||||
CookieName: authCfg.CookieName,
|
||||
CookieSecret: authCfg.CookieSecret,
|
||||
CookieSecure: authCfg.CookieSecure,
|
||||
}
|
||||
|
||||
errorHandler := session.ErrorHandler{
|
||||
OnParseError: func(w http.ResponseWriter, authCfg session.AuthConfig) {
|
||||
session.ClearCookie(w, authCfg)
|
||||
},
|
||||
OnSessionError: func(w http.ResponseWriter, authCfg session.AuthConfig) {
|
||||
session.ClearCookie(w, authCfg)
|
||||
},
|
||||
OnUserError: func(w http.ResponseWriter, authCfg session.AuthConfig) {
|
||||
session.ClearCookie(w, authCfg)
|
||||
},
|
||||
OnTenantError: func(err error) {
|
||||
session.ClearCookie(w, sessionAuthCfg)
|
||||
},
|
||||
}
|
||||
|
||||
authResult := session.TryAuth(ctx, w, r, authSvc, authzSvc, sessionAuthCfg, errorHandler)
|
||||
if authResult == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, sessionContextKey, authResult.Session)
|
||||
ctx = context.WithValue(ctx, userContextKey, authResult.User)
|
||||
ctx = context.WithValue(ctx, userTenantContextKey, &authResult.TenantIDs)
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
func tryTokenAuth(ctx context.Context, w http.ResponseWriter, r *http.Request, trustSvc *trust.Service, trustAuthCfg TrustAuthConfig) context.Context {
|
||||
cookie, err := r.Cookie(trustAuthCfg.CookieName)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
basicPayload, err := statelesstoken.ValidateToken[probo.TrustCenterAccessData](
|
||||
trustAuthCfg.TokenSecret,
|
||||
trustAuthCfg.TokenType,
|
||||
cookie.Value,
|
||||
)
|
||||
if err != nil {
|
||||
clearTokenCookie(w, trustAuthCfg)
|
||||
return nil
|
||||
}
|
||||
|
||||
tenantID := basicPayload.Data.TrustCenterID.TenantID()
|
||||
|
||||
tenantSvc := trustSvc.WithTenant(tenantID)
|
||||
if err := tenantSvc.TrustCenterAccesses.ValidateToken(ctx, basicPayload.Data.TrustCenterID, basicPayload.Data.Email); err != nil {
|
||||
clearTokenCookie(w, trustAuthCfg)
|
||||
return nil
|
||||
}
|
||||
|
||||
tokenAccess := &trustauth.TokenAccessData{
|
||||
TrustCenterID: basicPayload.Data.TrustCenterID,
|
||||
Email: basicPayload.Data.Email,
|
||||
TenantID: tenantID,
|
||||
Scope: trustAuthCfg.Scope,
|
||||
}
|
||||
|
||||
return context.WithValue(ctx, tokenAccessContextKey, tokenAccess)
|
||||
}
|
||||
|
||||
func clearTokenCookie(w http.ResponseWriter, trustAuthCfg TrustAuthConfig) {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: trustAuthCfg.CookieName,
|
||||
Value: "",
|
||||
Domain: trustAuthCfg.CookieDomain,
|
||||
Path: "/",
|
||||
MaxAge: -1,
|
||||
Secure: trustAuthCfg.CookieSecure,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
})
|
||||
}
|
||||
|
||||
func updateSessionIfNeeded(ctx context.Context, authSvc *auth.Service) {
|
||||
session := SessionFromContext(ctx)
|
||||
if session != nil {
|
||||
if _, err := authSvc.UpdateSession(ctx, session.ID); err != nil {
|
||||
panic(fmt.Errorf("cannot update session: %w", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func extractIPAddress(r *http.Request) string {
|
||||
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
|
||||
if ip := strings.Split(xff, ",")[0]; ip != "" {
|
||||
return strings.TrimSpace(ip)
|
||||
}
|
||||
}
|
||||
|
||||
if xri := r.Header.Get("X-Real-IP"); xri != "" {
|
||||
return strings.TrimSpace(xri)
|
||||
}
|
||||
|
||||
if ip := strings.Split(r.RemoteAddr, ":")[0]; ip != "" {
|
||||
return ip
|
||||
}
|
||||
|
||||
return "unknown"
|
||||
return r.trust.WithTenant(tenantID), nil
|
||||
}
|
||||
|
||||
@@ -24,22 +24,18 @@ import (
|
||||
"go.gearno.de/kit/httpserver"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/probo"
|
||||
console_v1 "go.probo.inc/probo/pkg/server/api/console/v1"
|
||||
"go.probo.inc/probo/pkg/server/session"
|
||||
"go.probo.inc/probo/pkg/statelesstoken"
|
||||
"go.probo.inc/probo/pkg/trust"
|
||||
)
|
||||
|
||||
type ctxKey struct {
|
||||
name string
|
||||
}
|
||||
|
||||
var (
|
||||
CustomDomainTenantIDKey = &ctxKey{name: "custom_domain_tenant_id"}
|
||||
CustomDomainOrganizationIDKey = &ctxKey{name: "custom_domain_organization_id"}
|
||||
)
|
||||
|
||||
func GetCustomDomainTenantID(ctx context.Context) (gid.TenantID, bool) {
|
||||
tenantID, ok := ctx.Value(CustomDomainTenantIDKey).(gid.TenantID)
|
||||
return tenantID, ok
|
||||
}
|
||||
|
||||
func GetCustomDomainOrganizationID(ctx context.Context) (gid.GID, bool) {
|
||||
organizationID, ok := ctx.Value(CustomDomainOrganizationIDKey).(gid.GID)
|
||||
return organizationID, ok
|
||||
@@ -141,37 +137,3 @@ func validateTrustCenterAccessToken(ctx context.Context, trustSvc *trust.Service
|
||||
|
||||
return &token.Data, nil
|
||||
}
|
||||
|
||||
func trustCenterLogoutHandler(authCfg console_v1.AuthConfig, trustAuthCfg TrustAuthConfig) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
// Determine cookie domain: use custom domain if present, otherwise use configured domain
|
||||
cookieDomain := trustAuthCfg.CookieDomain
|
||||
if _, ok := GetCustomDomainOrganizationID(r.Context()); ok {
|
||||
// On custom domain, use the request host
|
||||
if r.TLS != nil && r.TLS.ServerName != "" {
|
||||
cookieDomain = r.TLS.ServerName
|
||||
}
|
||||
}
|
||||
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: trustAuthCfg.CookieName,
|
||||
Value: "",
|
||||
Domain: cookieDomain,
|
||||
Path: "/",
|
||||
MaxAge: -1,
|
||||
Secure: trustAuthCfg.CookieSecure,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
})
|
||||
|
||||
session.ClearCookie(w, session.AuthConfig{
|
||||
CookieName: authCfg.CookieName,
|
||||
CookieSecret: authCfg.CookieSecret,
|
||||
CookieSecure: authCfg.CookieSecure,
|
||||
})
|
||||
|
||||
httpserver.RenderJSON(w, http.StatusOK, map[string]string{
|
||||
"message": "Logged out successfully",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,94 +0,0 @@
|
||||
// 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 auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"go.gearno.de/kit/httpserver"
|
||||
authsvc "go.probo.inc/probo/pkg/auth"
|
||||
"go.probo.inc/probo/pkg/authz"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/server/session"
|
||||
)
|
||||
|
||||
type (
|
||||
AcceptInvitationRequest struct {
|
||||
InvitationID gid.GID `json:"invitationId"`
|
||||
}
|
||||
|
||||
AcceptInvitationResponse struct {
|
||||
InvitationID gid.GID `json:"invitationId"`
|
||||
}
|
||||
)
|
||||
|
||||
func AcceptInvitationHandler(authSvc *authsvc.Service, authzSvc *authz.Service, cookieName string, cookieSecret string, cookieSecure bool) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
sessionAuthCfg := session.AuthConfig{
|
||||
CookieName: cookieName,
|
||||
CookieSecret: cookieSecret,
|
||||
CookieSecure: cookieSecure,
|
||||
}
|
||||
|
||||
errorHandler := session.ErrorHandler{
|
||||
OnCookieError: func(err error) {
|
||||
httpserver.RenderError(w, http.StatusUnauthorized, fmt.Errorf("invalid session"))
|
||||
},
|
||||
OnParseError: func(w http.ResponseWriter, authCfg session.AuthConfig) {
|
||||
session.ClearCookie(w, authCfg)
|
||||
httpserver.RenderError(w, http.StatusUnauthorized, fmt.Errorf("invalid session"))
|
||||
},
|
||||
OnSessionError: func(w http.ResponseWriter, authCfg session.AuthConfig) {
|
||||
session.ClearCookie(w, authCfg)
|
||||
httpserver.RenderError(w, http.StatusUnauthorized, fmt.Errorf("session expired"))
|
||||
},
|
||||
OnUserError: func(w http.ResponseWriter, authCfg session.AuthConfig) {
|
||||
session.ClearCookie(w, authCfg)
|
||||
httpserver.RenderError(w, http.StatusUnauthorized, fmt.Errorf("user not found"))
|
||||
},
|
||||
OnTenantError: func(err error) {
|
||||
panic(fmt.Errorf("cannot list tenants for user: %w", err))
|
||||
},
|
||||
}
|
||||
|
||||
authResult := session.TryAuth(ctx, w, r, authSvc, authzSvc, sessionAuthCfg, errorHandler)
|
||||
if authResult == nil {
|
||||
httpserver.RenderError(w, http.StatusUnauthorized, fmt.Errorf("authentication required"))
|
||||
return
|
||||
}
|
||||
|
||||
// Parse request body
|
||||
var req AcceptInvitationRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("invalid request body"))
|
||||
return
|
||||
}
|
||||
|
||||
// Accept the invitation
|
||||
_, err := authzSvc.AcceptInvitationByID(ctx, req.InvitationID, authResult.User.ID)
|
||||
if err != nil {
|
||||
httpserver.RenderError(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
response := AcceptInvitationResponse(req)
|
||||
|
||||
httpserver.RenderJSON(w, http.StatusOK, response)
|
||||
}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
// 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 auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"go.probo.inc/probo/pkg/auth"
|
||||
"go.probo.inc/probo/pkg/authz"
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
)
|
||||
|
||||
var (
|
||||
// UserContextKey is the context key for the authenticated user
|
||||
UserContextKey = &ctxKey{name: "user"}
|
||||
// UserTenantContextKey is the context key for tenant access information
|
||||
UserTenantContextKey = &ctxKey{name: "user_tenants"}
|
||||
// UserAPIKeyContextKey is the context key for the API key used for authentication
|
||||
UserAPIKeyContextKey = &ctxKey{name: "user_api_key"}
|
||||
)
|
||||
|
||||
type UserTenantAccess struct {
|
||||
TenantIDs []gid.TenantID
|
||||
AuthErrors map[gid.TenantID]error
|
||||
}
|
||||
|
||||
// UserFromContext extracts the authenticated user from the context.
|
||||
func UserFromContext(ctx context.Context) *coredata.User {
|
||||
user, _ := ctx.Value(UserContextKey).(*coredata.User)
|
||||
return user
|
||||
}
|
||||
|
||||
// UserAPIKeyFromContext extracts the API key from the context.
|
||||
func UserAPIKeyFromContext(ctx context.Context) *coredata.UserAPIKey {
|
||||
userAPIKey, _ := ctx.Value(UserAPIKeyContextKey).(*coredata.UserAPIKey)
|
||||
return userAPIKey
|
||||
}
|
||||
|
||||
// UserTenantAccessFromContext extracts the tenant access information from the context.
|
||||
func UserTenantAccessFromContext(ctx context.Context) *UserTenantAccess {
|
||||
access, _ := ctx.Value(UserTenantContextKey).(*UserTenantAccess)
|
||||
return access
|
||||
}
|
||||
|
||||
// AuthenticateWithAPIKey attempts to authenticate using an API key from the Authorization header.
|
||||
// It returns a context with authentication information if successful, or nil if no API key
|
||||
// was provided or authentication failed. This function does not return errors - it silently
|
||||
// fails to allow fallback to other authentication methods.
|
||||
func AuthenticateWithAPIKey(ctx context.Context, r *http.Request, authSvc *auth.Service, authzSvc *authz.Service) context.Context {
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(authHeader, "Bearer ") {
|
||||
return nil
|
||||
}
|
||||
|
||||
apiKeyString := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
|
||||
user, userAPIKey, err := authSvc.ValidateUserAPIKey(ctx, apiKeyString)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
organizations, err := authzSvc.GetAllOrganizationsForUserAPIKeyId(ctx, userAPIKey.ID)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
tenantIDs := make([]gid.TenantID, 0, len(organizations))
|
||||
for _, org := range organizations {
|
||||
tenantIDs = append(tenantIDs, org.ID.TenantID())
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, UserContextKey, user)
|
||||
ctx = context.WithValue(ctx, UserAPIKeyContextKey, userAPIKey)
|
||||
ctx = context.WithValue(ctx, UserTenantContextKey, &UserTenantAccess{
|
||||
TenantIDs: tenantIDs,
|
||||
AuthErrors: make(map[gid.TenantID]error),
|
||||
})
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
// RequireTenantAccess ensures that the authenticated user has access to the specified tenant.
|
||||
// It panics with an authz.TenantAccessError if access is denied.
|
||||
func RequireTenantAccess(ctx context.Context, tenantID gid.TenantID) {
|
||||
access := UserTenantAccessFromContext(ctx)
|
||||
|
||||
if access == nil {
|
||||
panic(&authz.TenantAccessError{Message: "tenant not found"})
|
||||
}
|
||||
|
||||
if !slices.Contains(access.TenantIDs, tenantID) {
|
||||
if access.AuthErrors != nil {
|
||||
if authErr := access.AuthErrors[tenantID]; authErr != nil {
|
||||
panic(authErr)
|
||||
}
|
||||
}
|
||||
|
||||
panic(&authz.TenantAccessError{Message: "tenant not found"})
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
// 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 auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"go.gearno.de/kit/log"
|
||||
authsvc "go.probo.inc/probo/pkg/auth"
|
||||
"go.probo.inc/probo/pkg/authz"
|
||||
"go.probo.inc/probo/pkg/filemanager"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Auth *authsvc.Service
|
||||
Authz *authz.Service
|
||||
SAML *authsvc.SAMLService
|
||||
CookieName string
|
||||
CookieDomain string
|
||||
SessionDuration time.Duration
|
||||
CookieSecret string
|
||||
CookieSecure bool
|
||||
FileManager *filemanager.Service
|
||||
Logger *log.Logger
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
router *chi.Mux
|
||||
}
|
||||
|
||||
func NewServer(cfg Config) (*Server, error) {
|
||||
router := chi.NewRouter()
|
||||
|
||||
router.Post("/register", SignUpHandler(cfg.Auth, cfg.CookieName, cfg.CookieSecret, cfg.CookieSecure))
|
||||
router.Post("/login", SignInHandler(cfg.Auth, cfg.CookieName, cfg.CookieSecret, cfg.CookieSecure))
|
||||
router.Delete("/logout", SignOutHandler(cfg.Auth, cfg.CookieName, cfg.CookieSecret, cfg.CookieSecure))
|
||||
router.Post("/signup-from-invitation", SignupFromInvitationHandler(cfg.Auth, cfg.CookieName, cfg.CookieSecret, cfg.CookieSecure))
|
||||
router.Post("/forget-password", ForgetPasswordHandler(cfg.Auth))
|
||||
router.Post("/reset-password", ResetPasswordHandler(cfg.Auth))
|
||||
router.Post("/check-sso", SAMLCheckSSOHandler(cfg.Auth, cfg.Logger))
|
||||
router.Get("/organizations", RequireAuth(cfg.Auth, cfg.Authz, cfg.CookieName, cfg.CookieSecret, cfg.CookieSecure, ListOrganizationsHandler(cfg.Auth, cfg.Authz)))
|
||||
router.Get("/organizations/{organizationID}/logo", RequireAuth(cfg.Auth, cfg.Authz, cfg.CookieName, cfg.CookieSecret, cfg.CookieSecure, OrganizationLogoHandler(cfg.Auth, cfg.FileManager)))
|
||||
router.Get("/invitations", RequireAuth(cfg.Auth, cfg.Authz, cfg.CookieName, cfg.CookieSecret, cfg.CookieSecure, ListInvitationsHandler(cfg.Authz)))
|
||||
router.Post("/invitations/accept", AcceptInvitationHandler(cfg.Auth, cfg.Authz, cfg.CookieName, cfg.CookieSecret, cfg.CookieSecure))
|
||||
|
||||
router.Get("/api-keys", RequireAuth(cfg.Auth, cfg.Authz, cfg.CookieName, cfg.CookieSecret, cfg.CookieSecure, ListUserAPIKeysHandler(cfg.Auth, cfg.Authz)))
|
||||
router.Post("/api-keys", RequireAuth(cfg.Auth, cfg.Authz, cfg.CookieName, cfg.CookieSecret, cfg.CookieSecure, CreateUserAPIKeyHandler(cfg.Auth, cfg.Authz)))
|
||||
router.Get("/api-keys/{id}", RequireAuth(cfg.Auth, cfg.Authz, cfg.CookieName, cfg.CookieSecret, cfg.CookieSecure, GetUserAPIKeyHandler(cfg.Auth)))
|
||||
router.Put("/api-keys", RequireAuth(cfg.Auth, cfg.Authz, cfg.CookieName, cfg.CookieSecret, cfg.CookieSecure, UpdateUserAPIKeyHandler(cfg.Auth, cfg.Authz)))
|
||||
router.Delete("/api-keys", RequireAuth(cfg.Auth, cfg.Authz, cfg.CookieName, cfg.CookieSecret, cfg.CookieSecure, DeleteUserAPIKeyHandler(cfg.Auth)))
|
||||
|
||||
router.Get("/saml/login/{samlConfigID}", SAMLLoginHandler(cfg.SAML, cfg.Auth, cfg.Logger))
|
||||
router.Post("/saml/consume", SAMLACSHandler(cfg.SAML, cfg.Auth, cfg.Authz, cfg.CookieName, cfg.CookieSecret, cfg.CookieSecure, cfg.SessionDuration, cfg.Logger))
|
||||
router.Get("/saml/metadata", SAMLMetadataHandler(cfg.SAML))
|
||||
|
||||
return &Server{
|
||||
router: router,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
s.router.ServeHTTP(w, r)
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
// 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 auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"go.gearno.de/kit/httpserver"
|
||||
authsvc "go.probo.inc/probo/pkg/auth"
|
||||
"go.probo.inc/probo/pkg/authz"
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/server/session"
|
||||
)
|
||||
|
||||
type ctxKey struct{ name string }
|
||||
|
||||
var (
|
||||
sessionContextKey = &ctxKey{name: "session"}
|
||||
)
|
||||
|
||||
func RequireAuth(
|
||||
authSvc *authsvc.Service,
|
||||
authzSvc *authz.Service,
|
||||
cookieName string,
|
||||
cookieSecret string,
|
||||
cookieSecure bool,
|
||||
next http.HandlerFunc,
|
||||
) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
sessionAuthCfg := session.AuthConfig{
|
||||
CookieName: cookieName,
|
||||
CookieSecret: cookieSecret,
|
||||
CookieSecure: cookieSecure,
|
||||
}
|
||||
|
||||
errorHandler := session.ErrorHandler{
|
||||
OnCookieError: func(err error) {
|
||||
httpserver.RenderError(w, http.StatusUnauthorized, fmt.Errorf("invalid session"))
|
||||
},
|
||||
OnParseError: func(w http.ResponseWriter, authCfg session.AuthConfig) {
|
||||
session.ClearCookie(w, authCfg)
|
||||
httpserver.RenderError(w, http.StatusUnauthorized, fmt.Errorf("invalid session"))
|
||||
},
|
||||
OnSessionError: func(w http.ResponseWriter, authCfg session.AuthConfig) {
|
||||
session.ClearCookie(w, authCfg)
|
||||
httpserver.RenderError(w, http.StatusUnauthorized, fmt.Errorf("session expired"))
|
||||
},
|
||||
OnUserError: func(w http.ResponseWriter, authCfg session.AuthConfig) {
|
||||
session.ClearCookie(w, authCfg)
|
||||
httpserver.RenderError(w, http.StatusUnauthorized, fmt.Errorf("user not found"))
|
||||
},
|
||||
OnTenantError: func(err error) {
|
||||
panic(fmt.Errorf("cannot list tenants for user: %w", err))
|
||||
},
|
||||
}
|
||||
|
||||
authResult := session.TryAuth(ctx, w, r, authSvc, authzSvc, sessionAuthCfg, errorHandler)
|
||||
if authResult == nil {
|
||||
httpserver.RenderError(w, http.StatusUnauthorized, fmt.Errorf("authentication required"))
|
||||
return
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, sessionContextKey, authResult.Session)
|
||||
ctx = context.WithValue(ctx, UserContextKey, authResult.User)
|
||||
|
||||
next(w, r.WithContext(ctx))
|
||||
}
|
||||
}
|
||||
|
||||
func SessionFromContext(ctx context.Context) *coredata.Session {
|
||||
session, _ := ctx.Value(sessionContextKey).(*coredata.Session)
|
||||
return session
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
// 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 auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"go.gearno.de/kit/httpserver"
|
||||
authsvc "go.probo.inc/probo/pkg/auth"
|
||||
"go.probo.inc/probo/pkg/authz"
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
)
|
||||
|
||||
type (
|
||||
CreateUserAPIKeyRequest struct {
|
||||
Name string `json:"name"`
|
||||
ExpiresAt time.Time `json:"expiresAt"`
|
||||
Organizations []UserAPIKeyOrganizationMembershipRequest `json:"organizations"`
|
||||
}
|
||||
|
||||
UserAPIKeyOrganizationMembershipRequest struct {
|
||||
OrganizationID string `json:"organizationId"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
|
||||
CreateUserAPIKeyResponse struct {
|
||||
UserAPIKey UserAPIKeyResponse `json:"apiKey"`
|
||||
Key string `json:"key"`
|
||||
}
|
||||
)
|
||||
|
||||
func CreateUserAPIKeyHandler(authSvc *authsvc.Service, authzSvc *authz.Service) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
user := UserFromContext(ctx)
|
||||
|
||||
var req CreateUserAPIKeyRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httpserver.RenderJSON(w, http.StatusBadRequest, map[string]string{
|
||||
"error": "invalid request body",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if req.ExpiresAt.IsZero() {
|
||||
httpserver.RenderJSON(w, http.StatusBadRequest, map[string]string{
|
||||
"error": "expiresAt is required",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if req.ExpiresAt.Before(time.Now()) {
|
||||
httpserver.RenderJSON(w, http.StatusBadRequest, map[string]string{
|
||||
"error": "expiration date must be in the future",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.Organizations) == 0 {
|
||||
httpserver.RenderJSON(w, http.StatusBadRequest, map[string]string{
|
||||
"error": "at least one organization is required",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
name := req.Name
|
||||
if name == "" {
|
||||
name = time.Now().Format("2006-01-02")
|
||||
}
|
||||
|
||||
orgInputs := make([]authsvc.UserAPIKeyOrganizationRequest, len(req.Organizations))
|
||||
for i, org := range req.Organizations {
|
||||
orgID, err := gid.ParseGID(org.OrganizationID)
|
||||
if err != nil {
|
||||
httpserver.RenderJSON(w, http.StatusBadRequest, map[string]string{
|
||||
"error": "invalid organization id",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user is an OWNER for this organization
|
||||
tenantAuthzSvc := authzSvc.WithTenant(orgID.TenantID())
|
||||
role, err := tenantAuthzSvc.GetUserRoleInOrganization(ctx, user.ID, orgID)
|
||||
if err != nil {
|
||||
httpserver.RenderJSON(w, http.StatusForbidden, map[string]string{
|
||||
"error": "user does not have access to this organization",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if role != coredata.MembershipRoleOwner {
|
||||
httpserver.RenderJSON(w, http.StatusForbidden, map[string]string{
|
||||
"error": "only owners can create API keys for this organization",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
orgInputs[i] = authsvc.UserAPIKeyOrganizationRequest{
|
||||
OrganizationID: orgID,
|
||||
Role: coredata.APIRole(org.Role),
|
||||
}
|
||||
}
|
||||
|
||||
memberships, err := authSvc.ValidateAndBuildUserAPIKeyMemberships(ctx, user.ID, orgInputs)
|
||||
if err != nil {
|
||||
httpserver.RenderJSON(w, http.StatusBadRequest, map[string]string{
|
||||
"error": "invalid organizations",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
userAPIKey, key, err := authSvc.CreateUserAPIKey(ctx, user.ID, name, req.ExpiresAt, memberships)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot create user api key: %w", err))
|
||||
}
|
||||
|
||||
response := CreateUserAPIKeyResponse{
|
||||
UserAPIKey: UserAPIKeyResponse{
|
||||
ID: userAPIKey.ID,
|
||||
Name: userAPIKey.Name,
|
||||
ExpiresAt: userAPIKey.ExpiresAt,
|
||||
CreatedAt: userAPIKey.CreatedAt,
|
||||
},
|
||||
Key: key,
|
||||
}
|
||||
|
||||
httpserver.RenderJSON(w, http.StatusCreated, response)
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
// 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 auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"go.gearno.de/kit/httpserver"
|
||||
authsvc "go.probo.inc/probo/pkg/auth"
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
)
|
||||
|
||||
type DeleteUserAPIKeyRequest struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
type DeleteUserAPIKeyResponse struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
func DeleteUserAPIKeyHandler(authSvc *authsvc.Service) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
user := UserFromContext(ctx)
|
||||
|
||||
var req DeleteUserAPIKeyRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httpserver.RenderJSON(w, http.StatusBadRequest, map[string]string{
|
||||
"error": "invalid request body",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if req.ID == "" {
|
||||
httpserver.RenderJSON(w, http.StatusBadRequest, map[string]string{
|
||||
"error": "user api key id is required",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
userAPIKeyID, err := gid.ParseGID(req.ID)
|
||||
if err != nil {
|
||||
httpserver.RenderJSON(w, http.StatusBadRequest, map[string]string{
|
||||
"error": "invalid user api key id",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if err := authSvc.DeleteUserAPIKey(ctx, userAPIKeyID, user.ID); err != nil {
|
||||
var errNotFound *coredata.ErrUserAPIKeyNotFound
|
||||
if errors.As(err, &errNotFound) {
|
||||
httpserver.RenderJSON(w, http.StatusNotFound, map[string]string{
|
||||
"error": "user api key not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
httpserver.RenderJSON(w, http.StatusInternalServerError, map[string]string{
|
||||
"error": "failed to delete user api key",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
response := DeleteUserAPIKeyResponse(req)
|
||||
|
||||
httpserver.RenderJSON(w, http.StatusOK, response)
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
// 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 auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"go.gearno.de/kit/httpserver"
|
||||
authsvc "go.probo.inc/probo/pkg/auth"
|
||||
"go.probo.inc/probo/pkg/mail"
|
||||
)
|
||||
|
||||
type (
|
||||
ForgetPasswordRequest struct {
|
||||
Email mail.Addr `json:"email"`
|
||||
}
|
||||
|
||||
ForgetPasswordResponse struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
)
|
||||
|
||||
func ForgetPasswordHandler(authSvc *authsvc.Service) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req ForgetPasswordRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("cannot decode body: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
err := authSvc.ForgetPassword(r.Context(), req.Email)
|
||||
if err != nil {
|
||||
// For security reasons, we don't expose whether an email exists or not
|
||||
httpserver.RenderError(w, http.StatusInternalServerError, fmt.Errorf("cannot process request: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
httpserver.RenderJSON(w, http.StatusOK, ForgetPasswordResponse{
|
||||
Success: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
// 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 auth
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"go.gearno.de/kit/httpserver"
|
||||
authsvc "go.probo.inc/probo/pkg/auth"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
)
|
||||
|
||||
type GetUserAPIKeyResponse struct {
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
func GetUserAPIKeyHandler(authSvc *authsvc.Service) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
user := UserFromContext(ctx)
|
||||
|
||||
userAPIKeyIDStr := chi.URLParam(r, "id")
|
||||
if userAPIKeyIDStr == "" {
|
||||
httpserver.RenderJSON(w, http.StatusBadRequest, map[string]string{
|
||||
"error": "user api key id is required",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
userAPIKeyID, err := gid.ParseGID(userAPIKeyIDStr)
|
||||
if err != nil {
|
||||
httpserver.RenderJSON(w, http.StatusBadRequest, map[string]string{
|
||||
"error": "invalid user api key id",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
key, err := authSvc.GetUserAPIKey(ctx, userAPIKeyID, user.ID)
|
||||
if err != nil {
|
||||
httpserver.RenderJSON(w, http.StatusNotFound, map[string]string{
|
||||
"error": "user api key not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
response := GetUserAPIKeyResponse{
|
||||
Key: key,
|
||||
}
|
||||
|
||||
httpserver.RenderJSON(w, http.StatusOK, response)
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
// 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 auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"go.gearno.de/kit/httpserver"
|
||||
authsvc "go.probo.inc/probo/pkg/auth"
|
||||
"go.probo.inc/probo/pkg/authz"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
)
|
||||
|
||||
type (
|
||||
ListUserAPIKeysResponse struct {
|
||||
UserAPIKeys []UserAPIKeyResponse `json:"apiKeys"`
|
||||
}
|
||||
|
||||
UserAPIKeyResponse struct {
|
||||
ID gid.GID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
ExpiresAt time.Time `json:"expiresAt"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
Organizations []UserAPIKeyOrganizationMembership `json:"organizations"`
|
||||
}
|
||||
|
||||
UserAPIKeyOrganizationMembership struct {
|
||||
OrganizationID gid.GID `json:"organizationId"`
|
||||
OrganizationName string `json:"organizationName"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
)
|
||||
|
||||
func ListUserAPIKeysHandler(authSvc *authsvc.Service, authzSvc *authz.Service) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
user := UserFromContext(ctx)
|
||||
|
||||
organizations, err := authzSvc.GetAllUserOrganizations(ctx, user.ID)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot list organizations for user: %w", err))
|
||||
}
|
||||
|
||||
tenantIDs := make([]gid.TenantID, 0, len(organizations))
|
||||
for _, org := range organizations {
|
||||
tenantIDs = append(tenantIDs, org.ID.TenantID())
|
||||
}
|
||||
|
||||
userAPIKeysWithMemberships, err := authSvc.ListUserAPIKeysWithMemberships(ctx, user.ID, tenantIDs)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot list user api keys: %w", err))
|
||||
}
|
||||
|
||||
response := ListUserAPIKeysResponse{
|
||||
UserAPIKeys: make([]UserAPIKeyResponse, 0, len(userAPIKeysWithMemberships)),
|
||||
}
|
||||
|
||||
for _, keyWithMemberships := range userAPIKeysWithMemberships {
|
||||
organizations := make([]UserAPIKeyOrganizationMembership, 0, len(keyWithMemberships.Memberships))
|
||||
for _, membership := range keyWithMemberships.Memberships {
|
||||
organizations = append(organizations, UserAPIKeyOrganizationMembership{
|
||||
OrganizationID: membership.OrganizationID,
|
||||
OrganizationName: membership.OrganizationName,
|
||||
Role: membership.Role.String(),
|
||||
})
|
||||
}
|
||||
|
||||
response.UserAPIKeys = append(response.UserAPIKeys, UserAPIKeyResponse{
|
||||
ID: keyWithMemberships.UserAPIKey.ID,
|
||||
Name: keyWithMemberships.UserAPIKey.Name,
|
||||
ExpiresAt: keyWithMemberships.UserAPIKey.ExpiresAt,
|
||||
CreatedAt: keyWithMemberships.UserAPIKey.CreatedAt,
|
||||
Organizations: organizations,
|
||||
})
|
||||
}
|
||||
|
||||
httpserver.RenderJSON(w, http.StatusOK, response)
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
// 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 auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"go.gearno.de/kit/httpserver"
|
||||
"go.probo.inc/probo/pkg/authz"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/mail"
|
||||
)
|
||||
|
||||
type (
|
||||
ListInvitationsResponse struct {
|
||||
Invitations []InvitationResponse `json:"invitations"`
|
||||
}
|
||||
|
||||
InvitationResponse struct {
|
||||
ID gid.GID `json:"id"`
|
||||
Email mail.Addr `json:"email"`
|
||||
FullName string `json:"fullName"`
|
||||
Role string `json:"role"`
|
||||
ExpiresAt string `json:"expiresAt"`
|
||||
AcceptedAt *string `json:"acceptedAt,omitempty"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
Organization OrganizationResponseSummary `json:"organization"`
|
||||
}
|
||||
|
||||
OrganizationResponseSummary struct {
|
||||
ID gid.GID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
)
|
||||
|
||||
func ListInvitationsHandler(authzSvc *authz.Service) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
user := UserFromContext(ctx)
|
||||
|
||||
invitations, err := authzSvc.GetUserPendingInvitations(ctx, user.EmailAddress)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot list invitations for user: %w", err))
|
||||
}
|
||||
|
||||
response := ListInvitationsResponse{
|
||||
Invitations: make([]InvitationResponse, 0, len(invitations)),
|
||||
}
|
||||
|
||||
for _, invitation := range invitations {
|
||||
invitationResp := InvitationResponse{
|
||||
ID: invitation.ID,
|
||||
Email: invitation.Email,
|
||||
FullName: invitation.FullName,
|
||||
Role: invitation.Role.String(),
|
||||
ExpiresAt: invitation.ExpiresAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
CreatedAt: invitation.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
Organization: OrganizationResponseSummary{
|
||||
ID: invitation.Organization.ID,
|
||||
Name: invitation.Organization.Name,
|
||||
},
|
||||
}
|
||||
|
||||
if invitation.AcceptedAt != nil {
|
||||
acceptedAtStr := invitation.AcceptedAt.Format("2006-01-02T15:04:05Z07:00")
|
||||
invitationResp.AcceptedAt = &acceptedAtStr
|
||||
}
|
||||
|
||||
response.Invitations = append(response.Invitations, invitationResp)
|
||||
}
|
||||
|
||||
httpserver.RenderJSON(w, http.StatusOK, response)
|
||||
}
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
// 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 auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"go.gearno.de/kit/httpserver"
|
||||
authsvc "go.probo.inc/probo/pkg/auth"
|
||||
"go.probo.inc/probo/pkg/authz"
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
)
|
||||
|
||||
type (
|
||||
AuthenticationStatus string
|
||||
|
||||
ListOrganizationsResponse struct {
|
||||
Organizations []OrganizationResponse `json:"organizations"`
|
||||
}
|
||||
|
||||
OrganizationResponse struct {
|
||||
ID gid.GID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
LogoURL *string `json:"logoUrl,omitempty"`
|
||||
AuthenticationMethod string `json:"authenticationMethod"` // "password", "saml", or "any"
|
||||
AuthStatus AuthenticationStatus `json:"authStatus"` // "authenticated", "unauthenticated", "expired"
|
||||
LoginURL string `json:"loginUrl"` // URL to login (SAML or password login page)
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
AuthStatusAuthenticated AuthenticationStatus = "authenticated"
|
||||
AuthStatusUnauthenticated AuthenticationStatus = "unauthenticated"
|
||||
AuthStatusExpired AuthenticationStatus = "expired"
|
||||
)
|
||||
|
||||
func buildOrganizationResponse(
|
||||
org *coredata.Organization,
|
||||
accessResult authsvc.AccessResult,
|
||||
sessionData coredata.SessionData,
|
||||
) OrganizationResponse {
|
||||
// Generate logo URL path if organization has a logo
|
||||
var logoURL *string
|
||||
if org.LogoFileID != nil {
|
||||
url := fmt.Sprintf("/connect/organizations/%s/logo", org.ID)
|
||||
logoURL = &url
|
||||
}
|
||||
|
||||
orgResponse := OrganizationResponse{
|
||||
ID: org.ID,
|
||||
Name: org.Name,
|
||||
LogoURL: logoURL,
|
||||
}
|
||||
|
||||
// User does not have required authentication
|
||||
if !accessResult.Allowed {
|
||||
orgResponse.AuthStatus = AuthStatusUnauthenticated
|
||||
|
||||
switch accessResult.MissingAuth {
|
||||
case authsvc.AuthMethodSAML, authsvc.AuthMethodAny:
|
||||
orgResponse.AuthenticationMethod = "saml"
|
||||
if accessResult.SAMLConfig != nil {
|
||||
orgResponse.LoginURL = fmt.Sprintf("/connect/saml/login/%s", accessResult.SAMLConfig.ID)
|
||||
}
|
||||
case authsvc.AuthMethodPassword:
|
||||
orgResponse.AuthenticationMethod = "password"
|
||||
orgResponse.LoginURL = "/auth/login?method=password"
|
||||
}
|
||||
return orgResponse
|
||||
}
|
||||
|
||||
// User has required authentication
|
||||
orgResponse.AuthStatus = AuthStatusAuthenticated
|
||||
|
||||
if sessionData.PasswordAuthenticated {
|
||||
orgResponse.AuthenticationMethod = "password"
|
||||
orgResponse.LoginURL = "/auth/login?method=password"
|
||||
} else if samlInfo, ok := sessionData.SAMLAuthenticatedOrgs[org.ID.String()]; ok {
|
||||
orgResponse.AuthenticationMethod = "saml"
|
||||
orgResponse.LoginURL = fmt.Sprintf("/connect/saml/login/%s", samlInfo.SAMLConfigID)
|
||||
} else {
|
||||
orgResponse.AuthenticationMethod = "any"
|
||||
orgResponse.LoginURL = "/auth/login?method=password"
|
||||
}
|
||||
|
||||
return orgResponse
|
||||
}
|
||||
|
||||
func ListOrganizationsHandler(authSvc *authsvc.Service, authzSvc *authz.Service) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
user := UserFromContext(ctx)
|
||||
sess := SessionFromContext(ctx)
|
||||
|
||||
var organizations coredata.Organizations
|
||||
var err error
|
||||
|
||||
roleFilter := r.URL.Query().Get("role")
|
||||
if roleFilter != "" {
|
||||
role := coredata.MembershipRole(roleFilter)
|
||||
organizations, err = authzSvc.GetUserOrganizationsWithRole(ctx, user.ID, role)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot list organizations for user with role: %w", err))
|
||||
}
|
||||
} else {
|
||||
organizations, err = authzSvc.GetAllUserOrganizations(ctx, user.ID)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot list organizations for user: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
orgIDs := make([]gid.GID, len(organizations))
|
||||
for i, org := range organizations {
|
||||
orgIDs[i] = org.ID
|
||||
}
|
||||
accessResults, err := authSvc.CheckOrganizationAccess(ctx, user, orgIDs, sess)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot check organization access: %w", err))
|
||||
}
|
||||
|
||||
response := ListOrganizationsResponse{
|
||||
Organizations: make([]OrganizationResponse, 0, len(organizations)),
|
||||
}
|
||||
for _, org := range organizations {
|
||||
accessResult := accessResults[org.ID]
|
||||
orgResponse := buildOrganizationResponse(org, accessResult, sess.Data)
|
||||
response.Organizations = append(response.Organizations, orgResponse)
|
||||
}
|
||||
|
||||
httpserver.RenderJSON(w, http.StatusOK, response)
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
// 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 auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
authsvc "go.probo.inc/probo/pkg/auth"
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
func OrganizationLogoHandler(authSvc *authsvc.Service, fileManager interface {
|
||||
GenerateFileUrl(ctx context.Context, file *coredata.File, duration time.Duration) (string, error)
|
||||
}) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
user := UserFromContext(ctx)
|
||||
session := SessionFromContext(ctx)
|
||||
|
||||
organizationIDStr := chi.URLParam(r, "organizationID")
|
||||
organizationID, err := gid.ParseGID(organizationIDStr)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid organization ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
logoFile, err := authSvc.GetOrganizationLogoFile(ctx, user, organizationID, session)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot get organization logo: %w", err))
|
||||
}
|
||||
|
||||
presignedURL, err := fileManager.GenerateFileUrl(ctx, logoFile, 1*time.Hour)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot generate presigned URL: %w", err))
|
||||
}
|
||||
|
||||
w.Header().Set("Cache-Control", "public, max-age=3600")
|
||||
|
||||
http.Redirect(w, r, presignedURL, http.StatusFound)
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
// 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 auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"errors"
|
||||
|
||||
authsvc "go.probo.inc/probo/pkg/auth"
|
||||
"go.gearno.de/kit/httpserver"
|
||||
)
|
||||
|
||||
type (
|
||||
ResetPasswordRequest struct {
|
||||
Token string `json:"token"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
ResetPasswordResponse struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
)
|
||||
|
||||
func ResetPasswordHandler(authSvc *authsvc.Service) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req ResetPasswordRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("cannot decode body: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
err := authSvc.ResetPassword(r.Context(), req.Token, req.Password)
|
||||
if err != nil {
|
||||
var invalidPasswordErr *authsvc.ErrInvalidPassword
|
||||
var invalidTokenErr *authsvc.ErrInvalidTokenType
|
||||
|
||||
if errors.As(err, &invalidPasswordErr) {
|
||||
httpserver.RenderError(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
if errors.As(err, &invalidTokenErr) {
|
||||
httpserver.RenderError(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
httpserver.RenderError(w, http.StatusInternalServerError, fmt.Errorf("cannot reset password: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
httpserver.RenderJSON(w, http.StatusOK, ResetPasswordResponse{
|
||||
Success: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
// 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 auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"go.gearno.de/kit/log"
|
||||
authsvc "go.probo.inc/probo/pkg/auth"
|
||||
"go.probo.inc/probo/pkg/authz"
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/securecookie"
|
||||
)
|
||||
|
||||
func getSessionIDFromCookie(r *http.Request, cookieName string, cookieSecret string, cookieSecure bool) (gid.GID, error) {
|
||||
cookieValue, err := securecookie.Get(
|
||||
r,
|
||||
securecookie.DefaultConfig(
|
||||
cookieName,
|
||||
cookieSecret,
|
||||
cookieSecure,
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
return gid.GID{}, err
|
||||
}
|
||||
|
||||
return gid.ParseGID(cookieValue)
|
||||
}
|
||||
|
||||
func SAMLACSHandler(samlSvc *authsvc.SAMLService, authSvc *authsvc.Service, authzSvc *authz.Service, cookieName string, cookieSecret string, cookieSecure bool, sessionDuration time.Duration, logger *log.Logger) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
if err := r.ParseForm(); err != nil {
|
||||
logger.ErrorCtx(ctx, "cannot parse form", log.Error(err))
|
||||
http.Error(w, "cannot parse form", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if r.FormValue("SAMLResponse") == "" {
|
||||
logger.WarnCtx(ctx, "missing SAMLResponse")
|
||||
http.Error(w, "missing SAMLResponse", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
userInfo, err := samlSvc.HandleSAMLAssertion(ctx, r)
|
||||
if err != nil {
|
||||
logger.ErrorCtx(ctx, "SAML authentication failed", log.Error(err))
|
||||
http.Error(w, "SAML authentication failed", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
var existingSession *coredata.Session
|
||||
if existingSessionID, err := getSessionIDFromCookie(r, cookieName, cookieSecret, cookieSecure); err == nil {
|
||||
if session, err := authSvc.GetSession(ctx, existingSessionID); err == nil {
|
||||
existingSession = session
|
||||
}
|
||||
}
|
||||
|
||||
session, user, err := authSvc.ProvisionSAMLUser(
|
||||
ctx,
|
||||
userInfo.SAMLConfigID,
|
||||
userInfo.OrganizationID,
|
||||
userInfo.Email,
|
||||
userInfo.FullName,
|
||||
userInfo.SAMLSubject,
|
||||
existingSession,
|
||||
sessionDuration,
|
||||
)
|
||||
if err != nil {
|
||||
var autoSignupDisabledErr *authsvc.ErrSAMLAutoSignupDisabled
|
||||
if errors.As(err, &autoSignupDisabledErr) {
|
||||
logger.WarnCtx(ctx, "SAML auto-signup is disabled")
|
||||
http.Error(w, "User does not exist and auto-signup is disabled for this organization", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
logger.ErrorCtx(ctx, "cannot provision SAML user", log.Error(err))
|
||||
http.Error(w, "cannot provision user", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
tenantAuthzSvc := authzSvc.WithTenant(userInfo.OrganizationID.TenantID())
|
||||
err = tenantAuthzSvc.EnsureSAMLMembership(ctx, user.ID, userInfo.OrganizationID, userInfo.Role)
|
||||
if err != nil {
|
||||
logger.ErrorCtx(ctx, "cannot ensure membership", log.Error(err), log.String("user_id", user.ID.String()), log.String("org_id", userInfo.OrganizationID.String()))
|
||||
http.Error(w, "cannot create membership", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
securecookie.Set(
|
||||
w,
|
||||
securecookie.DefaultConfig(
|
||||
cookieName,
|
||||
cookieSecret,
|
||||
cookieSecure,
|
||||
),
|
||||
session.ID.String(),
|
||||
)
|
||||
|
||||
logger.InfoCtx(ctx, "SAML login successful", log.String("user_id", user.ID.String()), log.String("org_id", userInfo.OrganizationID.String()))
|
||||
|
||||
redirectURL := fmt.Sprintf("/organizations/%s", userInfo.OrganizationID)
|
||||
http.Redirect(w, r, redirectURL, http.StatusFound)
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
// 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 auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/mail"
|
||||
|
||||
authsvc "go.probo.inc/probo/pkg/auth"
|
||||
"go.gearno.de/kit/httpserver"
|
||||
"go.gearno.de/kit/log"
|
||||
)
|
||||
|
||||
type (
|
||||
CheckSSORequest struct {
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
CheckSSOResponse struct {
|
||||
SSOAvailable bool `json:"ssoAvailable"`
|
||||
SAMLConfigID *string `json:"samlConfigId,omitempty"`
|
||||
OrganizationID *string `json:"organizationId,omitempty"`
|
||||
EnforcementPolicy *string `json:"enforcementPolicy,omitempty"`
|
||||
}
|
||||
)
|
||||
|
||||
func SAMLCheckSSOHandler(authSvc *authsvc.Service, logger *log.Logger) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
var req CheckSSORequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("cannot decode body: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
if req.Email == "" {
|
||||
httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("email is required"))
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := mail.ParseAddress(req.Email); err != nil {
|
||||
httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("invalid email format"))
|
||||
return
|
||||
}
|
||||
|
||||
configs, err := authSvc.CheckSSOAvailabilityByEmail(ctx, req.Email)
|
||||
if err != nil {
|
||||
logger.ErrorCtx(ctx, "cannot check SSO availability", log.Error(err))
|
||||
httpserver.RenderError(w, http.StatusInternalServerError, fmt.Errorf("cannot check SSO availability"))
|
||||
return
|
||||
}
|
||||
|
||||
// No SAML configs found for this domain
|
||||
if len(configs) == 0 {
|
||||
httpserver.RenderJSON(w, http.StatusOK, CheckSSOResponse{
|
||||
SSOAvailable: false,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Multiple SAML configs found - ambiguous, user must use organization-specific SSO URL
|
||||
if len(configs) > 1 {
|
||||
logger.WarnCtx(ctx, "multiple SAML configurations found for domain", log.Int("count", len(configs)))
|
||||
httpserver.RenderError(w, http.StatusConflict, fmt.Errorf("multiple SSO configurations found for this domain. Please use your organization-specific SSO login URL"))
|
||||
return
|
||||
}
|
||||
|
||||
// Single SAML config found - return it
|
||||
config := configs[0]
|
||||
configIDStr := config.ID.String()
|
||||
orgIDStr := config.OrganizationID.String()
|
||||
enforcementPolicy := string(config.EnforcementPolicy)
|
||||
|
||||
httpserver.RenderJSON(w, http.StatusOK, CheckSSOResponse{
|
||||
SSOAvailable: true,
|
||||
SAMLConfigID: &configIDStr,
|
||||
OrganizationID: &orgIDStr,
|
||||
EnforcementPolicy: &enforcementPolicy,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
// 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 auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
authsvc "go.probo.inc/probo/pkg/auth"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"go.gearno.de/kit/log"
|
||||
)
|
||||
|
||||
func SAMLLoginHandler(samlSvc *authsvc.SAMLService, authSvc *authsvc.Service, logger *log.Logger) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
samlConfigIDStr := chi.URLParam(r, "samlConfigID")
|
||||
if samlConfigIDStr == "" {
|
||||
logger.WarnCtx(ctx, "missing SAML config ID in URL")
|
||||
http.Error(w, "missing SAML config ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
samlConfigID, err := gid.ParseGID(samlConfigIDStr)
|
||||
if err != nil {
|
||||
logger.ErrorCtx(ctx, "invalid SAML config ID", log.Error(err), log.String("saml_config_id", samlConfigIDStr))
|
||||
http.Error(w, "invalid SAML config ID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
tenantID := samlConfigID.TenantID()
|
||||
|
||||
config, err := authSvc.WithTenant(tenantID).GetSAMLConfigurationByID(ctx, samlConfigID)
|
||||
if err != nil {
|
||||
logger.ErrorCtx(ctx, "cannot load SAML configuration", log.Error(err), log.String("saml_config_id", samlConfigID.String()))
|
||||
http.Error(w, "SAML configuration not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
redirectURL, err := samlSvc.InitiateSAMLLogin(ctx, config.OrganizationID, tenantID, config.EmailDomain)
|
||||
if err != nil {
|
||||
logger.ErrorCtx(ctx, "cannot initiate SAML login", log.Error(err), log.String("saml_config_id", samlConfigID.String()), log.String("org_id", config.OrganizationID.String()))
|
||||
http.Error(w, fmt.Sprintf("SAML login failed: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
logger.InfoCtx(ctx, "SAML login initiated", log.String("saml_config_id", samlConfigID.String()), log.String("org_id", config.OrganizationID.String()))
|
||||
|
||||
http.Redirect(w, r, redirectURL, http.StatusFound)
|
||||
}
|
||||
}
|
||||
@@ -1,102 +0,0 @@
|
||||
// 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 auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"go.gearno.de/kit/httpserver"
|
||||
authsvc "go.probo.inc/probo/pkg/auth"
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/mail"
|
||||
"go.probo.inc/probo/pkg/securecookie"
|
||||
)
|
||||
|
||||
type (
|
||||
SignInRequest struct {
|
||||
Email mail.Addr `json:"email"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
SignInResponse struct {
|
||||
User UserResponse `json:"user"`
|
||||
}
|
||||
|
||||
UserResponse struct {
|
||||
ID gid.GID `json:"id"`
|
||||
Email mail.Addr `json:"email"`
|
||||
FullName string `json:"fullName"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
)
|
||||
|
||||
func SignInHandler(authSvc *authsvc.Service, cookieName string, cookieSecret string, cookieSecure bool) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var req SignInRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("cannot decode body: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
var existingSession *coredata.Session
|
||||
if existingSessionID, err := getSessionIDFromCookie(r, cookieName, cookieSecret, cookieSecure); err == nil {
|
||||
if session, err := authSvc.GetSession(r.Context(), existingSessionID); err == nil {
|
||||
existingSession = session
|
||||
}
|
||||
}
|
||||
|
||||
session, user, err := authSvc.SignIn(r.Context(), req.Email, req.Password, existingSession)
|
||||
if err != nil {
|
||||
var ErrInvalidCredentials *authsvc.ErrInvalidCredentials
|
||||
if errors.As(err, &ErrInvalidCredentials) {
|
||||
httpserver.RenderError(w, http.StatusUnauthorized, err)
|
||||
return
|
||||
}
|
||||
|
||||
panic(fmt.Errorf("cannot sign in: %w", err))
|
||||
}
|
||||
|
||||
securecookie.Set(
|
||||
w,
|
||||
securecookie.DefaultConfig(
|
||||
cookieName,
|
||||
cookieSecret,
|
||||
cookieSecure,
|
||||
),
|
||||
session.ID.String(),
|
||||
)
|
||||
|
||||
httpserver.RenderJSON(
|
||||
w,
|
||||
http.StatusOK,
|
||||
SignInResponse{
|
||||
User: UserResponse{
|
||||
ID: user.ID,
|
||||
Email: user.EmailAddress,
|
||||
FullName: user.FullName,
|
||||
CreatedAt: user.CreatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
// 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 auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"go.gearno.de/kit/httpserver"
|
||||
authsvc "go.probo.inc/probo/pkg/auth"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/securecookie"
|
||||
)
|
||||
|
||||
func SignOutHandler(authSvc *authsvc.Service, cookieName string, cookieSecret string, cookieSecure bool) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
sessionID, err := securecookie.Get(r, securecookie.DefaultConfig(
|
||||
cookieName,
|
||||
cookieSecret,
|
||||
cookieSecure,
|
||||
))
|
||||
if err != nil {
|
||||
httpserver.RenderError(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
gid, err := gid.ParseGID(sessionID)
|
||||
if err != nil {
|
||||
httpserver.RenderError(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
err = authSvc.SignOut(r.Context(), gid)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot sign out: %w", err))
|
||||
}
|
||||
|
||||
securecookie.Clear(w, securecookie.DefaultConfig(
|
||||
cookieName,
|
||||
cookieSecret,
|
||||
cookieSecure,
|
||||
))
|
||||
|
||||
w.Header().Set("Clear-Site-Data", "*")
|
||||
|
||||
httpserver.RenderJSON(w, http.StatusOK, map[string]bool{"success": true})
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
// 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 auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"go.gearno.de/kit/httpserver"
|
||||
authsvc "go.probo.inc/probo/pkg/auth"
|
||||
"go.probo.inc/probo/pkg/mail"
|
||||
"go.probo.inc/probo/pkg/securecookie"
|
||||
)
|
||||
|
||||
type (
|
||||
SignUpRequest struct {
|
||||
Email mail.Addr `json:"email"`
|
||||
Password string `json:"password"`
|
||||
FullName string `json:"fullName"`
|
||||
}
|
||||
|
||||
SignUpResponse struct {
|
||||
User UserResponse `json:"user"`
|
||||
}
|
||||
)
|
||||
|
||||
func SignUpHandler(authSvc *authsvc.Service, cookieName string, cookieSecret string, cookieSecure bool) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req SignUpRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("cannot decode body: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
user, session, err := authSvc.SignUp(
|
||||
r.Context(),
|
||||
req.Email,
|
||||
req.Password,
|
||||
req.FullName,
|
||||
)
|
||||
if err != nil {
|
||||
var errUserAlreadyExists *authsvc.ErrUserAlreadyExists
|
||||
if errors.As(err, &errUserAlreadyExists) {
|
||||
httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("cannot register user: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
var errSignupDisabled *authsvc.ErrSignupDisabled
|
||||
if errors.As(err, &errSignupDisabled) {
|
||||
httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("cannot register user: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
panic(fmt.Errorf("cannot register user: %w", err))
|
||||
}
|
||||
|
||||
securecookie.Set(
|
||||
w,
|
||||
securecookie.DefaultConfig(
|
||||
cookieName,
|
||||
cookieSecret,
|
||||
cookieSecure,
|
||||
),
|
||||
session.ID.String(),
|
||||
)
|
||||
|
||||
httpserver.RenderJSON(
|
||||
w,
|
||||
http.StatusOK,
|
||||
SignUpResponse{
|
||||
User: UserResponse{
|
||||
ID: user.ID,
|
||||
Email: user.EmailAddress,
|
||||
FullName: user.FullName,
|
||||
CreatedAt: user.CreatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
// 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 auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"go.gearno.de/kit/httpserver"
|
||||
authsvc "go.probo.inc/probo/pkg/auth"
|
||||
"go.probo.inc/probo/pkg/securecookie"
|
||||
)
|
||||
|
||||
type (
|
||||
SignupFromInvitationRequest struct {
|
||||
Token string `json:"token"`
|
||||
Password string `json:"password"`
|
||||
FullName string `json:"fullName"`
|
||||
}
|
||||
|
||||
SignupFromInvitationResponse struct {
|
||||
}
|
||||
)
|
||||
|
||||
func SignupFromInvitationHandler(authSvc *authsvc.Service, cookieName string, cookieSecret string, cookieSecure bool) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
var req SignupFromInvitationRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httpserver.RenderError(w, http.StatusBadRequest, fmt.Errorf("cannot decode body: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
user, session, err := authSvc.SignupFromInvitation(r.Context(), req.Token, req.Password, req.FullName)
|
||||
if err != nil {
|
||||
httpserver.RenderError(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
securecookie.Set(
|
||||
w,
|
||||
securecookie.DefaultConfig(
|
||||
cookieName,
|
||||
cookieSecret,
|
||||
cookieSecure,
|
||||
),
|
||||
session.ID.String(),
|
||||
)
|
||||
|
||||
httpserver.RenderJSON(
|
||||
w,
|
||||
http.StatusOK,
|
||||
SignUpResponse{
|
||||
User: UserResponse{
|
||||
ID: user.ID,
|
||||
Email: user.EmailAddress,
|
||||
FullName: user.FullName,
|
||||
CreatedAt: user.CreatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
// 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 auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"go.gearno.de/kit/httpserver"
|
||||
authsvc "go.probo.inc/probo/pkg/auth"
|
||||
"go.probo.inc/probo/pkg/authz"
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
)
|
||||
|
||||
type UpdateUserAPIKeyRequest struct {
|
||||
ID string `json:"id"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
Organizations []UserAPIKeyOrganizationMembershipRequest `json:"organizations"`
|
||||
}
|
||||
|
||||
func UpdateUserAPIKeyHandler(authSvc *authsvc.Service, authzSvc *authz.Service) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
user := UserFromContext(ctx)
|
||||
|
||||
var req UpdateUserAPIKeyRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httpserver.RenderJSON(w, http.StatusBadRequest, map[string]string{
|
||||
"error": "invalid request body",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if req.ID == "" {
|
||||
httpserver.RenderJSON(w, http.StatusBadRequest, map[string]string{
|
||||
"error": "user api key id is required",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
userAPIKeyID, err := gid.ParseGID(req.ID)
|
||||
if err != nil {
|
||||
httpserver.RenderJSON(w, http.StatusBadRequest, map[string]string{
|
||||
"error": "invalid user api key id",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Name == nil && len(req.Organizations) == 0 {
|
||||
httpserver.RenderJSON(w, http.StatusBadRequest, map[string]string{
|
||||
"error": "at least one field must be provided for update",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Name != nil && *req.Name != "" {
|
||||
if err := authSvc.UpdateUserAPIKeyName(ctx, userAPIKeyID, user.ID, *req.Name); err != nil {
|
||||
var errNotFound *coredata.ErrUserAPIKeyNotFound
|
||||
if errors.As(err, &errNotFound) {
|
||||
httpserver.RenderJSON(w, http.StatusNotFound, map[string]string{
|
||||
"error": "user api key not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
if strings.Contains(err.Error(), "does not belong to user") {
|
||||
httpserver.RenderJSON(w, http.StatusForbidden, map[string]string{
|
||||
"error": "access denied",
|
||||
})
|
||||
return
|
||||
}
|
||||
httpserver.RenderJSON(w, http.StatusInternalServerError, map[string]string{
|
||||
"error": "failed to update user api key name",
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if len(req.Organizations) > 0 {
|
||||
orgInputs := make([]authsvc.UserAPIKeyOrganizationRequest, len(req.Organizations))
|
||||
for i, org := range req.Organizations {
|
||||
orgID, err := gid.ParseGID(org.OrganizationID)
|
||||
if err != nil {
|
||||
httpserver.RenderJSON(w, http.StatusBadRequest, map[string]string{
|
||||
"error": "invalid organization id",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user is an OWNER for this organization
|
||||
tenantAuthzSvc := authzSvc.WithTenant(orgID.TenantID())
|
||||
role, err := tenantAuthzSvc.GetUserRoleInOrganization(ctx, user.ID, orgID)
|
||||
if err != nil {
|
||||
httpserver.RenderJSON(w, http.StatusForbidden, map[string]string{
|
||||
"error": "user does not have access to this organization",
|
||||
})
|
||||
return
|
||||
}
|
||||
if role != coredata.MembershipRoleOwner {
|
||||
httpserver.RenderJSON(w, http.StatusForbidden, map[string]string{
|
||||
"error": "only owners can update API keys for this organization",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
orgInputs[i] = authsvc.UserAPIKeyOrganizationRequest{
|
||||
OrganizationID: orgID,
|
||||
Role: coredata.APIRole(org.Role),
|
||||
}
|
||||
}
|
||||
|
||||
memberships, err := authSvc.ValidateAndBuildUserAPIKeyMemberships(ctx, user.ID, orgInputs)
|
||||
if err != nil {
|
||||
httpserver.RenderJSON(w, http.StatusBadRequest, map[string]string{
|
||||
"error": "invalid organizations",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if err := authSvc.UpdateUserAPIKeyMemberships(ctx, userAPIKeyID, user.ID, memberships); err != nil {
|
||||
var errNotFound *coredata.ErrUserAPIKeyNotFound
|
||||
if errors.As(err, &errNotFound) {
|
||||
httpserver.RenderJSON(w, http.StatusNotFound, map[string]string{
|
||||
"error": "user api key not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
if strings.Contains(err.Error(), "does not belong to user") {
|
||||
httpserver.RenderJSON(w, http.StatusForbidden, map[string]string{
|
||||
"error": "access denied",
|
||||
})
|
||||
return
|
||||
}
|
||||
httpserver.RenderJSON(w, http.StatusInternalServerError, map[string]string{
|
||||
"error": "failed to update user api key",
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
httpserver.RenderJSON(w, http.StatusOK, map[string]string{
|
||||
"message": "User API key updated successfully",
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
// 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 authz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"go.gearno.de/kit/httpserver"
|
||||
"go.gearno.de/kit/log"
|
||||
authsvc "go.probo.inc/probo/pkg/auth"
|
||||
"go.probo.inc/probo/pkg/authz"
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/server/session"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Auth *authsvc.Service
|
||||
Authz *authz.Service
|
||||
Logger *log.Logger
|
||||
CookieName string
|
||||
CookieSecret string
|
||||
CookieSecure bool
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
router *chi.Mux
|
||||
}
|
||||
|
||||
type ctxKey struct{ name string }
|
||||
|
||||
var (
|
||||
userContextKey = &ctxKey{name: "user"}
|
||||
)
|
||||
|
||||
func NewServer(cfg Config) (*Server, error) {
|
||||
router := chi.NewRouter()
|
||||
|
||||
// Apply authentication middleware to all routes
|
||||
router.Use(requireAuth(cfg.Auth, cfg.Authz, cfg.CookieName, cfg.CookieSecret, cfg.CookieSecure))
|
||||
|
||||
router.Get("/{organizationID}/permissions", PermissionsHandler(cfg.Authz, UserFromContext, cfg.Logger))
|
||||
|
||||
return &Server{
|
||||
router: router,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
s.router.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// requireAuth is a middleware that requires authentication
|
||||
func requireAuth(
|
||||
authService *authsvc.Service,
|
||||
authzService *authz.Service,
|
||||
cookieName string,
|
||||
cookieSecret string,
|
||||
cookieSecure bool,
|
||||
) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
sessionAuthCfg := session.AuthConfig{
|
||||
CookieName: cookieName,
|
||||
CookieSecret: cookieSecret,
|
||||
CookieSecure: cookieSecure,
|
||||
}
|
||||
|
||||
errorHandler := session.ErrorHandler{
|
||||
OnCookieError: func(err error) {
|
||||
httpserver.RenderError(w, http.StatusUnauthorized, fmt.Errorf("invalid session"))
|
||||
},
|
||||
OnParseError: func(w http.ResponseWriter, authCfg session.AuthConfig) {
|
||||
session.ClearCookie(w, authCfg)
|
||||
httpserver.RenderError(w, http.StatusUnauthorized, fmt.Errorf("invalid session"))
|
||||
},
|
||||
OnSessionError: func(w http.ResponseWriter, authCfg session.AuthConfig) {
|
||||
session.ClearCookie(w, authCfg)
|
||||
httpserver.RenderError(w, http.StatusUnauthorized, fmt.Errorf("session expired"))
|
||||
},
|
||||
OnUserError: func(w http.ResponseWriter, authCfg session.AuthConfig) {
|
||||
session.ClearCookie(w, authCfg)
|
||||
httpserver.RenderError(w, http.StatusUnauthorized, fmt.Errorf("user not found"))
|
||||
},
|
||||
OnTenantError: func(err error) {
|
||||
panic(fmt.Errorf("cannot list tenants for user: %w", err))
|
||||
},
|
||||
}
|
||||
|
||||
authResult := session.TryAuth(ctx, w, r, authService, authzService, sessionAuthCfg, errorHandler)
|
||||
if authResult == nil {
|
||||
httpserver.RenderError(w, http.StatusUnauthorized, fmt.Errorf("authentication required"))
|
||||
return
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, userContextKey, authResult.User)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func UserFromContext(ctx context.Context) *coredata.User {
|
||||
user, _ := ctx.Value(userContextKey).(*coredata.User)
|
||||
return user
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
// 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 authz
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"go.gearno.de/kit/httpserver"
|
||||
"go.gearno.de/kit/log"
|
||||
"go.probo.inc/probo/pkg/authz"
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
)
|
||||
|
||||
// PermissionsHandler returns permissions for the current user's role in an organization
|
||||
// It uses the centralized permissions map
|
||||
func PermissionsHandler(
|
||||
authzService *authz.Service,
|
||||
userFromContext func(ctx context.Context) *coredata.User,
|
||||
logger *log.Logger,
|
||||
) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
orgIDStr := chi.URLParam(r, "organizationID")
|
||||
if orgIDStr == "" {
|
||||
http.Error(w, "organizationID parameter required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
orgID, err := gid.ParseGID(orgIDStr)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid organizationID", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
user := userFromContext(ctx)
|
||||
if user == nil {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
tenantAuthzSvc := authzService.WithTenant(orgID.TenantID())
|
||||
|
||||
memberRole, err := tenantAuthzSvc.GetUserRoleInOrganization(ctx, user.ID, orgID)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("cannot get user role: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
userRole := authz.Role(memberRole.String())
|
||||
|
||||
permissions := authz.GetPermissionsByRole(userRole)
|
||||
|
||||
response := map[string]any{
|
||||
"permissions": permissions,
|
||||
"role": memberRole.String(),
|
||||
}
|
||||
|
||||
httpserver.RenderJSON(w, http.StatusOK, response)
|
||||
}
|
||||
}
|
||||
71
pkg/server/gqlutils/handler.go
Normal file
71
pkg/server/gqlutils/handler.go
Normal file
@@ -0,0 +1,71 @@
|
||||
// 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 gqlutils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/99designs/gqlgen/graphql"
|
||||
"github.com/99designs/gqlgen/graphql/handler"
|
||||
"github.com/99designs/gqlgen/graphql/handler/extension"
|
||||
"github.com/99designs/gqlgen/graphql/handler/transport"
|
||||
|
||||
"go.gearno.de/kit/log"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
gqlhandler *handler.Server
|
||||
}
|
||||
|
||||
var (
|
||||
mb int64 = 1024 * 1024
|
||||
|
||||
postTransport = transport.POST{}
|
||||
optionsTransport = transport.Options{}
|
||||
multipartTransport = transport.MultipartForm{
|
||||
MaxMemory: 32 * mb,
|
||||
MaxUploadSize: 50 * mb,
|
||||
}
|
||||
|
||||
introspectionExtension = extension.Introspection{}
|
||||
)
|
||||
|
||||
func NewHandler[S graphql.ExecutableSchema](executableSchema S, logger *log.Logger) *Handler {
|
||||
handler := handler.New(executableSchema)
|
||||
|
||||
handler.AddTransport(postTransport)
|
||||
handler.AddTransport(optionsTransport)
|
||||
handler.AddTransport(multipartTransport)
|
||||
|
||||
handler.Use(introspectionExtension)
|
||||
handler.Use(NewTracingExtension(logger))
|
||||
|
||||
handler.SetRecoverFunc(RecoverFunc)
|
||||
|
||||
return &Handler{gqlhandler: handler}
|
||||
}
|
||||
|
||||
func (gqlh *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
gqlh.gqlhandler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (gqlh *Handler) Use(extension graphql.HandlerExtension) {
|
||||
gqlh.gqlhandler.Use(extension)
|
||||
}
|
||||
|
||||
func (gqlh *Handler) AroundOperations(f func(ctx context.Context, next graphql.OperationHandler) graphql.ResponseHandler) {
|
||||
gqlh.gqlhandler.AroundOperations(f)
|
||||
}
|
||||
@@ -22,8 +22,7 @@ import (
|
||||
"github.com/vektah/gqlparser/v2/gqlerror"
|
||||
"go.gearno.de/kit/httpserver"
|
||||
"go.gearno.de/kit/log"
|
||||
"go.probo.inc/probo/pkg/auth"
|
||||
"go.probo.inc/probo/pkg/authz"
|
||||
"go.probo.inc/probo/pkg/iam"
|
||||
"go.probo.inc/probo/pkg/validator"
|
||||
)
|
||||
|
||||
@@ -32,24 +31,25 @@ func RecoverFunc(ctx context.Context, err any) error {
|
||||
return gqlErr
|
||||
}
|
||||
|
||||
var errSAMLRequired auth.ErrSAMLAuthRequired
|
||||
if errors.As(asError(err), &errSAMLRequired) {
|
||||
return AuthenticationRequired(map[string]any{
|
||||
"requiresSaml": true,
|
||||
"redirectUrl": errSAMLRequired.RedirectURL,
|
||||
"samlConfigId": errSAMLRequired.ConfigID.String(),
|
||||
"organizationId": errSAMLRequired.OrganizationID.String(),
|
||||
})
|
||||
}
|
||||
// TODO: multi session here
|
||||
// var errSAMLRequired iam.ErrSAMLAuthRequired
|
||||
// if errors.As(asError(err), &errSAMLRequired) {
|
||||
// return AuthenticationRequired(map[string]any{
|
||||
// "requiresSaml": true,
|
||||
// "redirectUrl": errSAMLRequired.RedirectURL,
|
||||
// "samlConfigId": errSAMLRequired.ConfigID.String(),
|
||||
// "organizationId": errSAMLRequired.OrganizationID.String(),
|
||||
// })
|
||||
// }
|
||||
|
||||
var errPasswordRequired auth.ErrPasswordAuthRequired
|
||||
if errors.As(asError(err), &errPasswordRequired) {
|
||||
return AuthenticationRequired(map[string]any{
|
||||
"requiresSaml": false,
|
||||
"redirectUrl": errPasswordRequired.RedirectURL,
|
||||
"organizationId": errPasswordRequired.OrganizationID.String(),
|
||||
})
|
||||
}
|
||||
// var errPasswordRequired iam.ErrPasswordAuthRequired
|
||||
// if errors.As(asError(err), &errPasswordRequired) {
|
||||
// return AuthenticationRequired(map[string]any{
|
||||
// "requiresSaml": false,
|
||||
// "redirectUrl": errPasswordRequired.RedirectURL,
|
||||
// "organizationId": errPasswordRequired.OrganizationID.String(),
|
||||
// })
|
||||
// }
|
||||
|
||||
var errValidations validator.ValidationErrors
|
||||
if errors.As(asError(err), &errValidations) {
|
||||
@@ -72,12 +72,12 @@ func RecoverFunc(ctx context.Context, err any) error {
|
||||
return gqlErrors
|
||||
}
|
||||
|
||||
var tenantAccessErr *authz.TenantAccessError
|
||||
var tenantAccessErr *iam.TenantAccessError
|
||||
if errTyped, ok := err.(error); ok && errors.As(errTyped, &tenantAccessErr) {
|
||||
return Unauthorized()
|
||||
}
|
||||
|
||||
var permissionDeniedErr *authz.PermissionDeniedError
|
||||
var permissionDeniedErr *iam.ErrInsufficientPermissions
|
||||
if errTyped, ok := err.(error); ok && errors.As(errTyped, &permissionDeniedErr) {
|
||||
return Forbidden(permissionDeniedErr)
|
||||
}
|
||||
|
||||
@@ -23,52 +23,40 @@ import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"go.gearno.de/kit/httpserver"
|
||||
"go.gearno.de/kit/log"
|
||||
"go.gearno.de/kit/pg"
|
||||
"go.probo.inc/probo/pkg/agents"
|
||||
"go.probo.inc/probo/pkg/auth"
|
||||
"go.probo.inc/probo/pkg/authz"
|
||||
"go.probo.inc/probo/pkg/baseurl"
|
||||
"go.probo.inc/probo/pkg/connector"
|
||||
"go.probo.inc/probo/pkg/filemanager"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/iam"
|
||||
"go.probo.inc/probo/pkg/probo"
|
||||
"go.probo.inc/probo/pkg/saferedirect"
|
||||
"go.probo.inc/probo/pkg/securecookie"
|
||||
"go.probo.inc/probo/pkg/server/api"
|
||||
trust_v1 "go.probo.inc/probo/pkg/server/api/trust/v1"
|
||||
auth_server "go.probo.inc/probo/pkg/server/auth"
|
||||
authz_server "go.probo.inc/probo/pkg/server/authz"
|
||||
"go.probo.inc/probo/pkg/server/trust"
|
||||
"go.probo.inc/probo/pkg/server/web"
|
||||
trust_web "go.probo.inc/probo/pkg/server/trust"
|
||||
console_web "go.probo.inc/probo/pkg/server/web"
|
||||
"go.probo.inc/probo/pkg/slack"
|
||||
trust_pkg "go.probo.inc/probo/pkg/trust"
|
||||
"go.probo.inc/probo/pkg/trust"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
BaseURL *baseurl.BaseURL
|
||||
AllowedOrigins []string
|
||||
ExtraHeaderFields map[string]string
|
||||
Probo *probo.Service
|
||||
Auth *auth.Service
|
||||
Authz *authz.Service
|
||||
Trust *trust_pkg.Service
|
||||
IAM *iam.Service
|
||||
Trust *trust.Service
|
||||
Slack *slack.Service
|
||||
SAML *auth.SAMLService
|
||||
ConsoleAuth api.ConsoleAuthConfig
|
||||
TrustAuth api.TrustAuthConfig
|
||||
MCPConfig api.MCPConfig
|
||||
Cookie securecookie.Config
|
||||
ConnectorRegistry *connector.ConnectorRegistry
|
||||
Agent *agents.Agent
|
||||
SafeRedirect *saferedirect.SafeRedirect
|
||||
CustomDomainCname string
|
||||
FileManager *filemanager.Service
|
||||
PGClient *pg.Client
|
||||
Logger *log.Logger
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
apiServer *api.Server
|
||||
webServer *web.Server
|
||||
trustServer *trust.Server
|
||||
authServer *auth_server.Server
|
||||
authzServer *authz_server.Server
|
||||
consoleWebServer *console_web.Server
|
||||
trustWebServer *trust_web.Server
|
||||
router *chi.Mux
|
||||
extraHeaderFields map[string]string
|
||||
proboService *probo.Service
|
||||
@@ -77,60 +65,29 @@ type Server struct {
|
||||
|
||||
func NewServer(cfg Config) (*Server, error) {
|
||||
apiCfg := api.Config{
|
||||
BaseURL: cfg.BaseURL,
|
||||
AllowedOrigins: cfg.AllowedOrigins,
|
||||
Probo: cfg.Probo,
|
||||
Auth: cfg.Auth,
|
||||
Authz: cfg.Authz,
|
||||
IAM: cfg.IAM,
|
||||
Trust: cfg.Trust,
|
||||
Slack: cfg.Slack,
|
||||
SAML: cfg.SAML,
|
||||
ConsoleAuth: cfg.ConsoleAuth,
|
||||
TrustAuth: cfg.TrustAuth,
|
||||
MCPConfig: cfg.MCPConfig,
|
||||
Cookie: cfg.Cookie,
|
||||
ConnectorRegistry: cfg.ConnectorRegistry,
|
||||
SafeRedirect: cfg.SafeRedirect,
|
||||
CustomDomainCname: cfg.CustomDomainCname,
|
||||
Logger: cfg.Logger.Named("api"),
|
||||
}
|
||||
|
||||
apiServer, err := api.NewServer(apiCfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
webServer, err := web.NewServer()
|
||||
consoleWebServer, err := console_web.NewServer()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
trustServer, err := trust.NewServer()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
authServer, err := auth_server.NewServer(auth_server.Config{
|
||||
Auth: cfg.Auth,
|
||||
Authz: cfg.Authz,
|
||||
SAML: cfg.SAML,
|
||||
CookieName: cfg.ConsoleAuth.CookieName,
|
||||
CookieDomain: cfg.ConsoleAuth.CookieDomain,
|
||||
SessionDuration: cfg.ConsoleAuth.SessionDuration,
|
||||
CookieSecret: cfg.ConsoleAuth.CookieSecret,
|
||||
CookieSecure: cfg.ConsoleAuth.CookieSecure,
|
||||
FileManager: cfg.FileManager,
|
||||
Logger: cfg.Logger.Named("auth"),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
authzServer, err := authz_server.NewServer(authz_server.Config{
|
||||
Auth: cfg.Auth,
|
||||
Authz: cfg.Authz,
|
||||
Logger: cfg.Logger.Named("authz"),
|
||||
CookieName: cfg.ConsoleAuth.CookieName,
|
||||
CookieSecret: cfg.ConsoleAuth.CookieSecret,
|
||||
CookieSecure: cfg.ConsoleAuth.CookieSecure,
|
||||
})
|
||||
trustWebServer, err := trust_web.NewServer()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -139,10 +96,8 @@ func NewServer(cfg Config) (*Server, error) {
|
||||
|
||||
server := &Server{
|
||||
apiServer: apiServer,
|
||||
webServer: webServer,
|
||||
trustServer: trustServer,
|
||||
authServer: authServer,
|
||||
authzServer: authzServer,
|
||||
consoleWebServer: consoleWebServer,
|
||||
trustWebServer: trustWebServer,
|
||||
router: router,
|
||||
extraHeaderFields: cfg.ExtraHeaderFields,
|
||||
proboService: cfg.Probo,
|
||||
@@ -156,8 +111,6 @@ func NewServer(cfg Config) (*Server, error) {
|
||||
|
||||
func (s *Server) setupRoutes() {
|
||||
s.router.Mount("/api", s.apiServer)
|
||||
s.router.Mount("/connect", s.authServer)
|
||||
s.router.Mount("/authz", s.authzServer)
|
||||
|
||||
s.router.Route("/trust/{slugOrId}", func(r chi.Router) {
|
||||
r.Use(s.loadTrustCenterBySlugOrID)
|
||||
@@ -165,7 +118,7 @@ func (s *Server) setupRoutes() {
|
||||
r.Mount("/", s.trustCenterRouter())
|
||||
})
|
||||
|
||||
s.router.Mount("/", s.webServer)
|
||||
s.router.Mount("/", s.consoleWebServer)
|
||||
}
|
||||
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -286,7 +239,6 @@ func (s *Server) loadTrustCenterByDomain(next http.Handler) http.Handler {
|
||||
}
|
||||
|
||||
func (s *Server) addTrustCenterToContext(ctx context.Context, tenantID, organizationID interface{}) context.Context {
|
||||
ctx = context.WithValue(ctx, trust_v1.CustomDomainTenantIDKey, tenantID)
|
||||
ctx = context.WithValue(ctx, trust_v1.CustomDomainOrganizationIDKey, organizationID)
|
||||
return ctx
|
||||
}
|
||||
@@ -313,8 +265,8 @@ func (s *Server) stripTrustPrefix(next http.Handler) http.Handler {
|
||||
func (s *Server) trustCenterRouter() chi.Router {
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Mount("/api/trust/v1", s.apiServer.TrustAPIHandler())
|
||||
r.Handle("/*", s.trustServer)
|
||||
r.Mount("/api/trust/v1", s.apiServer.CompliancePageHandler())
|
||||
r.Handle("/*", s.trustWebServer)
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
// 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 session
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"go.probo.inc/probo/pkg/auth"
|
||||
"go.probo.inc/probo/pkg/authz"
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
"go.probo.inc/probo/pkg/gid"
|
||||
"go.probo.inc/probo/pkg/securecookie"
|
||||
)
|
||||
|
||||
type AuthConfig struct {
|
||||
CookieName string
|
||||
CookieSecret string
|
||||
CookieSecure bool
|
||||
}
|
||||
|
||||
type AuthResult struct {
|
||||
Session *coredata.Session
|
||||
User *coredata.User
|
||||
TenantIDs []gid.TenantID
|
||||
AuthErrors map[gid.TenantID]error // Maps tenant ID to authentication error
|
||||
}
|
||||
|
||||
type ErrorHandler struct {
|
||||
OnCookieError func(err error)
|
||||
OnParseError func(w http.ResponseWriter, authCfg AuthConfig)
|
||||
OnSessionError func(w http.ResponseWriter, authCfg AuthConfig)
|
||||
OnUserError func(w http.ResponseWriter, authCfg AuthConfig)
|
||||
OnTenantError func(err error)
|
||||
}
|
||||
|
||||
func TryAuth(
|
||||
ctx context.Context,
|
||||
w http.ResponseWriter,
|
||||
r *http.Request,
|
||||
authSvc *auth.Service,
|
||||
authzSvc *authz.Service,
|
||||
authCfg AuthConfig,
|
||||
errorHandler ErrorHandler,
|
||||
) *AuthResult {
|
||||
cookieValue, err := securecookie.Get(r, securecookie.DefaultConfig(
|
||||
authCfg.CookieName,
|
||||
authCfg.CookieSecret,
|
||||
authCfg.CookieSecure,
|
||||
))
|
||||
if err != nil {
|
||||
if !errors.Is(err, securecookie.ErrCookieNotFound) && errorHandler.OnCookieError != nil {
|
||||
errorHandler.OnCookieError(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
sessionID, err := gid.ParseGID(cookieValue)
|
||||
if err != nil {
|
||||
if errorHandler.OnParseError != nil {
|
||||
errorHandler.OnParseError(w, authCfg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
session, err := authSvc.GetSession(ctx, sessionID)
|
||||
if err != nil {
|
||||
if errorHandler.OnSessionError != nil {
|
||||
errorHandler.OnSessionError(w, authCfg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
user, err := authSvc.GetUserBySession(ctx, sessionID)
|
||||
if err != nil {
|
||||
if errorHandler.OnUserError != nil {
|
||||
errorHandler.OnUserError(w, authCfg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
organizations, err := authzSvc.GetAllUserOrganizations(ctx, user.ID)
|
||||
if err != nil {
|
||||
if errorHandler.OnTenantError != nil {
|
||||
errorHandler.OnTenantError(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate organization access based on authentication requirements
|
||||
// Only include organizations the user has proper authentication for
|
||||
allowedTenantIDs := make([]gid.TenantID, 0, len(organizations))
|
||||
authErrors := make(map[gid.TenantID]error)
|
||||
|
||||
// Extract organization IDs for batch check
|
||||
orgIDs := make([]gid.GID, len(organizations))
|
||||
for i, org := range organizations {
|
||||
orgIDs[i] = org.ID
|
||||
}
|
||||
|
||||
// Batch check access to all organizations in a single query
|
||||
accessResults, err := authSvc.CheckOrganizationAccess(ctx, user, orgIDs, session)
|
||||
if err != nil {
|
||||
if errorHandler.OnTenantError != nil {
|
||||
errorHandler.OnTenantError(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Process results
|
||||
for _, org := range organizations {
|
||||
result := accessResults[org.ID]
|
||||
if result.Allowed {
|
||||
// User has proper authentication for this org
|
||||
allowedTenantIDs = append(allowedTenantIDs, org.ID.TenantID())
|
||||
} else {
|
||||
// Store the authentication error for later use
|
||||
authErrors[org.ID.TenantID()] = result.ToError(authSvc.BaseURL())
|
||||
}
|
||||
}
|
||||
|
||||
return &AuthResult{
|
||||
Session: session,
|
||||
User: user,
|
||||
TenantIDs: allowedTenantIDs,
|
||||
AuthErrors: authErrors,
|
||||
}
|
||||
}
|
||||
|
||||
func ClearCookie(w http.ResponseWriter, authCfg AuthConfig) {
|
||||
securecookie.Clear(w, securecookie.DefaultConfig(
|
||||
authCfg.CookieName,
|
||||
authCfg.CookieSecret,
|
||||
authCfg.CookieSecure,
|
||||
))
|
||||
}
|
||||
Reference in New Issue
Block a user