package secretsengine import ( "context" "errors" "fmt" "time" "git.tardisproject.uk/tcmal/vault-plugin-kerberos-secrets/config" "github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/logical" ) const ( configStoragePath = "config" defaultCtxTimeout = 1 * time.Minute ) // ConfigPaths extends the Vault API with a `/config` endpoint for the backend. func pathConfig(b *krbBackend) []*framework.Path { return []*framework.Path{&framework.Path{ Pattern: "config", Fields: configSchema(), Operations: map[logical.Operation]framework.OperationHandler{ logical.ReadOperation: &framework.PathOperation{ Callback: b.pathConfigRead, }, logical.CreateOperation: &framework.PathOperation{ Callback: b.pathConfigWrite, }, logical.UpdateOperation: &framework.PathOperation{ Callback: b.pathConfigWrite, }, logical.DeleteOperation: &framework.PathOperation{ Callback: b.pathConfigDelete, }, }, ExistenceCheck: b.pathConfigExistenceCheck, HelpSynopsis: pathConfigHelpSynopsis, HelpDescription: pathConfigHelpDescription, }, } } func configSchema() map[string]*framework.FieldSchema { return map[string]*framework.FieldSchema{ "realm": { Type: framework.TypeString, Description: "The realm to authenticate against", Required: true, DisplayAttrs: &framework.DisplayAttributes{ Name: "Realm", Sensitive: false, }, }, "username": { Type: framework.TypeString, Description: "The username to access kadmin with", Required: true, DisplayAttrs: &framework.DisplayAttributes{ Name: "Username", Sensitive: false, }, }, "password": { Type: framework.TypeString, Description: "The user's password to access kadmin with", Required: true, DisplayAttrs: &framework.DisplayAttributes{ Name: "Password", Sensitive: true, }, }, "kdc": { Type: framework.TypeCommaStringSlice, Description: "Available KDCs for the realm", Required: true, DisplayAttrs: &framework.DisplayAttributes{ Name: "KDCs", Sensitive: false, }, }, "kpasswd_server": { Type: framework.TypeCommaStringSlice, Description: "KPasswd servers for the realm", Required: true, DisplayAttrs: &framework.DisplayAttributes{ Name: "KPasswd Servers", Sensitive: false, }, }, } } // pathConfigExistenceCheck verifies if the configuration exists. func (b *krbBackend) pathConfigExistenceCheck(ctx context.Context, req *logical.Request, data *framework.FieldData) (bool, error) { out, err := req.Storage.Get(ctx, req.Path) if err != nil { return false, fmt.Errorf("existence check failed: %w", err) } return out != nil, nil } // pathConfigRead reads the configuration and outputs non-sensitive information. func (b *krbBackend) pathConfigRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { config, err := getConfig(ctx, req.Storage) if err != nil { return nil, err } if config == nil { return nil, fmt.Errorf("config not yet created") } return &logical.Response{ Data: map[string]interface{}{ "realm": config.Realm, "kdc": config.KDC, "kpasswd_server": config.KPasswdServer, "username": config.Username, }, }, nil } // pathConfigWrite updates the configuration for the backend func (b *krbBackend) pathConfigWrite(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { c, err := getConfig(ctx, req.Storage) if err != nil { return nil, err } createOperation := (req.Operation == logical.CreateOperation) if c == nil { if !createOperation { return nil, errors.New("config not found during update operation") } c = new(config.Config) } if realm, ok := data.GetOk("realm"); ok { c.Realm = realm.(string) } else if !ok && createOperation { return nil, fmt.Errorf("missing realm in configuration") } // TODO: Also validate these aren't empty if kdc, ok := data.GetOk("kdc"); ok { c.KDC = kdc.([]string) } else if !ok && createOperation { return nil, fmt.Errorf("missing KDCs in configuration") } if len(c.KDC) == 0 { return nil, fmt.Errorf("no KDCs specified") } if kpasswd_server, ok := data.GetOk("kpasswd_server"); ok { c.KPasswdServer = kpasswd_server.([]string) } else if !ok && createOperation { return nil, fmt.Errorf("missing kpasswd servers in configuration") } if len(c.KPasswdServer) == 0 { return nil, fmt.Errorf("no kpasswd servers specified") } if username, ok := data.GetOk("username"); ok { c.Username = username.(string) } else if !ok && createOperation { return nil, fmt.Errorf("missing username in configuration") } if password, ok := data.GetOk("password"); ok { c.Password = password.(string) } else if !ok && createOperation { return nil, fmt.Errorf("missing password in configuration") } entry, err := logical.StorageEntryJSON(configStoragePath, c) if err != nil { return nil, err } if err := req.Storage.Put(ctx, entry); err != nil { return nil, err } b.reset() return nil, nil } // pathConfigDelete removes the configuration for the backend func (b *krbBackend) pathConfigDelete(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { err := req.Storage.Delete(ctx, configStoragePath) if err == nil { b.reset() } return nil, err } func getConfig(ctx context.Context, s logical.Storage) (*config.Config, error) { entry, err := s.Get(ctx, configStoragePath) if err != nil { return nil, err } if entry == nil { return nil, nil } c := new(config.Config) if err := entry.DecodeJSON(&c); err != nil { return nil, fmt.Errorf("error reading root configuration: %w", err) } // return the config, we are done return c, nil } // pathConfigHelpSynopsis summarizes the help text for the configuration const pathConfigHelpSynopsis = `Configure the Kerberos backend.` // pathConfigHelpDescription describes the help text for the configuration const pathConfigHelpDescription = ` The Kerberos secret backend requires credentials and connection details for the Kerberos servers. The user provided must be able to create & modify principals in order for this backend to work correctly. `