1package openldap 2 3import ( 4 "context" 5 "time" 6 7 "github.com/hashicorp/vault/sdk/framework" 8 "github.com/hashicorp/vault/sdk/helper/locksutil" 9 "github.com/hashicorp/vault/sdk/logical" 10 "github.com/hashicorp/vault/sdk/queue" 11) 12 13const ( 14 staticRolePath = "static-role/" 15) 16 17func (b *backend) pathListRoles() []*framework.Path { 18 return []*framework.Path{ 19 { 20 Pattern: staticRolePath + "?$", 21 Operations: map[logical.Operation]framework.OperationHandler{ 22 logical.ListOperation: &framework.PathOperation{ 23 Callback: b.pathRoleList, 24 }, 25 }, 26 HelpSynopsis: staticRolesListHelpSynopsis, 27 HelpDescription: staticRolesListHelpDescription, 28 }, 29 } 30} 31 32func (b *backend) pathRoles() []*framework.Path { 33 return []*framework.Path{ 34 { 35 Pattern: staticRolePath + framework.GenericNameRegex("name"), 36 Fields: fieldsForType(staticRolePath), 37 ExistenceCheck: b.pathStaticRoleExistenceCheck, 38 Operations: map[logical.Operation]framework.OperationHandler{ 39 logical.UpdateOperation: &framework.PathOperation{ 40 Callback: b.pathStaticRoleCreateUpdate, 41 }, 42 logical.CreateOperation: &framework.PathOperation{ 43 Callback: b.pathStaticRoleCreateUpdate, 44 }, 45 logical.ReadOperation: &framework.PathOperation{ 46 Callback: b.pathStaticRoleRead, 47 }, 48 logical.DeleteOperation: &framework.PathOperation{ 49 Callback: b.pathStaticRoleDelete, 50 }, 51 }, 52 HelpSynopsis: staticRoleHelpSynopsis, 53 HelpDescription: staticRoleHelpDescription, 54 }, 55 } 56} 57 58// fieldsForType returns a map of string/FieldSchema items for the given role 59// type. The purpose is to keep the shared fields between dynamic and static 60// roles consistent, and allow for each type to override or provide their own 61// specific fields 62func fieldsForType(roleType string) map[string]*framework.FieldSchema { 63 fields := map[string]*framework.FieldSchema{ 64 "name": { 65 Type: framework.TypeLowerCaseString, 66 Description: "Name of the role", 67 }, 68 "username": { 69 Type: framework.TypeString, 70 Description: "The username/logon name for the entry with which this role will be associated.", 71 }, 72 "dn": { 73 Type: framework.TypeString, 74 Description: "The distinguished name of the entry to manage.", 75 }, 76 "ttl": { 77 Type: framework.TypeDurationSecond, 78 Description: "The time-to-live for the password.", 79 }, 80 } 81 82 // Get the fields that are specific to the type of role, and add them to the 83 // common fields. In the future we can add additional for dynamic roles. 84 var typeFields map[string]*framework.FieldSchema 85 switch roleType { 86 case staticRolePath: 87 typeFields = staticFields() 88 } 89 90 for k, v := range typeFields { 91 fields[k] = v 92 } 93 94 return fields 95} 96 97// staticFields returns a map of key and field schema items that are specific 98// only to static roles 99func staticFields() map[string]*framework.FieldSchema { 100 fields := map[string]*framework.FieldSchema{ 101 "rotation_period": { 102 Type: framework.TypeDurationSecond, 103 Description: "Period for automatic credential rotation of the given entry.", 104 }, 105 } 106 return fields 107} 108 109func (b *backend) pathStaticRoleExistenceCheck(ctx context.Context, req *logical.Request, data *framework.FieldData) (bool, error) { 110 role, err := b.StaticRole(ctx, req.Storage, data.Get("name").(string)) 111 if err != nil { 112 return false, err 113 } 114 return role != nil, nil 115} 116 117func (b *backend) pathStaticRoleDelete(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 118 name := data.Get("name").(string) 119 120 // Grab the exclusive lock 121 lock := locksutil.LockForKey(b.roleLocks, name) 122 lock.Lock() 123 defer lock.Unlock() 124 125 //TODO: Add retry logic 126 127 // Remove the item from the queue 128 _, err := b.popFromRotationQueueByKey(name) 129 if err != nil { 130 return nil, err 131 } 132 133 role, err := b.StaticRole(ctx, req.Storage, name) 134 if err != nil { 135 return nil, err 136 } 137 if role == nil { 138 return nil, nil 139 } 140 141 err = req.Storage.Delete(ctx, staticRolePath+name) 142 if err != nil { 143 return nil, err 144 } 145 146 return nil, nil 147} 148 149func (b *backend) pathStaticRoleRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { 150 role, err := b.StaticRole(ctx, req.Storage, d.Get("name").(string)) 151 if err != nil { 152 return nil, err 153 } 154 if role == nil { 155 return nil, nil 156 } 157 158 data := map[string]interface{}{ 159 "dn": role.StaticAccount.DN, 160 "username": role.StaticAccount.Username, 161 } 162 163 data["rotation_period"] = role.StaticAccount.RotationPeriod.Seconds() 164 if !role.StaticAccount.LastVaultRotation.IsZero() { 165 data["last_vault_rotation"] = role.StaticAccount.LastVaultRotation 166 } 167 168 return &logical.Response{Data: data}, nil 169} 170 171func (b *backend) pathStaticRoleCreateUpdate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { 172 name := data.Get("name").(string) 173 174 // Grab the exclusive lock as well potentially pop and re-push the queue item 175 // for this role 176 lock := locksutil.LockForKey(b.roleLocks, name) 177 lock.Lock() 178 defer lock.Unlock() 179 180 role, err := b.StaticRole(ctx, req.Storage, data.Get("name").(string)) 181 if err != nil { 182 return nil, err 183 } 184 185 if role == nil { 186 role = &roleEntry{ 187 StaticAccount: &staticAccount{}, 188 } 189 } 190 191 dn := data.Get("dn").(string) 192 if dn == "" { 193 return logical.ErrorResponse("dn is a required field to manage a static account"), nil 194 } 195 role.StaticAccount.DN = dn 196 197 username := data.Get("username").(string) 198 if username == "" { 199 return logical.ErrorResponse("username is a required field to manage a static account"), nil 200 } 201 role.StaticAccount.Username = username 202 203 rotationPeriodSecondsRaw, ok := data.GetOk("rotation_period") 204 if !ok { 205 return logical.ErrorResponse("rotation_period is required for static accounts"), nil 206 } 207 rotationPeriodSeconds := rotationPeriodSecondsRaw.(int) 208 if rotationPeriodSeconds < queueTickSeconds { 209 // If rotation frequency is specified the value 210 // must be at least that of the constant queueTickSeconds (5 seconds at 211 // time of writing), otherwise we wont be able to rotate in time 212 return logical.ErrorResponse("rotation_period must be %d seconds or more", queueTickSeconds), nil 213 } 214 role.StaticAccount.RotationPeriod = time.Duration(rotationPeriodSeconds) * time.Second 215 216 // lvr represents the role's LastVaultRotation 217 lvr := role.StaticAccount.LastVaultRotation 218 219 // Only call setStaticAccountPassword if we're creating the role for the 220 // first time 221 switch req.Operation { 222 case logical.CreateOperation: 223 // setStaticAccountPassword calls Storage.Put and saves the role to storage 224 resp, err := b.setStaticAccountPassword(ctx, req.Storage, &setStaticAccountInput{ 225 RoleName: name, 226 Role: role, 227 }) 228 if err != nil { 229 return nil, err 230 } 231 // guard against RotationTime not being set or zero-value 232 lvr = resp.RotationTime 233 case logical.UpdateOperation: 234 // store updated Role 235 entry, err := logical.StorageEntryJSON(staticRolePath+name, role) 236 if err != nil { 237 return nil, err 238 } 239 if err := req.Storage.Put(ctx, entry); err != nil { 240 return nil, err 241 } 242 243 // In case this is an update, remove any previous version of the item from 244 // the queue 245 246 //TODO: Add retry logic 247 _, err = b.popFromRotationQueueByKey(name) 248 if err != nil { 249 return nil, err 250 } 251 } 252 253 // Add their rotation to the queue 254 if err := b.pushItem(&queue.Item{ 255 Key: name, 256 Priority: lvr.Add(role.StaticAccount.RotationPeriod).Unix(), 257 }); err != nil { 258 return nil, err 259 } 260 261 return nil, nil 262} 263 264type roleEntry struct { 265 StaticAccount *staticAccount `json:"static_account" mapstructure:"static_account"` 266} 267 268type staticAccount struct { 269 // DN to create or assume management for static accounts 270 DN string `json:"dn"` 271 272 // Username to create or assume management for static accounts 273 Username string `json:"username"` 274 275 // Password is the current password for static accounts. As an input, this is 276 // used/required when trying to assume management of an existing static 277 // account. Return this on credential request if it exists. 278 Password string `json:"password"` 279 280 // LastVaultRotation represents the last time Vault rotated the password 281 LastVaultRotation time.Time `json:"last_vault_rotation"` 282 283 // RotationPeriod is number in seconds between each rotation, effectively a 284 // "time to live". This value is compared to the LastVaultRotation to 285 // determine if a password needs to be rotated 286 RotationPeriod time.Duration `json:"rotation_period"` 287} 288 289// NextRotationTime calculates the next rotation by adding the Rotation Period 290// to the last known vault rotation 291func (s *staticAccount) NextRotationTime() time.Time { 292 return s.LastVaultRotation.Add(s.RotationPeriod) 293} 294 295// PasswordTTL calculates the approximate time remaining until the password is 296// no longer valid. This is approximate because the periodic rotation is only 297// checked approximately every 5 seconds, and each rotation can take a small 298// amount of time to process. This can result in a negative TTL time while the 299// rotation function processes the Static Role and performs the rotation. If the 300// TTL is negative, zero is returned. Users should not trust passwords with a 301// Zero TTL, as they are likely in the process of being rotated and will quickly 302// be invalidated. 303func (s *staticAccount) PasswordTTL() time.Duration { 304 next := s.NextRotationTime() 305 ttl := next.Sub(time.Now()).Round(time.Second) 306 if ttl < 0 { 307 ttl = time.Duration(0) 308 } 309 return ttl 310} 311 312func (b *backend) pathRoleList(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) { 313 path := staticRolePath 314 entries, err := req.Storage.List(ctx, path) 315 if err != nil { 316 return nil, err 317 } 318 319 return logical.ListResponse(entries), nil 320} 321 322func (b *backend) StaticRole(ctx context.Context, s logical.Storage, roleName string) (*roleEntry, error) { 323 return b.roleAtPath(ctx, s, roleName, staticRolePath) 324} 325 326func (b *backend) roleAtPath(ctx context.Context, s logical.Storage, roleName string, pathPrefix string) (*roleEntry, error) { 327 entry, err := s.Get(ctx, pathPrefix+roleName) 328 if err != nil { 329 return nil, err 330 } 331 if entry == nil { 332 return nil, nil 333 } 334 335 var result roleEntry 336 if err := entry.DecodeJSON(&result); err != nil { 337 return nil, err 338 } 339 340 return &result, nil 341} 342 343const staticRoleHelpSynopsis = ` 344Manage the static roles that can be created with this backend. 345` 346 347const staticRoleHelpDescription = ` 348This path lets you manage the static roles that can be created with this 349backend. Static Roles are associated with a single LDAP entry, and manage the 350password based on a rotation period, automatically rotating the password. 351 352The "dn" parameter is required and configures the domain name to use when managing 353the existing entry. 354 355The "username" parameter is required and configures the username for the LDAP entry. 356This is helpful to provide a usable name when domain name (DN) isn't used directly for 357authentication. 358 359 360The "rotation_period' parameter is required and configures how often, in seconds, the credentials should be 361automatically rotated by Vault. The minimum is 5 seconds (5s). 362` 363 364const staticRolesListHelpDescription = ` 365List all the static roles being managed by Vault. 366` 367 368const staticRolesListHelpSynopsis = ` 369This path lists all the static roles Vault is currently managing in OpenLDAP. 370` 371