1package openldap 2 3import ( 4 "context" 5 "encoding/base64" 6 "fmt" 7 "path" 8 "time" 9 10 "github.com/go-ldap/ldif" 11 "github.com/hashicorp/go-multierror" 12 "github.com/hashicorp/vault/sdk/framework" 13 "github.com/hashicorp/vault/sdk/helper/parseutil" 14 "github.com/hashicorp/vault/sdk/logical" 15 "github.com/mitchellh/mapstructure" 16) 17 18const ( 19 secretCredsType = "creds" 20 21 dynamicRolePath = "role/" 22 dynamicCredPath = "creds/" 23) 24 25func (b *backend) pathDynamicRoles() []*framework.Path { 26 return []*framework.Path{ 27 // POST/GET/DELETE role/:name 28 { 29 Pattern: path.Join(dynamicRolePath, framework.GenericNameRegex("name")), 30 Fields: map[string]*framework.FieldSchema{ 31 "name": { 32 Type: framework.TypeLowerCaseString, 33 Description: "Name of the role (lowercase)", 34 Required: true, 35 }, 36 "creation_ldif": { 37 Type: framework.TypeString, 38 Description: "LDIF string used to create new entities within OpenLDAP. This LDIF can be templated.", 39 Required: true, 40 }, 41 "deletion_ldif": { 42 Type: framework.TypeString, 43 Description: "LDIF string used to delete entities created within OpenLDAP. This LDIF can be templated.", 44 Required: true, 45 }, 46 "rollback_ldif": { 47 Type: framework.TypeString, 48 Description: "LDIF string used to rollback changes in the event of a failure to create credentials. This LDIF can be templated.", 49 }, 50 "username_template": { 51 Type: framework.TypeString, 52 Description: "The template used to create a username", 53 }, 54 "default_ttl": { 55 Type: framework.TypeDurationSecond, 56 Description: "Default TTL for dynamic credentials", 57 }, 58 "max_ttl": { 59 Type: framework.TypeDurationSecond, 60 Description: "Max TTL a dynamic credential can be extended to", 61 }, 62 }, 63 ExistenceCheck: b.pathDynamicRoleExistenceCheck, 64 Operations: map[logical.Operation]framework.OperationHandler{ 65 logical.UpdateOperation: &framework.PathOperation{ 66 Callback: b.pathDynamicRoleCreateUpdate, 67 }, 68 logical.CreateOperation: &framework.PathOperation{ 69 Callback: b.pathDynamicRoleCreateUpdate, 70 }, 71 logical.ReadOperation: &framework.PathOperation{ 72 Callback: b.pathDynamicRoleRead, 73 }, 74 logical.DeleteOperation: &framework.PathOperation{ 75 Callback: b.pathDynamicRoleDelete, 76 }, 77 }, 78 HelpSynopsis: staticRoleHelpSynopsis, 79 HelpDescription: staticRoleHelpDescription, 80 }, 81 // LIST role 82 { 83 Pattern: dynamicRolePath + "?$", 84 Operations: map[logical.Operation]framework.OperationHandler{ 85 logical.ListOperation: &framework.PathOperation{ 86 Callback: b.pathDynamicRoleList, 87 }, 88 }, 89 HelpSynopsis: "List all the dynamic roles Vault is currently managing in OpenLDAP.", 90 HelpDescription: "List all the dynamic roles being managed by Vault.", 91 }, 92 // GET credentials 93 { 94 Pattern: path.Join(dynamicCredPath, framework.MatchAllRegex("name")), 95 Fields: map[string]*framework.FieldSchema{ 96 "name": { 97 Type: framework.TypeLowerCaseString, 98 Description: "Name of the dynamic role.", 99 }, 100 }, 101 Operations: map[logical.Operation]framework.OperationHandler{ 102 logical.ReadOperation: &framework.PathOperation{ 103 Callback: b.pathDynamicCredsRead, 104 }, 105 }, 106 HelpSynopsis: "Request LDAP credentials for a dynamic role. These credentials are " + 107 "created within OpenLDAP when querying this endpoint.", 108 HelpDescription: "This path requests new LDAP credentials for a certain dynamic role. " + 109 "The credentials are created within OpenLDAP based on the creation_ldif specified " + 110 "within the dynamic role configuration.", 111 }, 112 } 113} 114 115func dynamicSecretCreds(b *backend) *framework.Secret { 116 return &framework.Secret{ 117 Type: secretCredsType, 118 Fields: map[string]*framework.FieldSchema{ 119 "username": { 120 Type: framework.TypeString, 121 Description: "Username of the generated account", 122 }, 123 "password": { 124 Type: framework.TypeString, 125 Description: "Password to access the generated account", 126 }, 127 "distinguished_names": { 128 Type: framework.TypeStringSlice, 129 Description: "List of the distinguished names (DN) created. Each name in this list corresponds to" + 130 "each action taken within the creation_ldif statements. This does not de-duplicate entries, " + 131 "so this will have one entry for each LDIF statement within creation_ldif.", 132 }, 133 }, 134 135 Renew: b.secretCredsRenew(), 136 Revoke: b.secretCredsRevoke(), 137 } 138} 139 140func (b *backend) pathDynamicRoleCreateUpdate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 141 rawData := data.Raw 142 err := convertToDuration(rawData, "default_ttl", "max_ttl") 143 if err != nil { 144 return nil, fmt.Errorf("failed to convert TTLs to duration: %w", err) 145 } 146 147 roleName := data.Get("name").(string) 148 dRole, err := retrieveDynamicRole(ctx, req.Storage, roleName) 149 if err != nil { 150 return nil, fmt.Errorf("unable to look for existing role: %w", err) 151 } 152 if dRole == nil { 153 if req.Operation == logical.UpdateOperation { 154 return nil, fmt.Errorf("unable to update role: role does not exist") 155 } 156 dRole = &dynamicRole{} 157 } 158 err = mapstructure.WeakDecode(rawData, dRole) 159 if err != nil { 160 return nil, fmt.Errorf("failed to decode request: %w", err) 161 } 162 163 dRole.CreationLDIF = decodeBase64(dRole.CreationLDIF) 164 dRole.RollbackLDIF = decodeBase64(dRole.RollbackLDIF) 165 dRole.DeletionLDIF = decodeBase64(dRole.DeletionLDIF) 166 167 err = validateDynamicRole(dRole) 168 if err != nil { 169 return nil, err 170 } 171 172 err = storeDynamicRole(ctx, req.Storage, dRole) 173 if err != nil { 174 return nil, fmt.Errorf("failed to save dynamic role: %w", err) 175 } 176 177 return nil, nil 178} 179 180func validateDynamicRole(dRole *dynamicRole) error { 181 if dRole.CreationLDIF == "" { 182 return fmt.Errorf("missing creation_ldif") 183 } 184 185 if dRole.DeletionLDIF == "" { 186 return fmt.Errorf("missing deletion_ldif") 187 } 188 189 err := assertValidLDIFTemplate(dRole.CreationLDIF) 190 if err != nil { 191 return fmt.Errorf("invalid creation_ldif: %w", err) 192 } 193 194 err = assertValidLDIFTemplate(dRole.DeletionLDIF) 195 if err != nil { 196 return fmt.Errorf("invalid deletion_ldif: %w", err) 197 } 198 199 if dRole.RollbackLDIF != "" { 200 err = assertValidLDIFTemplate(dRole.RollbackLDIF) 201 if err != nil { 202 return fmt.Errorf("invalid rollback_ldif: %w", err) 203 } 204 } 205 206 return nil 207} 208 209// convertToDuration all keys in the data map into time.Duration objects. Keys not found in the map will be ignored 210func convertToDuration(data map[string]interface{}, keys ...string) error { 211 merr := new(multierror.Error) 212 for _, key := range keys { 213 val, exists := data[key] 214 if !exists { 215 continue 216 } 217 218 dur, err := parseutil.ParseDurationSecond(val) 219 if err != nil { 220 merr = multierror.Append(merr, fmt.Errorf("invalid duration %s: %w", key, err)) 221 continue 222 } 223 data[key] = dur 224 } 225 return merr.ErrorOrNil() 226} 227 228// decodeBase64 attempts to base64 decode the provided string. If the string is not base64 encoded, this 229// returns the original string. 230// This is equivalent to "if string is base64 encoded, decode it and return, otherwise return the original string" 231func decodeBase64(str string) string { 232 if str == "" { 233 return "" 234 } 235 decoded, err := base64.StdEncoding.DecodeString(str) 236 if err != nil { 237 return str 238 } 239 return string(decoded) 240} 241 242func assertValidLDIFTemplate(rawTemplate string) error { 243 // Test the template to ensure there aren't any errors in the template syntax 244 now := time.Now() 245 exp := now.Add(24 * time.Hour) 246 testTemplateData := dynamicTemplateData{ 247 Username: "testuser", 248 Password: "testpass", 249 DisplayName: "testdisplayname", 250 RoleName: "testrolename", 251 IssueTime: now.Format(time.RFC3339), 252 IssueTimeSeconds: now.Unix(), 253 ExpirationTime: exp.Format(time.RFC3339), 254 ExpirationTimeSeconds: exp.Unix(), 255 } 256 257 testLDIF, err := applyTemplate(rawTemplate, testTemplateData) 258 if err != nil { 259 return fmt.Errorf("invalid template: %w", err) 260 } 261 262 // Test the LDIF to ensure there aren't any errors in the syntax 263 entries, err := ldif.Parse(testLDIF) 264 if err != nil { 265 return fmt.Errorf("LDIF is invalid: %w", err) 266 } 267 268 if len(entries.Entries) == 0 { 269 return fmt.Errorf("must specify at least one LDIF entry") 270 } 271 272 return nil 273} 274 275func (b *backend) pathDynamicRoleRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 276 roleName := data.Get("name").(string) 277 278 dRole, err := retrieveDynamicRole(ctx, req.Storage, roleName) 279 if err != nil { 280 return nil, fmt.Errorf("failed to retrieve dynamic role: %w", err) 281 } 282 if dRole == nil { 283 return nil, nil 284 } 285 286 resp := &logical.Response{ 287 Data: map[string]interface{}{ 288 "creation_ldif": dRole.CreationLDIF, 289 "deletion_ldif": dRole.DeletionLDIF, 290 "rollback_ldif": dRole.RollbackLDIF, 291 "username_template": dRole.UsernameTemplate, 292 "default_ttl": dRole.DefaultTTL.Seconds(), 293 "max_ttl": dRole.MaxTTL.Seconds(), 294 }, 295 } 296 return resp, nil 297} 298 299func (b *backend) pathDynamicRoleList(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 300 roles, err := req.Storage.List(ctx, dynamicRolePath) 301 if err != nil { 302 return nil, fmt.Errorf("failed to list roles: %w", err) 303 } 304 305 return logical.ListResponse(roles), nil 306} 307 308func (b *backend) pathDynamicRoleExistenceCheck(ctx context.Context, req *logical.Request, data *framework.FieldData) (bool, error) { 309 roleName := data.Get("name").(string) 310 role, err := retrieveDynamicRole(ctx, req.Storage, roleName) 311 if err != nil { 312 return false, fmt.Errorf("error finding role: %w", err) 313 } 314 return role != nil, nil 315} 316 317func (b *backend) pathDynamicRoleDelete(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 318 roleName := data.Get("name").(string) 319 320 err := deleteDynamicRole(ctx, req.Storage, roleName) 321 if err != nil { 322 return nil, fmt.Errorf("failed to delete role: %w", err) 323 } 324 return nil, nil 325} 326