Skip to content

Commit

Permalink
feature (auth/ldap): add repo and reading an auth method (#2718)
Browse files Browse the repository at this point in the history
  • Loading branch information
jimlambrt committed Jan 15, 2023
1 parent 2d44425 commit cf6f757
Show file tree
Hide file tree
Showing 13 changed files with 1,082 additions and 137 deletions.
2 changes: 1 addition & 1 deletion internal/auth/ldap/auth_method.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func NewAuthMethod(ctx context.Context, scopeId string, urls []*url.URL, opt ...
ScopeId: scopeId,
Name: opts.withName,
Description: opts.withDescription,
OperationalState: string(InactiveState), // all new auth methods are initially inactive
OperationalState: string(opts.withOperationalState), // if no option is specified, a new auth method is initially inactive
Urls: strUrls,
StartTls: opts.withStartTls,
InsecureTls: opts.withInsecureTls,
Expand Down
47 changes: 46 additions & 1 deletion internal/auth/ldap/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,20 @@ type options struct {
withBindPassword string
withClientCertificate string
withClientCertificateKey []byte
withLimit int
withUnauthenticatedUser bool
withOrderByCreateTime bool
ascending bool
withOperationalState AuthMethodState
}

// Option - how options are passed as args
type Option func(*options) error

func getDefaultOptions() options {
return options{}
return options{
withOperationalState: InactiveState,
}
}

func getOpts(opt ...Option) (options, error) {
Expand Down Expand Up @@ -223,3 +230,41 @@ func WithClientCertificate(ctx context.Context, privKey []byte, cert *x509.Certi
return nil
}
}

// WithLimit provides an option to provide a limit. Intentionally allowing
// negative integers. If WithLimit < 0, then unlimited results are returned.
// If WithLimit == 0, then default limits are used for results.
func WithLimit(l int) Option {
return func(o *options) error {
o.withLimit = l
return nil
}
}

// WithUnauthenticatedUser provides an option for filtering results for
// an unauthenticated users.
func WithUnauthenticatedUser(enabled bool) Option {
return func(o *options) error {
o.withUnauthenticatedUser = enabled
return nil
}
}

// WithOrderByCreateTime provides an option to specify ordering by the
// CreateTime field.
func WithOrderByCreateTime(ascending bool) Option {
return func(o *options) error {
o.withOrderByCreateTime = true
o.ascending = ascending
return nil
}
}

// WithOperationalState provides an option for specifying the auth method's
// operational state
func WithOperationalState(state AuthMethodState) Option {
return func(o *options) error {
o.withOperationalState = state
return nil
}
}
32 changes: 32 additions & 0 deletions internal/auth/ldap/options_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,4 +204,36 @@ func Test_getOpts(t *testing.T) {
require.Error(t, err)
assert.Contains(err.Error(), "asn1: structure error")
})
t.Run("WithLimit", func(t *testing.T) {
opts, err := getOpts(WithLimit(5))
require.NoError(t, err)
testOpts := getDefaultOptions()
testOpts.withLimit = 5
assert.Equal(t, opts, testOpts)
})
t.Run("WithUnauthenticatedUser", func(t *testing.T) {
assert := assert.New(t)
opts, err := getOpts(WithUnauthenticatedUser(true))
require.NoError(t, err)
testOpts := getDefaultOptions()
testOpts.withUnauthenticatedUser = true
assert.Equal(opts, testOpts)
})
t.Run("WithOrderByCreateTime", func(t *testing.T) {
assert := assert.New(t)
opts, err := getOpts(WithOrderByCreateTime(true))
require.NoError(t, err)
testOpts := getDefaultOptions()
testOpts.withOrderByCreateTime = true
testOpts.ascending = true
assert.Equal(opts, testOpts)
})
t.Run("WithOperationalState", func(t *testing.T) {
assert := assert.New(t)
opts, err := getOpts(WithOperationalState(ActivePublicState))
require.NoError(t, err)
testOpts := getDefaultOptions()
testOpts.withOperationalState = ActivePublicState
assert.Equal(opts, testOpts)
})
}
48 changes: 48 additions & 0 deletions internal/auth/ldap/repository.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package ldap

import (
"context"

"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/kms"
)

// Repository is the ldap repository
type Repository struct {
reader db.Reader
writer db.Writer
kms *kms.Kms

// defaultLimit provides a default for limiting the number of results returned from the repo
defaultLimit int
}

// NewRepository creates a new ldap Repository. Supports the options: WithLimit
// which sets a default limit on results returned by repo operations.
func NewRepository(ctx context.Context, r db.Reader, w db.Writer, kms *kms.Kms, opt ...Option) (*Repository, error) {
const op = "ldap.NewRepository"
if r == nil {
return nil, errors.New(ctx, errors.InvalidParameter, op, "reader is nil")
}
if w == nil {
return nil, errors.New(ctx, errors.InvalidParameter, op, "writer is nil")
}
if kms == nil {
return nil, errors.New(ctx, errors.InvalidParameter, op, "kms is nil")
}
opts, err := getOpts(opt...)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
if opts.withLimit == 0 {
// zero signals the boundary defaults should be used.
opts.withLimit = db.DefaultLimit
}
return &Repository{
reader: r,
writer: w,
kms: kms,
defaultLimit: opts.withLimit,
}, nil
}
237 changes: 237 additions & 0 deletions internal/auth/ldap/repository_auth_method_read.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
package ldap

import (
"context"
"fmt"
"strings"

"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/db/timestamp"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/go-kms-wrapping/v2/extras/structwrapping"
)

// LookupAuthMethod will lookup an auth method in the repo, along with its
// associated Value Objects of SigningAlgs, CallbackUrls, AudClaims and
// Certificates. If it's not found, it will return nil, nil. The
// WithUnauthenticatedUser options is supported and all other options are
// ignored.
func (r *Repository) LookupAuthMethod(ctx context.Context, publicId string, opt ...Option) (*AuthMethod, error) {
const op = "ldap.(Repository).LookupAuthMethod"
if publicId == "" {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing public id")
}
opts, err := getOpts(opt...)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
return r.lookupAuthMethod(ctx, publicId, WithUnauthenticatedUser(opts.withUnauthenticatedUser))
}

// ListAuthMethods returns a slice of AuthMethods for the scopeId. The
// WithUnauthenticatedUser, WithLimit and WithOrder options are supported and
// all other options are ignored.
func (r *Repository) ListAuthMethods(ctx context.Context, scopeIds []string, opt ...Option) ([]*AuthMethod, error) {
const op = "ldap.(Repository).ListAuthMethods"
if len(scopeIds) == 0 {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing scope IDs")
}
authMethods, err := r.getAuthMethods(ctx, "", scopeIds, opt...)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
return authMethods, nil
}

// lookupAuthMethod will lookup a single auth method
func (r *Repository) lookupAuthMethod(ctx context.Context, authMethodId string, opt ...Option) (*AuthMethod, error) {
const op = "ldap.(Repository).lookupAuthMethod"
var err error
ams, err := r.getAuthMethods(ctx, authMethodId, nil, opt...)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
switch {
case len(ams) == 0:
return nil, nil // not an error to return no rows for a "lookup"
case len(ams) > 1:
return nil, errors.New(ctx, errors.NotSpecificIntegrity, op, fmt.Sprintf("%s matched more than 1 ", authMethodId))
default:
return ams[0], nil
}
}

// getAuthMethods allows the caller to either lookup a specific AuthMethod via
// its id or search for a set AuthMethods within a set of scopes. Passing both
// scopeIds and a authMethod is an error. The WithUnauthenticatedUser,
// WithLimit and WithOrder options are supported and all other options are
// ignored.
//
// The AuthMethod returned has its value objects populated (SigningAlgs,
// CallbackUrls, AudClaims and Certificates). The AuthMethod returned has its
// IsPrimaryAuthMethod bool set.
//
// When no record is found it returns nil, nil
func (r *Repository) getAuthMethods(ctx context.Context, authMethodId string, scopeIds []string, opt ...Option) ([]*AuthMethod, error) {
const op = "ldap.(Repository).getAuthMethods"
if authMethodId == "" && len(scopeIds) == 0 {
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing search criteria: both auth method id and scope ids are empty")
}
if authMethodId != "" && len(scopeIds) > 0 {
return nil, errors.New(ctx, errors.InvalidParameter, op, "searching for both an auth method id and scope ids is not supported")
}

const aggregateDelimiter = "|"

dbArgs := []db.Option{}
opts, err := getOpts(opt...)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}
limit := r.defaultLimit
if opts.withLimit != 0 {
// non-zero signals an override of the default limit for the repo.
limit = opts.withLimit
}
dbArgs = append(dbArgs, db.WithLimit(limit))

if opts.withOrderByCreateTime {
if opts.ascending {
dbArgs = append(dbArgs, db.WithOrder("create_time asc"))
} else {
dbArgs = append(dbArgs, db.WithOrder("create_time"))
}
}

var args []any
var where []string
switch {
case authMethodId != "":
where, args = append(where, "public_id = ?"), append(args, authMethodId)
default:
where, args = append(where, "scope_id in(?)"), append(args, scopeIds)
}

if opts.withUnauthenticatedUser {
// the caller is asking for a list of auth methods which can be returned
// to unauthenticated users (so they can authen).
where, args = append(where, "state = ?"), append(args, string(ActivePublicState))
}

var aggAuthMethods []*authMethodAgg
err = r.reader.SearchWhere(ctx, &aggAuthMethods, strings.Join(where, " and "), args, dbArgs...)
if err != nil {
return nil, errors.Wrap(ctx, err, op)
}

if len(aggAuthMethods) == 0 { // we're done if nothing is found.
return nil, nil
}

authMethods := make([]*AuthMethod, 0, len(aggAuthMethods))
for _, agg := range aggAuthMethods {

ccKey := struct {
Ct []byte `wrapping:"ct,certificate_key"`
Pt []byte `wrapping:"pt,certificate_key"`
}{Ct: agg.ClientCertificateKey}
if agg.ClientCertificateCert != nil {
ccWrapper, err := r.kms.GetWrapper(ctx, agg.ScopeId, kms.KeyPurposeDatabase, kms.WithKeyId(agg.ClientCertificateKeyId))
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("failed to get database wrapper for client certificate"))
}
if err := structwrapping.UnwrapStruct(ctx, ccWrapper, &ccKey); err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithCode(errors.Decrypt), errors.WithMsg("failed to decrypt client certificate key"))
}
}
bindPassword := struct {
Ct []byte `wrapping:"ct,password"`
Pt []byte `wrapping:"pt,password"`
}{Ct: agg.BindPassword}
if agg.BindPassword != nil {
bindWrapper, err := r.kms.GetWrapper(ctx, agg.ScopeId, kms.KeyPurposeDatabase, kms.WithKeyId(agg.BindKeyId))
if err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("failed to get database wrapper for bind password"))
}
if err := structwrapping.UnwrapStruct(ctx, bindWrapper, &bindPassword); err != nil {
return nil, errors.Wrap(ctx, err, op, errors.WithCode(errors.Decrypt), errors.WithMsg("failed to decrypt bind password"))
}
}
am := allocAuthMethod()
am.PublicId = agg.PublicId
am.ScopeId = agg.ScopeId
am.IsPrimaryAuthMethod = agg.IsPrimaryAuthMethod
am.Name = agg.Name
am.Description = agg.Description
am.CreateTime = agg.CreateTime
am.UpdateTime = agg.UpdateTime
am.Version = agg.Version
am.OperationalState = agg.State
am.StartTls = agg.StartTLS
am.InsecureTls = agg.InsecureTLS
am.DiscoverDn = agg.DiscoverDn
am.AnonGroupSearch = agg.AnonGroupSearch
am.UpnDomain = agg.UpnDomain
if agg.Urls != "" {
am.Urls = strings.Split(agg.Urls, aggregateDelimiter)
}
if agg.Certs != "" {
am.Certificates = strings.Split(agg.Certs, aggregateDelimiter)
}
am.UserDn = agg.UserDn
am.UserAttr = agg.UserAttr
am.UserFilter = agg.UserFilter
am.GroupDn = agg.GroupDn
am.GroupAttr = agg.GroupAttr
am.GroupFilter = agg.GroupFilter
am.ClientCertificateKey = ccKey.Pt
am.ClientCertificateKeyHmac = agg.ClientCertificateKeyHmac
am.ClientCertificate = string(agg.ClientCertificateCert)
am.BindDn = agg.BindDn
am.BindPassword = string(bindPassword.Pt)
am.BindPasswordHmac = agg.BindPasswordHmac

authMethods = append(authMethods, &am)
}
return authMethods, nil
}

// authMethodAgg is a view that aggregates the auth method's value objects. If
// the value object can have multiple values like Urls and Certs, then the
// string field is delimited with the aggregateDelimiter of "|"
type authMethodAgg struct {
PublicId string `gorm:"primary_key"`
ScopeId string
IsPrimaryAuthMethod bool
Name string
Description string
CreateTime *timestamp.Timestamp
UpdateTime *timestamp.Timestamp
Version uint32
State string
StartTLS bool
InsecureTLS bool
DiscoverDn bool
AnonGroupSearch bool
UpnDomain string
Urls string
Certs string
UserDn string
UserAttr string
UserFilter string
GroupDn string
GroupAttr string
GroupFilter string
ClientCertificateKey []byte
ClientCertificateKeyHmac []byte
ClientCertificateKeyId string
ClientCertificateCert []byte
BindDn string
BindPassword []byte
BindPasswordHmac []byte
BindKeyId string
}

// TableName returns the table name for gorm
func (agg *authMethodAgg) TableName() string { return "ldap_auth_method_with_value_obj" }
Loading

0 comments on commit cf6f757

Please sign in to comment.