mirror of
https://github.com/netbirdio/netbird.git
synced 2026-04-16 15:26:40 +00:00
304 lines
10 KiB
Go
304 lines
10 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/dexidp/dex/storage"
|
|
"github.com/rs/xid"
|
|
log "github.com/sirupsen/logrus"
|
|
|
|
"github.com/netbirdio/netbird/idp/dex"
|
|
"github.com/netbirdio/netbird/management/server/activity"
|
|
"github.com/netbirdio/netbird/management/server/idp"
|
|
"github.com/netbirdio/netbird/management/server/permissions/modules"
|
|
"github.com/netbirdio/netbird/management/server/permissions/operations"
|
|
"github.com/netbirdio/netbird/management/server/types"
|
|
"github.com/netbirdio/netbird/shared/management/status"
|
|
)
|
|
|
|
// oidcProviderJSON represents the OpenID Connect discovery document
|
|
type oidcProviderJSON struct {
|
|
Issuer string `json:"issuer"`
|
|
}
|
|
|
|
// validateOIDCIssuer validates the OIDC issuer by fetching the OpenID configuration
|
|
// and verifying that the returned issuer matches the configured one.
|
|
func validateOIDCIssuer(ctx context.Context, issuer string) error {
|
|
wellKnown := strings.TrimSuffix(issuer, "/") + "/.well-known/openid-configuration"
|
|
|
|
httpClient := &http.Client{
|
|
Timeout: 10 * time.Second,
|
|
}
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, wellKnown, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %v", types.ErrIdentityProviderIssuerUnreachable, err)
|
|
}
|
|
|
|
resp, err := httpClient.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("%w: %v", types.ErrIdentityProviderIssuerUnreachable, err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return fmt.Errorf("%w: unable to read response body: %v", types.ErrIdentityProviderIssuerUnreachable, err)
|
|
}
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
return fmt.Errorf("%w: %s: %s", types.ErrIdentityProviderIssuerUnreachable, resp.Status, body)
|
|
}
|
|
|
|
var p oidcProviderJSON
|
|
if err := json.Unmarshal(body, &p); err != nil {
|
|
return fmt.Errorf("%w: failed to decode provider discovery object: %v", types.ErrIdentityProviderIssuerUnreachable, err)
|
|
}
|
|
|
|
if p.Issuer != issuer {
|
|
return fmt.Errorf("%w: expected %q got %q", types.ErrIdentityProviderIssuerMismatch, issuer, p.Issuer)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateIdentityProviderConfig validates the identity provider configuration including
|
|
// basic validation and OIDC issuer verification.
|
|
func validateIdentityProviderConfig(ctx context.Context, idpConfig *types.IdentityProvider) error {
|
|
if err := idpConfig.Validate(); err != nil {
|
|
return status.Errorf(status.InvalidArgument, "%s", err.Error())
|
|
}
|
|
|
|
// Validate the issuer by calling the OIDC discovery endpoint
|
|
if idpConfig.Issuer != "" {
|
|
if err := validateOIDCIssuer(ctx, idpConfig.Issuer); err != nil {
|
|
return status.Errorf(status.InvalidArgument, "%s", err.Error())
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetIdentityProviders returns all identity providers for an account
|
|
func (am *DefaultAccountManager) GetIdentityProviders(ctx context.Context, accountID, userID string) ([]*types.IdentityProvider, error) {
|
|
ok, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.IdentityProviders, operations.Read)
|
|
if err != nil {
|
|
return nil, status.NewPermissionValidationError(err)
|
|
}
|
|
if !ok {
|
|
return nil, status.NewPermissionDeniedError()
|
|
}
|
|
|
|
embeddedManager, ok := am.idpManager.(*idp.EmbeddedIdPManager)
|
|
if !ok {
|
|
log.Warn("identity provider management requires embedded IdP")
|
|
return []*types.IdentityProvider{}, nil
|
|
}
|
|
|
|
connectors, err := embeddedManager.ListConnectors(ctx)
|
|
if err != nil {
|
|
return nil, status.Errorf(status.Internal, "failed to list identity providers: %v", err)
|
|
}
|
|
|
|
result := make([]*types.IdentityProvider, 0, len(connectors))
|
|
for _, conn := range connectors {
|
|
result = append(result, connectorConfigToIdentityProvider(conn, accountID))
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// GetIdentityProvider returns a specific identity provider by ID
|
|
func (am *DefaultAccountManager) GetIdentityProvider(ctx context.Context, accountID, idpID, userID string) (*types.IdentityProvider, error) {
|
|
ok, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.IdentityProviders, operations.Read)
|
|
if err != nil {
|
|
return nil, status.NewPermissionValidationError(err)
|
|
}
|
|
if !ok {
|
|
return nil, status.NewPermissionDeniedError()
|
|
}
|
|
|
|
embeddedManager, ok := am.idpManager.(*idp.EmbeddedIdPManager)
|
|
if !ok {
|
|
return nil, status.Errorf(status.Internal, "identity provider management requires embedded IdP")
|
|
}
|
|
|
|
conn, err := embeddedManager.GetConnector(ctx, idpID)
|
|
if err != nil {
|
|
if errors.Is(err, storage.ErrNotFound) {
|
|
return nil, status.Errorf(status.NotFound, "identity provider not found")
|
|
}
|
|
return nil, status.Errorf(status.Internal, "failed to get identity provider: %v", err)
|
|
}
|
|
|
|
return connectorConfigToIdentityProvider(conn, accountID), nil
|
|
}
|
|
|
|
// CreateIdentityProvider creates a new identity provider
|
|
func (am *DefaultAccountManager) CreateIdentityProvider(ctx context.Context, accountID, userID string, idpConfig *types.IdentityProvider) (*types.IdentityProvider, error) {
|
|
ok, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.IdentityProviders, operations.Create)
|
|
if err != nil {
|
|
return nil, status.NewPermissionValidationError(err)
|
|
}
|
|
if !ok {
|
|
return nil, status.NewPermissionDeniedError()
|
|
}
|
|
|
|
if err := validateIdentityProviderConfig(ctx, idpConfig); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
embeddedManager, ok := am.idpManager.(*idp.EmbeddedIdPManager)
|
|
if !ok {
|
|
return nil, status.Errorf(status.Internal, "identity provider management requires embedded IdP")
|
|
}
|
|
|
|
// Generate ID if not provided
|
|
if idpConfig.ID == "" {
|
|
idpConfig.ID = generateIdentityProviderID(idpConfig.Type)
|
|
}
|
|
idpConfig.AccountID = accountID
|
|
|
|
connCfg := identityProviderToConnectorConfig(idpConfig)
|
|
|
|
_, err = embeddedManager.CreateConnector(ctx, connCfg)
|
|
if err != nil {
|
|
return nil, status.Errorf(status.Internal, "failed to create identity provider: %v", err)
|
|
}
|
|
|
|
am.StoreEvent(ctx, userID, idpConfig.ID, accountID, activity.IdentityProviderCreated, idpConfig.EventMeta())
|
|
|
|
return idpConfig, nil
|
|
}
|
|
|
|
// UpdateIdentityProvider updates an existing identity provider
|
|
func (am *DefaultAccountManager) UpdateIdentityProvider(ctx context.Context, accountID, idpID, userID string, idpConfig *types.IdentityProvider) (*types.IdentityProvider, error) {
|
|
ok, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.IdentityProviders, operations.Update)
|
|
if err != nil {
|
|
return nil, status.NewPermissionValidationError(err)
|
|
}
|
|
if !ok {
|
|
return nil, status.NewPermissionDeniedError()
|
|
}
|
|
|
|
if err := validateIdentityProviderConfig(ctx, idpConfig); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
embeddedManager, ok := am.idpManager.(*idp.EmbeddedIdPManager)
|
|
if !ok {
|
|
return nil, status.Errorf(status.Internal, "identity provider management requires embedded IdP")
|
|
}
|
|
|
|
idpConfig.ID = idpID
|
|
idpConfig.AccountID = accountID
|
|
|
|
connCfg := identityProviderToConnectorConfig(idpConfig)
|
|
|
|
if err := embeddedManager.UpdateConnector(ctx, connCfg); err != nil {
|
|
return nil, status.Errorf(status.Internal, "failed to update identity provider: %v", err)
|
|
}
|
|
|
|
am.StoreEvent(ctx, userID, idpConfig.ID, accountID, activity.IdentityProviderUpdated, idpConfig.EventMeta())
|
|
|
|
return idpConfig, nil
|
|
}
|
|
|
|
// DeleteIdentityProvider deletes an identity provider
|
|
func (am *DefaultAccountManager) DeleteIdentityProvider(ctx context.Context, accountID, idpID, userID string) error {
|
|
ok, err := am.permissionsManager.ValidateUserPermissions(ctx, accountID, userID, modules.IdentityProviders, operations.Delete)
|
|
if err != nil {
|
|
return status.NewPermissionValidationError(err)
|
|
}
|
|
if !ok {
|
|
return status.NewPermissionDeniedError()
|
|
}
|
|
|
|
embeddedManager, ok := am.idpManager.(*idp.EmbeddedIdPManager)
|
|
if !ok {
|
|
return status.Errorf(status.Internal, "identity provider management requires embedded IdP")
|
|
}
|
|
|
|
// Get the IDP info before deleting for the activity event
|
|
conn, err := embeddedManager.GetConnector(ctx, idpID)
|
|
if err != nil {
|
|
if errors.Is(err, storage.ErrNotFound) {
|
|
return status.Errorf(status.NotFound, "identity provider not found")
|
|
}
|
|
return status.Errorf(status.Internal, "failed to get identity provider: %v", err)
|
|
}
|
|
idpConfig := connectorConfigToIdentityProvider(conn, accountID)
|
|
|
|
if err := embeddedManager.DeleteConnector(ctx, idpID); err != nil {
|
|
if errors.Is(err, storage.ErrNotFound) {
|
|
return status.Errorf(status.NotFound, "identity provider not found")
|
|
}
|
|
return status.Errorf(status.Internal, "failed to delete identity provider: %v", err)
|
|
}
|
|
|
|
am.StoreEvent(ctx, userID, idpID, accountID, activity.IdentityProviderDeleted, idpConfig.EventMeta())
|
|
|
|
return nil
|
|
}
|
|
|
|
// connectorConfigToIdentityProvider converts a dex.ConnectorConfig to types.IdentityProvider
|
|
func connectorConfigToIdentityProvider(conn *dex.ConnectorConfig, accountID string) *types.IdentityProvider {
|
|
return &types.IdentityProvider{
|
|
ID: conn.ID,
|
|
AccountID: accountID,
|
|
Type: types.IdentityProviderType(conn.Type),
|
|
Name: conn.Name,
|
|
Issuer: conn.Issuer,
|
|
ClientID: conn.ClientID,
|
|
ClientSecret: conn.ClientSecret,
|
|
}
|
|
}
|
|
|
|
// identityProviderToConnectorConfig converts a types.IdentityProvider to dex.ConnectorConfig
|
|
func identityProviderToConnectorConfig(idpConfig *types.IdentityProvider) *dex.ConnectorConfig {
|
|
return &dex.ConnectorConfig{
|
|
ID: idpConfig.ID,
|
|
Name: idpConfig.Name,
|
|
Type: string(idpConfig.Type),
|
|
Issuer: idpConfig.Issuer,
|
|
ClientID: idpConfig.ClientID,
|
|
ClientSecret: idpConfig.ClientSecret,
|
|
}
|
|
}
|
|
|
|
// generateIdentityProviderID generates a unique ID for an identity provider.
|
|
// For specific provider types (okta, zitadel, entra, google, pocketid, microsoft),
|
|
// the ID is prefixed with the type name. Generic OIDC providers get no prefix.
|
|
func generateIdentityProviderID(idpType types.IdentityProviderType) string {
|
|
id := xid.New().String()
|
|
|
|
switch idpType {
|
|
case types.IdentityProviderTypeOkta:
|
|
return "okta-" + id
|
|
case types.IdentityProviderTypeZitadel:
|
|
return "zitadel-" + id
|
|
case types.IdentityProviderTypeEntra:
|
|
return "entra-" + id
|
|
case types.IdentityProviderTypeGoogle:
|
|
return "google-" + id
|
|
case types.IdentityProviderTypePocketID:
|
|
return "pocketid-" + id
|
|
case types.IdentityProviderTypeMicrosoft:
|
|
return "microsoft-" + id
|
|
case types.IdentityProviderTypeAuthentik:
|
|
return "authentik-" + id
|
|
case types.IdentityProviderTypeKeycloak:
|
|
return "keycloak-" + id
|
|
default:
|
|
// Generic OIDC - no prefix
|
|
return id
|
|
}
|
|
}
|