mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-11 16:09:07 +02:00
Add Zitadel IdP
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
)
|
||||
|
||||
// setFlagsFromEnvVars reads and updates flag values from environment variables with prefix NB_IDP_
|
||||
func setFlagsFromEnvVars(cmd *cobra.Command) {
|
||||
flags := cmd.PersistentFlags()
|
||||
flags.VisitAll(func(f *pflag.Flag) {
|
||||
newEnvVar := flagNameToEnvVar(f.Name, "NB_IDP_")
|
||||
value, present := os.LookupEnv(newEnvVar)
|
||||
if !present {
|
||||
return
|
||||
}
|
||||
|
||||
err := flags.Set(f.Name, value)
|
||||
if err != nil {
|
||||
log.Infof("unable to configure flag %s using variable %s, err: %v", f.Name, newEnvVar, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// flagNameToEnvVar converts flag name to environment var name adding a prefix,
|
||||
// replacing dashes and making all uppercase (e.g. data-dir is converted to NB_IDP_DATA_DIR)
|
||||
func flagNameToEnvVar(cmdFlag string, prefix string) string {
|
||||
parsed := strings.ReplaceAll(cmdFlag, "-", "_")
|
||||
upper := strings.ToUpper(parsed)
|
||||
return prefix + upper
|
||||
}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/netbirdio/netbird/idp/oidcprovider"
|
||||
"github.com/netbirdio/netbird/util"
|
||||
)
|
||||
|
||||
// Config holds the IdP server configuration
|
||||
type Config struct {
|
||||
ListenPort int
|
||||
Issuer string
|
||||
DataDir string
|
||||
LogLevel string
|
||||
LogFile string
|
||||
DevMode bool
|
||||
DashboardRedirectURIs []string
|
||||
CLIRedirectURIs []string
|
||||
DashboardClientID string
|
||||
CLIClientID string
|
||||
}
|
||||
|
||||
var (
|
||||
config *Config
|
||||
rootCmd = &cobra.Command{
|
||||
Use: "idp",
|
||||
Short: "NetBird Identity Provider",
|
||||
Long: "Embedded OIDC Identity Provider for NetBird",
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
RunE: execute,
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
_ = util.InitLog("trace", util.LogConsole)
|
||||
config = &Config{}
|
||||
|
||||
rootCmd.PersistentFlags().IntVarP(&config.ListenPort, "port", "p", 33081, "port to listen on")
|
||||
rootCmd.PersistentFlags().StringVarP(&config.Issuer, "issuer", "i", "", "OIDC issuer URL (default: http://localhost:<port>)")
|
||||
rootCmd.PersistentFlags().StringVarP(&config.DataDir, "data-dir", "d", "/var/lib/netbird", "directory to store IdP data")
|
||||
rootCmd.PersistentFlags().StringVar(&config.LogLevel, "log-level", "info", "log level (trace, debug, info, warn, error)")
|
||||
rootCmd.PersistentFlags().StringVar(&config.LogFile, "log-file", "console", "log file path or 'console'")
|
||||
rootCmd.PersistentFlags().BoolVar(&config.DevMode, "dev-mode", false, "enable development mode (allows HTTP)")
|
||||
rootCmd.PersistentFlags().StringSliceVar(&config.DashboardRedirectURIs, "dashboard-redirect-uris", []string{
|
||||
"http://localhost:3000/callback",
|
||||
"http://localhost:3000/silent-callback",
|
||||
}, "allowed redirect URIs for dashboard client")
|
||||
rootCmd.PersistentFlags().StringSliceVar(&config.CLIRedirectURIs, "cli-redirect-uris", []string{
|
||||
"http://localhost:53000",
|
||||
"http://localhost:54000",
|
||||
}, "allowed redirect URIs for CLI client")
|
||||
rootCmd.PersistentFlags().StringVar(&config.DashboardClientID, "dashboard-client-id", "netbird-dashboard", "client ID for dashboard")
|
||||
rootCmd.PersistentFlags().StringVar(&config.CLIClientID, "cli-client-id", "netbird-client", "client ID for CLI")
|
||||
|
||||
// Add subcommands
|
||||
rootCmd.AddCommand(userCmd)
|
||||
|
||||
setFlagsFromEnvVars(rootCmd)
|
||||
}
|
||||
|
||||
// Execute runs the root command
|
||||
func Execute() error {
|
||||
return rootCmd.Execute()
|
||||
}
|
||||
|
||||
func execute(cmd *cobra.Command, args []string) error {
|
||||
err := util.InitLog(config.LogLevel, config.LogFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to initialize log: %s", err)
|
||||
}
|
||||
|
||||
// Set default issuer if not provided
|
||||
issuer := config.Issuer
|
||||
if issuer == "" {
|
||||
issuer = fmt.Sprintf("http://localhost:%d", config.ListenPort)
|
||||
}
|
||||
|
||||
log.Infof("Starting NetBird Identity Provider")
|
||||
log.Infof(" Port: %d", config.ListenPort)
|
||||
log.Infof(" Issuer: %s", issuer)
|
||||
log.Infof(" Data directory: %s", config.DataDir)
|
||||
log.Infof(" Dev mode: %v", config.DevMode)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
// Create provider config
|
||||
providerConfig := &oidcprovider.Config{
|
||||
Issuer: issuer,
|
||||
Port: config.ListenPort,
|
||||
DataDir: config.DataDir,
|
||||
DevMode: config.DevMode,
|
||||
}
|
||||
|
||||
// Create the provider
|
||||
provider, err := oidcprovider.NewProvider(ctx, providerConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create IdP: %w", err)
|
||||
}
|
||||
|
||||
// Ensure default clients exist
|
||||
if err := provider.EnsureDefaultClients(ctx, config.DashboardRedirectURIs, config.CLIRedirectURIs); err != nil {
|
||||
return fmt.Errorf("failed to create default clients: %w", err)
|
||||
}
|
||||
|
||||
// Start the provider
|
||||
if err := provider.Start(ctx); err != nil {
|
||||
return fmt.Errorf("failed to start IdP: %w", err)
|
||||
}
|
||||
|
||||
log.Infof("IdP is running")
|
||||
log.Infof(" Discovery: %s/.well-known/openid-configuration", issuer)
|
||||
log.Infof(" Authorization: %s/authorize", issuer)
|
||||
log.Infof(" Token: %s/oauth/token", issuer)
|
||||
log.Infof(" Device authorization: %s/device_authorization", issuer)
|
||||
log.Infof(" JWKS: %s/keys", issuer)
|
||||
log.Infof(" Login: %s/login", issuer)
|
||||
log.Infof(" Device flow: %s/device", issuer)
|
||||
|
||||
// Wait for exit signal
|
||||
waitForExitSignal()
|
||||
|
||||
log.Infof("Shutting down IdP...")
|
||||
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 10)
|
||||
defer shutdownCancel()
|
||||
|
||||
if err := provider.Stop(shutdownCtx); err != nil {
|
||||
return fmt.Errorf("failed to stop IdP: %w", err)
|
||||
}
|
||||
|
||||
log.Infof("IdP stopped")
|
||||
return nil
|
||||
}
|
||||
|
||||
func waitForExitSignal() {
|
||||
osSigs := make(chan os.Signal, 1)
|
||||
signal.Notify(osSigs, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-osSigs
|
||||
}
|
||||
+249
@@ -0,0 +1,249 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"syscall"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"golang.org/x/term"
|
||||
|
||||
"github.com/netbirdio/netbird/idp/oidcprovider"
|
||||
)
|
||||
|
||||
var userCmd = &cobra.Command{
|
||||
Use: "user",
|
||||
Short: "Manage IdP users",
|
||||
Long: "Commands for managing users in the embedded IdP",
|
||||
}
|
||||
|
||||
var userAddCmd = &cobra.Command{
|
||||
Use: "add",
|
||||
Short: "Add a new user",
|
||||
Long: "Add a new user to the embedded IdP",
|
||||
RunE: userAdd,
|
||||
}
|
||||
|
||||
var userListCmd = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List all users",
|
||||
Long: "List all users in the embedded IdP",
|
||||
RunE: userList,
|
||||
}
|
||||
|
||||
var userDeleteCmd = &cobra.Command{
|
||||
Use: "delete <username>",
|
||||
Short: "Delete a user",
|
||||
Long: "Delete a user from the embedded IdP",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: userDelete,
|
||||
}
|
||||
|
||||
var userPasswordCmd = &cobra.Command{
|
||||
Use: "password <username>",
|
||||
Short: "Change user password",
|
||||
Long: "Change password for a user in the embedded IdP",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: userChangePassword,
|
||||
}
|
||||
|
||||
// User add flags
|
||||
var (
|
||||
userUsername string
|
||||
userEmail string
|
||||
userFirstName string
|
||||
userLastName string
|
||||
userPassword string
|
||||
)
|
||||
|
||||
func init() {
|
||||
userAddCmd.Flags().StringVarP(&userUsername, "username", "u", "", "username (required)")
|
||||
userAddCmd.Flags().StringVarP(&userEmail, "email", "e", "", "email address (required)")
|
||||
userAddCmd.Flags().StringVarP(&userFirstName, "first-name", "f", "", "first name")
|
||||
userAddCmd.Flags().StringVarP(&userLastName, "last-name", "l", "", "last name")
|
||||
userAddCmd.Flags().StringVarP(&userPassword, "password", "p", "", "password (will prompt if not provided)")
|
||||
_ = userAddCmd.MarkFlagRequired("username")
|
||||
_ = userAddCmd.MarkFlagRequired("email")
|
||||
|
||||
userCmd.AddCommand(userAddCmd)
|
||||
userCmd.AddCommand(userListCmd)
|
||||
userCmd.AddCommand(userDeleteCmd)
|
||||
userCmd.AddCommand(userPasswordCmd)
|
||||
}
|
||||
|
||||
func getStore() (*oidcprovider.Store, error) {
|
||||
ctx := context.Background()
|
||||
store, err := oidcprovider.NewStore(ctx, config.DataDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open store: %w", err)
|
||||
}
|
||||
return store, nil
|
||||
}
|
||||
|
||||
func userAdd(cmd *cobra.Command, args []string) error {
|
||||
store, err := getStore()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
password := userPassword
|
||||
if password == "" {
|
||||
// Prompt for password
|
||||
fmt.Print("Enter password: ")
|
||||
bytePassword, err := term.ReadPassword(int(syscall.Stdin))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read password: %w", err)
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
fmt.Print("Confirm password: ")
|
||||
byteConfirm, err := term.ReadPassword(int(syscall.Stdin))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read password confirmation: %w", err)
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
if string(bytePassword) != string(byteConfirm) {
|
||||
return fmt.Errorf("passwords do not match")
|
||||
}
|
||||
password = string(bytePassword)
|
||||
}
|
||||
|
||||
if password == "" {
|
||||
return fmt.Errorf("password cannot be empty")
|
||||
}
|
||||
|
||||
user := &oidcprovider.User{
|
||||
Username: userUsername,
|
||||
Email: userEmail,
|
||||
FirstName: userFirstName,
|
||||
LastName: userLastName,
|
||||
Password: password,
|
||||
EmailVerified: true, // Mark as verified since admin is creating the user
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
if err := store.CreateUser(ctx, user); err != nil {
|
||||
return fmt.Errorf("failed to create user: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("User '%s' created successfully (ID: %s)\n", userUsername, user.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func userList(cmd *cobra.Command, args []string) error {
|
||||
store, err := getStore()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
users, err := store.ListUsers(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to list users: %w", err)
|
||||
}
|
||||
|
||||
if len(users) == 0 {
|
||||
fmt.Println("No users found")
|
||||
return nil
|
||||
}
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
|
||||
fmt.Fprintln(w, "ID\tUSERNAME\tEMAIL\tNAME\tVERIFIED\tCREATED")
|
||||
for _, user := range users {
|
||||
name := fmt.Sprintf("%s %s", user.FirstName, user.LastName)
|
||||
verified := "No"
|
||||
if user.EmailVerified {
|
||||
verified = "Yes"
|
||||
}
|
||||
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n",
|
||||
user.ID,
|
||||
user.Username,
|
||||
user.Email,
|
||||
name,
|
||||
verified,
|
||||
user.CreatedAt.Format("2006-01-02 15:04"),
|
||||
)
|
||||
}
|
||||
w.Flush()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func userDelete(cmd *cobra.Command, args []string) error {
|
||||
username := args[0]
|
||||
|
||||
store, err := getStore()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Find user by username
|
||||
user, err := store.GetUserByUsername(ctx, username)
|
||||
if err != nil {
|
||||
return fmt.Errorf("user '%s' not found", username)
|
||||
}
|
||||
|
||||
if err := store.DeleteUser(ctx, user.ID); err != nil {
|
||||
return fmt.Errorf("failed to delete user: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("User '%s' deleted successfully\n", username)
|
||||
return nil
|
||||
}
|
||||
|
||||
func userChangePassword(cmd *cobra.Command, args []string) error {
|
||||
username := args[0]
|
||||
|
||||
store, err := getStore()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer store.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Find user by username
|
||||
user, err := store.GetUserByUsername(ctx, username)
|
||||
if err != nil {
|
||||
return fmt.Errorf("user '%s' not found", username)
|
||||
}
|
||||
|
||||
// Prompt for new password
|
||||
fmt.Print("Enter new password: ")
|
||||
bytePassword, err := term.ReadPassword(int(syscall.Stdin))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read password: %w", err)
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
fmt.Print("Confirm new password: ")
|
||||
byteConfirm, err := term.ReadPassword(int(syscall.Stdin))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read password confirmation: %w", err)
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
if string(bytePassword) != string(byteConfirm) {
|
||||
return fmt.Errorf("passwords do not match")
|
||||
}
|
||||
|
||||
password := string(bytePassword)
|
||||
if password == "" {
|
||||
return fmt.Errorf("password cannot be empty")
|
||||
}
|
||||
|
||||
if err := store.UpdateUserPassword(ctx, user.ID, password); err != nil {
|
||||
return fmt.Errorf("failed to update password: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Password updated for user '%s'\n", username)
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user