// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package radius import ( "context" "strings" "github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/helper/tokenutil" "github.com/hashicorp/vault/sdk/logical" ) func pathConfig(b *backend) *framework.Path { p := &framework.Path{ Pattern: "config", DisplayAttrs: &framework.DisplayAttributes{ OperationPrefix: operationPrefixRadius, Action: "Configure", }, Fields: map[string]*framework.FieldSchema{ "host": { Type: framework.TypeString, Description: "RADIUS server host", DisplayAttrs: &framework.DisplayAttributes{ Name: "Host", }, }, "port": { Type: framework.TypeInt, Default: 1812, Description: "RADIUS server port (default: 1812)", DisplayAttrs: &framework.DisplayAttributes{ Value: 1812, }, }, "secret": { Type: framework.TypeString, Description: "Secret shared with the RADIUS server", }, "unregistered_user_policies": { Type: framework.TypeString, Default: "", Description: "Comma-separated list of policies to grant upon successful RADIUS authentication of an unregisted user (default: empty)", DisplayAttrs: &framework.DisplayAttributes{ Name: "Policies for unregistered users", }, }, "dial_timeout": { Type: framework.TypeDurationSecond, Default: 10, Description: "Number of seconds before connect times out (default: 10)", DisplayAttrs: &framework.DisplayAttributes{ Value: 10, }, }, "read_timeout": { Type: framework.TypeDurationSecond, Default: 10, Description: "Number of seconds before response times out (default: 10)", DisplayAttrs: &framework.DisplayAttributes{ Value: 10, }, }, "nas_port": { Type: framework.TypeInt, Default: 10, Description: "RADIUS NAS port field (default: 10)", DisplayAttrs: &framework.DisplayAttributes{ Name: "NAS Port", Value: 10, }, }, "nas_identifier": { Type: framework.TypeString, Default: "", Description: "RADIUS NAS Identifier field (optional)", DisplayAttrs: &framework.DisplayAttributes{ Name: "NAS Identifier", }, }, }, ExistenceCheck: b.configExistenceCheck, Operations: map[logical.Operation]framework.OperationHandler{ logical.ReadOperation: &framework.PathOperation{ Callback: b.pathConfigRead, DisplayAttrs: &framework.DisplayAttributes{ OperationSuffix: "configuration", }, }, logical.CreateOperation: &framework.PathOperation{ Callback: b.pathConfigCreateUpdate, DisplayAttrs: &framework.DisplayAttributes{ OperationVerb: "configure", }, }, logical.UpdateOperation: &framework.PathOperation{ Callback: b.pathConfigCreateUpdate, DisplayAttrs: &framework.DisplayAttributes{ OperationVerb: "configure", }, }, }, HelpSynopsis: pathConfigHelpSyn, HelpDescription: pathConfigHelpDesc, } tokenutil.AddTokenFields(p.Fields) p.Fields["token_policies"].Description += ". This will apply to all tokens generated by this auth method, in addition to any configured for specific users." return p } // Establishes dichotomy of request operation between CreateOperation and UpdateOperation. // Returning 'true' forces an UpdateOperation, CreateOperation otherwise. func (b *backend) configExistenceCheck(ctx context.Context, req *logical.Request, data *framework.FieldData) (bool, error) { entry, err := b.Config(ctx, req) if err != nil { return false, err } return entry != nil, nil } /* * Construct ConfigEntry struct using stored configuration. */ func (b *backend) Config(ctx context.Context, req *logical.Request) (*ConfigEntry, error) { storedConfig, err := req.Storage.Get(ctx, "config") if err != nil { return nil, err } if storedConfig == nil { return nil, nil } var result ConfigEntry if err := storedConfig.DecodeJSON(&result); err != nil { return nil, err } return &result, nil } func (b *backend) pathConfigRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { cfg, err := b.Config(ctx, req) if err != nil { return nil, err } if cfg == nil { return nil, nil } data := map[string]interface{}{ "host": cfg.Host, "port": cfg.Port, "unregistered_user_policies": cfg.UnregisteredUserPolicies, "dial_timeout": cfg.DialTimeout, "read_timeout": cfg.ReadTimeout, "nas_port": cfg.NasPort, "nas_identifier": cfg.NasIdentifier, } cfg.PopulateTokenData(data) return &logical.Response{ Data: data, }, nil } func (b *backend) pathConfigCreateUpdate(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { // Build a ConfigEntry struct out of the supplied FieldData cfg, err := b.Config(ctx, req) if err != nil { return nil, err } if cfg == nil { cfg = &ConfigEntry{} } if err := cfg.ParseTokenFields(req, d); err != nil { return logical.ErrorResponse(err.Error()), logical.ErrInvalidRequest } host, ok := d.GetOk("host") if ok { cfg.Host = strings.ToLower(host.(string)) } else if req.Operation == logical.CreateOperation { cfg.Host = strings.ToLower(d.Get("host").(string)) } if cfg.Host == "" { return logical.ErrorResponse("config parameter `host` cannot be empty"), nil } port, ok := d.GetOk("port") if ok { cfg.Port = port.(int) } else if req.Operation == logical.CreateOperation { cfg.Port = d.Get("port").(int) } secret, ok := d.GetOk("secret") if ok { cfg.Secret = secret.(string) } else if req.Operation == logical.CreateOperation { cfg.Secret = d.Get("secret").(string) } if cfg.Secret == "" { return logical.ErrorResponse("config parameter `secret` cannot be empty"), nil } policies := make([]string, 0) unregisteredUserPoliciesRaw, ok := d.GetOk("unregistered_user_policies") if ok { unregisteredUserPoliciesStr := unregisteredUserPoliciesRaw.(string) if strings.TrimSpace(unregisteredUserPoliciesStr) != "" { policies = strings.Split(unregisteredUserPoliciesStr, ",") for _, policy := range policies { if policy == "root" { return logical.ErrorResponse("root policy cannot be granted by an auth method"), nil } } } cfg.UnregisteredUserPolicies = policies } else if req.Operation == logical.CreateOperation { cfg.UnregisteredUserPolicies = policies } dialTimeout, ok := d.GetOk("dial_timeout") if ok { cfg.DialTimeout = dialTimeout.(int) } else if req.Operation == logical.CreateOperation { cfg.DialTimeout = d.Get("dial_timeout").(int) } readTimeout, ok := d.GetOk("read_timeout") if ok { cfg.ReadTimeout = readTimeout.(int) } else if req.Operation == logical.CreateOperation { cfg.ReadTimeout = d.Get("read_timeout").(int) } nasPort, ok := d.GetOk("nas_port") if ok { cfg.NasPort = nasPort.(int) } else if req.Operation == logical.CreateOperation { cfg.NasPort = d.Get("nas_port").(int) } nasIdentifier, ok := d.GetOk("nas_identifier") if ok { cfg.NasIdentifier = nasIdentifier.(string) } else if req.Operation == logical.CreateOperation { cfg.NasIdentifier = d.Get("nas_identifier").(string) } entry, err := logical.StorageEntryJSON("config", cfg) if err != nil { return nil, err } if err := req.Storage.Put(ctx, entry); err != nil { return nil, err } return nil, nil } type ConfigEntry struct { tokenutil.TokenParams Host string `json:"host" structs:"host" mapstructure:"host"` Port int `json:"port" structs:"port" mapstructure:"port"` Secret string `json:"secret" structs:"secret" mapstructure:"secret"` UnregisteredUserPolicies []string `json:"unregistered_user_policies" structs:"unregistered_user_policies" mapstructure:"unregistered_user_policies"` DialTimeout int `json:"dial_timeout" structs:"dial_timeout" mapstructure:"dial_timeout"` ReadTimeout int `json:"read_timeout" structs:"read_timeout" mapstructure:"read_timeout"` NasPort int `json:"nas_port" structs:"nas_port" mapstructure:"nas_port"` NasIdentifier string `json:"nas_identifier" structs:"nas_identifier" mapstructure:"nas_identifier"` } const pathConfigHelpSyn = ` Configure the RADIUS server to connect to, along with its options. ` const pathConfigHelpDesc = ` This endpoint allows you to configure the RADIUS server to connect to and its configuration options. `