feature (auth/ldap): add LDAP auth method along with associated accounts and managed groups (#2912)
* feature (auth): required schema changes for auth ldap method (#2669) chore (auth/ldap): move schema changes to next avail migration number * feature (auth/ldap): define AuthMethod and all its value objects (#2703) * feature (auth/ldap): storage protos * feature (auth/ldap): define AuthMethod and all its value objects * feature (auth/ldap): add repo and reading an auth method (#2718) * feature (auth/ldap): add Repository.CreateAuthMethod(...) and Repository.DeleteAuthMethod(...) (#2724) * feature (auth/ldap): add Repository.UpdateAuthMethod(...) (#2739) * feature (auth/ldap): add Account * feature (auth/ldap): add AuthMethod.EnableGroups * fix (auth/ldap): refactor AuthMethod.oplog to enforce proper constraints * feature (auth/ldap): add Account repo functions * refactor (auth/ldap): remove entry attributes from account Realized the entry attributes could have absolutely anything in them (including binary data) and since we absolutely don't have to have them there's just no reason to take on the risk. * feature (auth/ldap): add AuthMethod.UseTokenGroups * feature (auth/ldap): add Authenticate(...) * refactor (auth/ldap): ensure options take a context as the 1st parameter * feature (auth/ldap): add Account attribute maps * chore (auth/ldap): make fmt deltas * feature (auth/ldap): add managed groups (#2760) * tests (auth/ldap): add missing unit test to Repository.DeleteAccount(...) Add bits to test the delete operation when you're not able to generate oplog metadata * feature (auth/ldap): add managed groups fixup! feature (auth/ldap): add managed groups (#2760) * feature (auth/ldap): service proto definition (#2761) * feature (handlers/authmethods): add handlers for ldap auth method operations (#2794) * feature (auth/ldap): add Account attribute maps * chore (auth/ldap): update cap/ldap to latest version * feature (auth/ldap): add ldap api generation definitions * feature (authmethods): add ldap repo NewService(...) * feature (authmethods/ldap): add proper mask_mapping to protobufs * feature (authmethods): add support to get an ldap auth method * refactor (auth/ldap): export TestGenerateCA(...) * feature (authmethods): add support to create an ldap auth method * feature (authmethods): add support to delete an ldap auth method * feature (authmethods): add support to list ldap auth methods * refactor (auth/ldap): make urls optional for NewAuthMethod(...) * refactor (auth/ldap): export ldap.TestInvalidPem * chore: make fmt changes * fix (auth/ldap): properly handle group search config Add constraints and tests to ensure when an ldap AuthMethod.EnableGroups is true, and UseTokenGroups is false; that there's a GroupDn configured for finding a user's associated groups * feature (authmethods): add support to update an ldap auth methods * chore (db/ldap): tmp mv migrations so there's no conflict with ongoing work * feature (verifier): add ldap auth method to verifier bits * fix (controller): prevent panic when controller stops when there's no listener * feature (authmethods): add support to authenticate via ldap auth methods * chore (migrations): fix whitespace in stmt * chore: fmt fixup * tests (auth/ldap): invalid err msg * feature (cli/authmethods): add support for ldap auth-methods CRUD and authenticate (#2810) * feature (authmethods): add CLI support for ldap auth methods CRUD * tests (api/auth): ldap auth method classification tests * feature (authmethods): add CLI support for ldap auth authenticate * feature (auth/ldap): set request timeouts for ldap server connections * feature (handlers/authmethods): handle u_anon listing properly. * feature (account/handers): ldap account and managed group CRUDL APIs (#2852) * feature (auth/ldap) add repository Listing of ManagedGroupMemberAccount * feature (controller/handlers): add ldapRepoFn to accounts service * feature (auth/ldap) register ldap managed group subtype * feature (account/handlers): ldap account CRUDL APIs * feature (controller/handlers): add ldapRepo to managed groups service * feature (account/handlers): ldap managed group CRUDL APIs * docs (domain): add LDAP accounts, auth-methods and managed groups (#2857) * feature (ldap/cli) add ldap accounts and managed groups CRUDL commands (#2856) * fix (handlers/authmethods): fix ldap authorized actions (#2892) * feature (cli/ldap/authenticate): use primary auth method if none is provided (#2890) * fix (auth/ldap): support setting the state attribute * feature (cli/ldap/authenticate): use primary auth method if none is provided * feature (wh/ldap) add tests for new ldap auth method and accounts (#2919) * refactor (migrations/ldap): mv to correct directory * chore: add copyright headers * fix (api/authmethods/ldap): renumber new LdapAuthMethodAttributes field * fix (auth/ldap): allow the auth method state to be updated (#2951) * chore: update sdk and api versions for llb - this is tmp until merging * tests (managed groups): add required errContains for new test
This commit is contained in:
121 files changed
+26652
-671
No files matched your search
@@ -191,6 +191,7 @@ protobuild:
|
||||
@protoc-go-inject-tag -input=./internal/credential/vault/store/vault.pb.go
|
||||
@protoc-go-inject-tag -input=./internal/credential/static/store/static.pb.go
|
||||
@protoc-go-inject-tag -input=./internal/kms/store/audit_key.pb.go
|
||||
@protoc-go-inject-tag -input=./internal/auth/ldap/store/ldap.pb.go
|
||||
|
||||
# inject classification tags (see: https://github.com/hashicorp/go-eventlogger/tree/main/filters/encrypt)
|
||||
@protoc-go-inject-tag -input=./internal/gen/controller/api/services/auth_method_service.pb.go
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
// Code generated by "make api"; DO NOT EDIT.
|
||||
package accounts
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
type LdapAccountAttributes struct {
|
||||
LoginName string `json:"login_name,omitempty"`
|
||||
FullName string `json:"full_name,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
Dn string `json:"dn,omitempty"`
|
||||
MemberOfGroups []string `json:"member_of_groups,omitempty"`
|
||||
}
|
||||
|
||||
func AttributesMapToLdapAccountAttributes(in map[string]interface{}) (*LdapAccountAttributes, error) {
|
||||
if in == nil {
|
||||
return nil, fmt.Errorf("nil input map")
|
||||
}
|
||||
var out LdapAccountAttributes
|
||||
dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
||||
Result: &out,
|
||||
TagName: "json",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating mapstructure decoder: %w", err)
|
||||
}
|
||||
if err := dec.Decode(in); err != nil {
|
||||
return nil, fmt.Errorf("error decoding: %w", err)
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func (pt *Account) GetLdapAccountAttributes() (*LdapAccountAttributes, error) {
|
||||
if pt.Type != "ldap" {
|
||||
return nil, fmt.Errorf("asked to fetch %s-type attributes but account is of type %s", "ldap", pt.Type)
|
||||
}
|
||||
return AttributesMapToLdapAccountAttributes(pt.Attributes)
|
||||
}
|
||||
@@ -125,6 +125,30 @@ func DefaultOidcAccountIssuer() Option {
|
||||
}
|
||||
}
|
||||
|
||||
func WithLdapAccountLoginName(inLoginName string) Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
if !ok {
|
||||
raw = interface{}(map[string]interface{}{})
|
||||
}
|
||||
val := raw.(map[string]interface{})
|
||||
val["login_name"] = inLoginName
|
||||
o.postMap["attributes"] = val
|
||||
}
|
||||
}
|
||||
|
||||
func DefaultLdapAccountLoginName() Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
if !ok {
|
||||
raw = interface{}(map[string]interface{}{})
|
||||
}
|
||||
val := raw.(map[string]interface{})
|
||||
val["login_name"] = nil
|
||||
o.postMap["attributes"] = val
|
||||
}
|
||||
}
|
||||
|
||||
func WithPasswordAccountLoginName(inLoginName string) Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
// Code generated by "make api"; DO NOT EDIT.
|
||||
package authmethods
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
type LdapAuthMethodAttributes struct {
|
||||
State string `json:"state,omitempty"`
|
||||
StartTls bool `json:"start_tls,omitempty"`
|
||||
InsecureTls bool `json:"insecure_tls,omitempty"`
|
||||
DiscoverDn bool `json:"discover_dn,omitempty"`
|
||||
AnonGroupSearch bool `json:"anon_group_search,omitempty"`
|
||||
UpnDomain string `json:"upn_domain,omitempty"`
|
||||
Urls []string `json:"urls,omitempty"`
|
||||
UserDn string `json:"user_dn,omitempty"`
|
||||
UserAttr string `json:"user_attr,omitempty"`
|
||||
UserFilter string `json:"user_filter,omitempty"`
|
||||
EnableGroups bool `json:"enable_groups,omitempty"`
|
||||
GroupDn string `json:"group_dn,omitempty"`
|
||||
GroupAttr string `json:"group_attr,omitempty"`
|
||||
GroupFilter string `json:"group_filter,omitempty"`
|
||||
Certificates []string `json:"certificates,omitempty"`
|
||||
ClientCertificate string `json:"client_certificate,omitempty"`
|
||||
ClientCertificateKey string `json:"client_certificate_key,omitempty"`
|
||||
ClientCertificateKeyHmac string `json:"client_certificate_key_hmac,omitempty"`
|
||||
BindDn string `json:"bind_dn,omitempty"`
|
||||
BindPassword string `json:"bind_password,omitempty"`
|
||||
BindPasswordHmac string `json:"bind_password_hmac,omitempty"`
|
||||
UseTokenGroups bool `json:"use_token_groups,omitempty"`
|
||||
AccountAttributeMaps []string `json:"account_attribute_maps,omitempty"`
|
||||
}
|
||||
|
||||
func AttributesMapToLdapAuthMethodAttributes(in map[string]interface{}) (*LdapAuthMethodAttributes, error) {
|
||||
if in == nil {
|
||||
return nil, fmt.Errorf("nil input map")
|
||||
}
|
||||
var out LdapAuthMethodAttributes
|
||||
dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
||||
Result: &out,
|
||||
TagName: "json",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating mapstructure decoder: %w", err)
|
||||
}
|
||||
if err := dec.Decode(in); err != nil {
|
||||
return nil, fmt.Errorf("error decoding: %w", err)
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func (pt *AuthMethod) GetLdapAuthMethodAttributes() (*LdapAuthMethodAttributes, error) {
|
||||
if pt.Type != "ldap" {
|
||||
return nil, fmt.Errorf("asked to fetch %s-type attributes but auth-method is of type %s", "ldap", pt.Type)
|
||||
}
|
||||
return AttributesMapToLdapAuthMethodAttributes(pt.Attributes)
|
||||
}
|
||||
@@ -90,6 +90,30 @@ func WithRecursive(recurse bool) Option {
|
||||
}
|
||||
}
|
||||
|
||||
func WithLdapAuthMethodAccountAttributeMaps(inAccountAttributeMaps []string) Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
if !ok {
|
||||
raw = interface{}(map[string]interface{}{})
|
||||
}
|
||||
val := raw.(map[string]interface{})
|
||||
val["account_attribute_maps"] = inAccountAttributeMaps
|
||||
o.postMap["attributes"] = val
|
||||
}
|
||||
}
|
||||
|
||||
func DefaultLdapAuthMethodAccountAttributeMaps() Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
if !ok {
|
||||
raw = interface{}(map[string]interface{}{})
|
||||
}
|
||||
val := raw.(map[string]interface{})
|
||||
val["account_attribute_maps"] = nil
|
||||
o.postMap["attributes"] = val
|
||||
}
|
||||
}
|
||||
|
||||
func WithOidcAuthMethodAccountClaimMaps(inAccountClaimMaps []string) Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
@@ -138,6 +162,30 @@ func DefaultOidcAuthMethodAllowedAudiences() Option {
|
||||
}
|
||||
}
|
||||
|
||||
func WithLdapAuthMethodAnonGroupSearch(inAnonGroupSearch bool) Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
if !ok {
|
||||
raw = interface{}(map[string]interface{}{})
|
||||
}
|
||||
val := raw.(map[string]interface{})
|
||||
val["anon_group_search"] = inAnonGroupSearch
|
||||
o.postMap["attributes"] = val
|
||||
}
|
||||
}
|
||||
|
||||
func DefaultLdapAuthMethodAnonGroupSearch() Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
if !ok {
|
||||
raw = interface{}(map[string]interface{}{})
|
||||
}
|
||||
val := raw.(map[string]interface{})
|
||||
val["anon_group_search"] = nil
|
||||
o.postMap["attributes"] = val
|
||||
}
|
||||
}
|
||||
|
||||
func WithOidcAuthMethodApiUrlPrefix(inApiUrlPrefix string) Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
@@ -174,6 +222,78 @@ func DefaultAttributes() Option {
|
||||
}
|
||||
}
|
||||
|
||||
func WithLdapAuthMethodBindDn(inBindDn string) Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
if !ok {
|
||||
raw = interface{}(map[string]interface{}{})
|
||||
}
|
||||
val := raw.(map[string]interface{})
|
||||
val["bind_dn"] = inBindDn
|
||||
o.postMap["attributes"] = val
|
||||
}
|
||||
}
|
||||
|
||||
func DefaultLdapAuthMethodBindDn() Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
if !ok {
|
||||
raw = interface{}(map[string]interface{}{})
|
||||
}
|
||||
val := raw.(map[string]interface{})
|
||||
val["bind_dn"] = nil
|
||||
o.postMap["attributes"] = val
|
||||
}
|
||||
}
|
||||
|
||||
func WithLdapAuthMethodBindPassword(inBindPassword string) Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
if !ok {
|
||||
raw = interface{}(map[string]interface{}{})
|
||||
}
|
||||
val := raw.(map[string]interface{})
|
||||
val["bind_password"] = inBindPassword
|
||||
o.postMap["attributes"] = val
|
||||
}
|
||||
}
|
||||
|
||||
func DefaultLdapAuthMethodBindPassword() Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
if !ok {
|
||||
raw = interface{}(map[string]interface{}{})
|
||||
}
|
||||
val := raw.(map[string]interface{})
|
||||
val["bind_password"] = nil
|
||||
o.postMap["attributes"] = val
|
||||
}
|
||||
}
|
||||
|
||||
func WithLdapAuthMethodCertificates(inCertificates []string) Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
if !ok {
|
||||
raw = interface{}(map[string]interface{}{})
|
||||
}
|
||||
val := raw.(map[string]interface{})
|
||||
val["certificates"] = inCertificates
|
||||
o.postMap["attributes"] = val
|
||||
}
|
||||
}
|
||||
|
||||
func DefaultLdapAuthMethodCertificates() Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
if !ok {
|
||||
raw = interface{}(map[string]interface{}{})
|
||||
}
|
||||
val := raw.(map[string]interface{})
|
||||
val["certificates"] = nil
|
||||
o.postMap["attributes"] = val
|
||||
}
|
||||
}
|
||||
|
||||
func WithOidcAuthMethodClaimsScopes(inClaimsScopes []string) Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
@@ -198,6 +318,54 @@ func DefaultOidcAuthMethodClaimsScopes() Option {
|
||||
}
|
||||
}
|
||||
|
||||
func WithLdapAuthMethodClientCertificate(inClientCertificate string) Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
if !ok {
|
||||
raw = interface{}(map[string]interface{}{})
|
||||
}
|
||||
val := raw.(map[string]interface{})
|
||||
val["client_certificate"] = inClientCertificate
|
||||
o.postMap["attributes"] = val
|
||||
}
|
||||
}
|
||||
|
||||
func DefaultLdapAuthMethodClientCertificate() Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
if !ok {
|
||||
raw = interface{}(map[string]interface{}{})
|
||||
}
|
||||
val := raw.(map[string]interface{})
|
||||
val["client_certificate"] = nil
|
||||
o.postMap["attributes"] = val
|
||||
}
|
||||
}
|
||||
|
||||
func WithLdapAuthMethodClientCertificateKey(inClientCertificateKey string) Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
if !ok {
|
||||
raw = interface{}(map[string]interface{}{})
|
||||
}
|
||||
val := raw.(map[string]interface{})
|
||||
val["client_certificate_key"] = inClientCertificateKey
|
||||
o.postMap["attributes"] = val
|
||||
}
|
||||
}
|
||||
|
||||
func DefaultLdapAuthMethodClientCertificateKey() Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
if !ok {
|
||||
raw = interface{}(map[string]interface{}{})
|
||||
}
|
||||
val := raw.(map[string]interface{})
|
||||
val["client_certificate_key"] = nil
|
||||
o.postMap["attributes"] = val
|
||||
}
|
||||
}
|
||||
|
||||
func WithOidcAuthMethodClientId(inClientId string) Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
@@ -282,6 +450,30 @@ func DefaultOidcAuthMethodDisableDiscoveredConfigValidation() Option {
|
||||
}
|
||||
}
|
||||
|
||||
func WithLdapAuthMethodDiscoverDn(inDiscoverDn bool) Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
if !ok {
|
||||
raw = interface{}(map[string]interface{}{})
|
||||
}
|
||||
val := raw.(map[string]interface{})
|
||||
val["discover_dn"] = inDiscoverDn
|
||||
o.postMap["attributes"] = val
|
||||
}
|
||||
}
|
||||
|
||||
func DefaultLdapAuthMethodDiscoverDn() Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
if !ok {
|
||||
raw = interface{}(map[string]interface{}{})
|
||||
}
|
||||
val := raw.(map[string]interface{})
|
||||
val["discover_dn"] = nil
|
||||
o.postMap["attributes"] = val
|
||||
}
|
||||
}
|
||||
|
||||
func WithOidcAuthMethodDryRun(inDryRun bool) Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
@@ -306,6 +498,102 @@ func DefaultOidcAuthMethodDryRun() Option {
|
||||
}
|
||||
}
|
||||
|
||||
func WithLdapAuthMethodEnableGroups(inEnableGroups bool) Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
if !ok {
|
||||
raw = interface{}(map[string]interface{}{})
|
||||
}
|
||||
val := raw.(map[string]interface{})
|
||||
val["enable_groups"] = inEnableGroups
|
||||
o.postMap["attributes"] = val
|
||||
}
|
||||
}
|
||||
|
||||
func DefaultLdapAuthMethodEnableGroups() Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
if !ok {
|
||||
raw = interface{}(map[string]interface{}{})
|
||||
}
|
||||
val := raw.(map[string]interface{})
|
||||
val["enable_groups"] = nil
|
||||
o.postMap["attributes"] = val
|
||||
}
|
||||
}
|
||||
|
||||
func WithLdapAuthMethodGroupAttr(inGroupAttr string) Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
if !ok {
|
||||
raw = interface{}(map[string]interface{}{})
|
||||
}
|
||||
val := raw.(map[string]interface{})
|
||||
val["group_attr"] = inGroupAttr
|
||||
o.postMap["attributes"] = val
|
||||
}
|
||||
}
|
||||
|
||||
func DefaultLdapAuthMethodGroupAttr() Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
if !ok {
|
||||
raw = interface{}(map[string]interface{}{})
|
||||
}
|
||||
val := raw.(map[string]interface{})
|
||||
val["group_attr"] = nil
|
||||
o.postMap["attributes"] = val
|
||||
}
|
||||
}
|
||||
|
||||
func WithLdapAuthMethodGroupDn(inGroupDn string) Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
if !ok {
|
||||
raw = interface{}(map[string]interface{}{})
|
||||
}
|
||||
val := raw.(map[string]interface{})
|
||||
val["group_dn"] = inGroupDn
|
||||
o.postMap["attributes"] = val
|
||||
}
|
||||
}
|
||||
|
||||
func DefaultLdapAuthMethodGroupDn() Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
if !ok {
|
||||
raw = interface{}(map[string]interface{}{})
|
||||
}
|
||||
val := raw.(map[string]interface{})
|
||||
val["group_dn"] = nil
|
||||
o.postMap["attributes"] = val
|
||||
}
|
||||
}
|
||||
|
||||
func WithLdapAuthMethodGroupFilter(inGroupFilter string) Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
if !ok {
|
||||
raw = interface{}(map[string]interface{}{})
|
||||
}
|
||||
val := raw.(map[string]interface{})
|
||||
val["group_filter"] = inGroupFilter
|
||||
o.postMap["attributes"] = val
|
||||
}
|
||||
}
|
||||
|
||||
func DefaultLdapAuthMethodGroupFilter() Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
if !ok {
|
||||
raw = interface{}(map[string]interface{}{})
|
||||
}
|
||||
val := raw.(map[string]interface{})
|
||||
val["group_filter"] = nil
|
||||
o.postMap["attributes"] = val
|
||||
}
|
||||
}
|
||||
|
||||
func WithOidcAuthMethodIdpCaCerts(inIdpCaCerts []string) Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
@@ -330,6 +618,30 @@ func DefaultOidcAuthMethodIdpCaCerts() Option {
|
||||
}
|
||||
}
|
||||
|
||||
func WithLdapAuthMethodInsecureTls(inInsecureTls bool) Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
if !ok {
|
||||
raw = interface{}(map[string]interface{}{})
|
||||
}
|
||||
val := raw.(map[string]interface{})
|
||||
val["insecure_tls"] = inInsecureTls
|
||||
o.postMap["attributes"] = val
|
||||
}
|
||||
}
|
||||
|
||||
func DefaultLdapAuthMethodInsecureTls() Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
if !ok {
|
||||
raw = interface{}(map[string]interface{}{})
|
||||
}
|
||||
val := raw.(map[string]interface{})
|
||||
val["insecure_tls"] = nil
|
||||
o.postMap["attributes"] = val
|
||||
}
|
||||
}
|
||||
|
||||
func WithOidcAuthMethodIssuer(inIssuer string) Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
@@ -461,3 +773,195 @@ func DefaultOidcAuthMethodSigningAlgorithms() Option {
|
||||
o.postMap["attributes"] = val
|
||||
}
|
||||
}
|
||||
|
||||
func WithLdapAuthMethodStartTls(inStartTls bool) Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
if !ok {
|
||||
raw = interface{}(map[string]interface{}{})
|
||||
}
|
||||
val := raw.(map[string]interface{})
|
||||
val["start_tls"] = inStartTls
|
||||
o.postMap["attributes"] = val
|
||||
}
|
||||
}
|
||||
|
||||
func DefaultLdapAuthMethodStartTls() Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
if !ok {
|
||||
raw = interface{}(map[string]interface{}{})
|
||||
}
|
||||
val := raw.(map[string]interface{})
|
||||
val["start_tls"] = nil
|
||||
o.postMap["attributes"] = val
|
||||
}
|
||||
}
|
||||
|
||||
func WithLdapAuthMethodState(inState string) Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
if !ok {
|
||||
raw = interface{}(map[string]interface{}{})
|
||||
}
|
||||
val := raw.(map[string]interface{})
|
||||
val["state"] = inState
|
||||
o.postMap["attributes"] = val
|
||||
}
|
||||
}
|
||||
|
||||
func DefaultLdapAuthMethodState() Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
if !ok {
|
||||
raw = interface{}(map[string]interface{}{})
|
||||
}
|
||||
val := raw.(map[string]interface{})
|
||||
val["state"] = nil
|
||||
o.postMap["attributes"] = val
|
||||
}
|
||||
}
|
||||
|
||||
func WithLdapAuthMethodUpnDomain(inUpnDomain string) Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
if !ok {
|
||||
raw = interface{}(map[string]interface{}{})
|
||||
}
|
||||
val := raw.(map[string]interface{})
|
||||
val["upn_domain"] = inUpnDomain
|
||||
o.postMap["attributes"] = val
|
||||
}
|
||||
}
|
||||
|
||||
func DefaultLdapAuthMethodUpnDomain() Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
if !ok {
|
||||
raw = interface{}(map[string]interface{}{})
|
||||
}
|
||||
val := raw.(map[string]interface{})
|
||||
val["upn_domain"] = nil
|
||||
o.postMap["attributes"] = val
|
||||
}
|
||||
}
|
||||
|
||||
func WithLdapAuthMethodUrls(inUrls []string) Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
if !ok {
|
||||
raw = interface{}(map[string]interface{}{})
|
||||
}
|
||||
val := raw.(map[string]interface{})
|
||||
val["urls"] = inUrls
|
||||
o.postMap["attributes"] = val
|
||||
}
|
||||
}
|
||||
|
||||
func DefaultLdapAuthMethodUrls() Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
if !ok {
|
||||
raw = interface{}(map[string]interface{}{})
|
||||
}
|
||||
val := raw.(map[string]interface{})
|
||||
val["urls"] = nil
|
||||
o.postMap["attributes"] = val
|
||||
}
|
||||
}
|
||||
|
||||
func WithLdapAuthMethodUseTokenGroups(inUseTokenGroups bool) Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
if !ok {
|
||||
raw = interface{}(map[string]interface{}{})
|
||||
}
|
||||
val := raw.(map[string]interface{})
|
||||
val["use_token_groups"] = inUseTokenGroups
|
||||
o.postMap["attributes"] = val
|
||||
}
|
||||
}
|
||||
|
||||
func DefaultLdapAuthMethodUseTokenGroups() Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
if !ok {
|
||||
raw = interface{}(map[string]interface{}{})
|
||||
}
|
||||
val := raw.(map[string]interface{})
|
||||
val["use_token_groups"] = nil
|
||||
o.postMap["attributes"] = val
|
||||
}
|
||||
}
|
||||
|
||||
func WithLdapAuthMethodUserAttr(inUserAttr string) Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
if !ok {
|
||||
raw = interface{}(map[string]interface{}{})
|
||||
}
|
||||
val := raw.(map[string]interface{})
|
||||
val["user_attr"] = inUserAttr
|
||||
o.postMap["attributes"] = val
|
||||
}
|
||||
}
|
||||
|
||||
func DefaultLdapAuthMethodUserAttr() Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
if !ok {
|
||||
raw = interface{}(map[string]interface{}{})
|
||||
}
|
||||
val := raw.(map[string]interface{})
|
||||
val["user_attr"] = nil
|
||||
o.postMap["attributes"] = val
|
||||
}
|
||||
}
|
||||
|
||||
func WithLdapAuthMethodUserDn(inUserDn string) Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
if !ok {
|
||||
raw = interface{}(map[string]interface{}{})
|
||||
}
|
||||
val := raw.(map[string]interface{})
|
||||
val["user_dn"] = inUserDn
|
||||
o.postMap["attributes"] = val
|
||||
}
|
||||
}
|
||||
|
||||
func DefaultLdapAuthMethodUserDn() Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
if !ok {
|
||||
raw = interface{}(map[string]interface{}{})
|
||||
}
|
||||
val := raw.(map[string]interface{})
|
||||
val["user_dn"] = nil
|
||||
o.postMap["attributes"] = val
|
||||
}
|
||||
}
|
||||
|
||||
func WithLdapAuthMethodUserFilter(inUserFilter string) Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
if !ok {
|
||||
raw = interface{}(map[string]interface{}{})
|
||||
}
|
||||
val := raw.(map[string]interface{})
|
||||
val["user_filter"] = inUserFilter
|
||||
o.postMap["attributes"] = val
|
||||
}
|
||||
}
|
||||
|
||||
func DefaultLdapAuthMethodUserFilter() Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
if !ok {
|
||||
raw = interface{}(map[string]interface{}{})
|
||||
}
|
||||
val := raw.(map[string]interface{})
|
||||
val["user_filter"] = nil
|
||||
o.postMap["attributes"] = val
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Code generated by "make api"; DO NOT EDIT.
|
||||
package managedgroups
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
type LdapManagedGroupAttributes struct {
|
||||
GroupNames []string `json:"group_names,omitempty"`
|
||||
}
|
||||
|
||||
func AttributesMapToLdapManagedGroupAttributes(in map[string]interface{}) (*LdapManagedGroupAttributes, error) {
|
||||
if in == nil {
|
||||
return nil, fmt.Errorf("nil input map")
|
||||
}
|
||||
var out LdapManagedGroupAttributes
|
||||
dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
||||
Result: &out,
|
||||
TagName: "json",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error creating mapstructure decoder: %w", err)
|
||||
}
|
||||
if err := dec.Decode(in); err != nil {
|
||||
return nil, fmt.Errorf("error decoding: %w", err)
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func (pt *ManagedGroup) GetLdapManagedGroupAttributes() (*LdapManagedGroupAttributes, error) {
|
||||
if pt.Type != "ldap" {
|
||||
return nil, fmt.Errorf("asked to fetch %s-type attributes but managed-group is of type %s", "ldap", pt.Type)
|
||||
}
|
||||
return AttributesMapToLdapManagedGroupAttributes(pt.Attributes)
|
||||
}
|
||||
@@ -113,6 +113,30 @@ func WithOidcManagedGroupFilter(inFilter string) Option {
|
||||
}
|
||||
}
|
||||
|
||||
func WithLdapManagedGroupGroupNames(inGroupNames []string) Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
if !ok {
|
||||
raw = interface{}(map[string]interface{}{})
|
||||
}
|
||||
val := raw.(map[string]interface{})
|
||||
val["group_names"] = inGroupNames
|
||||
o.postMap["attributes"] = val
|
||||
}
|
||||
}
|
||||
|
||||
func DefaultLdapManagedGroupGroupNames() Option {
|
||||
return func(o *options) {
|
||||
raw, ok := o.postMap["attributes"]
|
||||
if !ok {
|
||||
raw = interface{}(map[string]interface{}{})
|
||||
}
|
||||
val := raw.(map[string]interface{})
|
||||
val["group_names"] = nil
|
||||
o.postMap["attributes"] = val
|
||||
}
|
||||
}
|
||||
|
||||
func WithName(inName string) Option {
|
||||
return func(o *options) {
|
||||
o.postMap["name"] = inName
|
||||
|
||||
@@ -29,6 +29,14 @@ const (
|
||||
// ids
|
||||
OidcManagedGroupPrefix = "mgoidc"
|
||||
|
||||
// LdapManagedGroupPrefix defines the prefix for ManagedGroup public ids
|
||||
// from the LDAP auth method.
|
||||
LdapManagedGroupPrefix = "mgldap"
|
||||
// AuthMethodPrefix defines the prefix for AuthMethod public ids.
|
||||
LdapAuthMethodPrefix = "amldap"
|
||||
// AccountPrefix defines the prefix for Account public ids.
|
||||
LdapAccountPrefix = "acctldap"
|
||||
|
||||
// ProjectPrefix is the prefix for project scopes
|
||||
ProjectPrefix = "p"
|
||||
// OrgPrefix is the prefix for org scopes
|
||||
|
||||
@@ -20,8 +20,8 @@ require (
|
||||
github.com/google/go-cmp v0.5.9
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.2
|
||||
github.com/hashicorp/boundary/api v0.0.34
|
||||
github.com/hashicorp/boundary/sdk v0.0.30
|
||||
github.com/hashicorp/boundary/api v0.0.34-0.20230215183053-218de6441d53
|
||||
github.com/hashicorp/boundary/sdk v0.0.30-0.20230215183053-218de6441d53
|
||||
github.com/hashicorp/cap v0.1.1
|
||||
github.com/hashicorp/dawdle v0.4.0
|
||||
github.com/hashicorp/dbassert v0.0.0-20210708202608-ecf920cf1ed8
|
||||
@@ -93,12 +93,14 @@ require (
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0
|
||||
github.com/cenkalti/backoff/v4 v4.1.3
|
||||
github.com/creack/pty v1.1.11
|
||||
github.com/hashicorp/cap/ldap v0.0.0-20230123181313-9c0fb924b0d9
|
||||
github.com/hashicorp/go-kms-wrapping/extras/kms/v2 v2.0.0-20221122211539-47c893099f13
|
||||
github.com/hashicorp/go-version v1.3.0
|
||||
github.com/hashicorp/nodeenrollment v0.1.18
|
||||
github.com/jimlambrt/gldap v0.1.2
|
||||
github.com/kelseyhightower/envconfig v1.4.0
|
||||
github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a
|
||||
golang.org/x/exp v0.0.0-20220921164117-439092de6870
|
||||
golang.org/x/exp v0.0.0-20221230185412-738e83a70c30
|
||||
golang.org/x/net v0.7.0
|
||||
)
|
||||
|
||||
@@ -106,6 +108,7 @@ require (
|
||||
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect
|
||||
github.com/AlecAivazis/survey/v2 v2.2.9 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20211209120228-48547f28849e // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.2.0 // indirect
|
||||
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
|
||||
@@ -128,6 +131,8 @@ require (
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
github.com/docker/go-units v0.4.0 // indirect
|
||||
github.com/dvsekhvalnov/jose2go v1.5.0 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect
|
||||
github.com/go-ldap/ldap/v3 v3.4.3 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/glog v1.0.0 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
|
||||
@@ -66,6 +66,8 @@ github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935
|
||||
github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
|
||||
github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
|
||||
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20211209120228-48547f28849e h1:ZU22z/2YRFLyf/P4ZwUYSdNCWsMEI0VeyrFoI2rAhJQ=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20211209120228-48547f28849e/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/ClickHouse/clickhouse-go v1.4.3/go.mod h1:EaI/sW7Azgz9UATzd5ZdZHRUhHgv5+JMS9NSr2smCJI=
|
||||
@@ -434,6 +436,8 @@ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm
|
||||
github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14=
|
||||
github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M=
|
||||
github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g=
|
||||
github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks=
|
||||
github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY=
|
||||
@@ -448,6 +452,8 @@ github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vb
|
||||
github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U=
|
||||
github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc=
|
||||
github.com/go-ldap/ldap/v3 v3.1.10/go.mod h1:5Zun81jBTabRaI8lzN7E1JjyEl1g6zI6u9pd8luAK4Q=
|
||||
github.com/go-ldap/ldap/v3 v3.4.3 h1:JCKUtJPIcyOuG7ctGabLKMgIlKnGumD/iGjuWeEruDI=
|
||||
github.com/go-ldap/ldap/v3 v3.4.3/go.mod h1:7LdHfVt6iIOESVEe3Bs4Jp2sHEKgDeduAhgM1/f9qmo=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
@@ -651,6 +657,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.2/go.mod h1:ZbS3MZTZq/apAfAEHGoB
|
||||
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
|
||||
github.com/hashicorp/cap v0.1.1 h1:GjO4+9+H0wv/89YoEsxeVc2jIizL19r5v5l2lpaH8Kg=
|
||||
github.com/hashicorp/cap v0.1.1/go.mod h1:VfBvK2ULRyqsuqAnjgZl7HJ7/CGMC7ro4H5eXiZuun8=
|
||||
github.com/hashicorp/cap/ldap v0.0.0-20230123181313-9c0fb924b0d9 h1:hTJWCzuFRrGnPjPWbb+fxBQD50MKswdQnwpTH4A7YDE=
|
||||
github.com/hashicorp/cap/ldap v0.0.0-20230123181313-9c0fb924b0d9/go.mod h1:raCUTLtdFrAMx/rsQNeYoWDrsfFcqQuTK1vJ5tYd3qA=
|
||||
github.com/hashicorp/dawdle v0.4.0 h1:nAE+aCoAmVulgCPPPrp6oGOykjPhxEe4VUNms/mTKeA=
|
||||
github.com/hashicorp/dawdle v0.4.0/go.mod h1:gOBY8rFXpW4Rw39up1FIacw/1pG/fO779LfchCY3KUw=
|
||||
github.com/hashicorp/dbassert v0.0.0-20210708202608-ecf920cf1ed8 h1:/3iubtqEdLgdXB6ue+WAKvbcpn313jjL5mQvU5ByymE=
|
||||
@@ -860,6 +868,8 @@ github.com/jefferai/keyring v1.1.7-0.20220316160357-58a74bb55891/go.mod h1:iwmrB
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74=
|
||||
github.com/jhump/protoreflect v1.9.1-0.20210817181203-db1a327a393e h1:Yb4fEGk+GtBSNuvy5rs0ZJt/jtopc/z9azQaj3xbies=
|
||||
github.com/jimlambrt/gldap v0.1.2 h1:Xprug+i9WdvdQd8u2bi05JVbllZ+SHhNu4alDY38+Kw=
|
||||
github.com/jimlambrt/gldap v0.1.2/go.mod h1:sKo9VprcJwZRj7OoE7p8YLaPEeNxw3WIEY42NS/iV7E=
|
||||
github.com/jinzhu/gorm v1.9.12 h1:Drgk1clyWT9t9ERbzHza6Mj/8FY/CqMyVzOiHviMo6Q=
|
||||
github.com/jinzhu/gorm v1.9.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
@@ -1400,6 +1410,7 @@ golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5y
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
|
||||
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
|
||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||
@@ -1417,8 +1428,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
|
||||
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
|
||||
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
|
||||
golang.org/x/exp v0.0.0-20220921164117-439092de6870 h1:j8b6j9gzSigH28O5SjSpQSSh9lFd6f5D/q0aHjNTulc=
|
||||
golang.org/x/exp v0.0.0-20220921164117-439092de6870/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
|
||||
golang.org/x/exp v0.0.0-20221230185412-738e83a70c30 h1:m9O6OTJ627iFnN2JIWfdqlZCzneRO6EEBsHXI25P8ws=
|
||||
golang.org/x/exp v0.0.0-20221230185412-738e83a70c30/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
@@ -1515,6 +1526,7 @@ golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qx
|
||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211013171255-e13a2654a71e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
|
||||
|
||||
@@ -320,6 +320,15 @@ var inputStructs = []*structInfo{
|
||||
mapstructureConversionTemplate,
|
||||
},
|
||||
},
|
||||
{
|
||||
inProto: &authmethods.LdapAuthMethodAttributes{},
|
||||
outFile: "authmethods/ldap_auth_method_attributes.gen.go",
|
||||
subtypeName: "LdapAuthMethod",
|
||||
parentTypeName: "AuthMethod",
|
||||
templates: []*template.Template{
|
||||
mapstructureConversionTemplate,
|
||||
},
|
||||
},
|
||||
{
|
||||
inProto: &authmethods.OidcAuthMethodAttributes{},
|
||||
outFile: "authmethods/oidc_auth_method_attributes.gen.go",
|
||||
@@ -379,6 +388,15 @@ var inputStructs = []*structInfo{
|
||||
mapstructureConversionTemplate,
|
||||
},
|
||||
},
|
||||
{
|
||||
inProto: &accounts.LdapAccountAttributes{},
|
||||
outFile: "accounts/ldap_account_attributes.gen.go",
|
||||
subtypeName: "LdapAccount",
|
||||
parentTypeName: "Account",
|
||||
templates: []*template.Template{
|
||||
mapstructureConversionTemplate,
|
||||
},
|
||||
},
|
||||
{
|
||||
inProto: &accounts.OidcAccountAttributes{},
|
||||
outFile: "accounts/oidc_account_attributes.gen.go",
|
||||
@@ -420,6 +438,21 @@ var inputStructs = []*structInfo{
|
||||
mapstructureConversionTemplate,
|
||||
},
|
||||
},
|
||||
{
|
||||
inProto: &managedgroups.LdapManagedGroupAttributes{},
|
||||
outFile: "managedgroups/ldap_managed_group_attributes.gen.go",
|
||||
subtypeName: "LdapManagedGroup",
|
||||
fieldOverrides: []fieldInfo{
|
||||
{
|
||||
Name: "Filter",
|
||||
SkipDefault: true,
|
||||
},
|
||||
},
|
||||
parentTypeName: "ManagedGroup",
|
||||
templates: []*template.Template{
|
||||
mapstructureConversionTemplate,
|
||||
},
|
||||
},
|
||||
{
|
||||
inProto: &managedgroups.ManagedGroup{},
|
||||
outFile: "managedgroups/managedgroups.gen.go",
|
||||
|
||||
@@ -219,7 +219,9 @@ func TestSelfReadingDifferentOutputFields(t *testing.T) {
|
||||
tc.Controller().PasswordAuthRepoFn,
|
||||
tc.Controller().OidcRepoFn,
|
||||
tc.Controller().IamRepoFn,
|
||||
tc.Controller().AuthTokenRepoFn)
|
||||
tc.Controller().AuthTokenRepoFn,
|
||||
tc.Controller().LdapRepoFn,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create two auth tokens belonging to different users in the org. Each will
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/auth"
|
||||
"github.com/hashicorp/boundary/internal/auth/ldap/store"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/oplog"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// accountTableName defines the default table name for an Account
|
||||
const accountTableName = "auth_ldap_account"
|
||||
|
||||
// Account contains an ldap auth account. It is assigned to an ldap AuthMethod
|
||||
// and updates/deletes to that AuthMethod are cascaded to its Accounts.
|
||||
type Account struct {
|
||||
*store.Account
|
||||
tableName string
|
||||
}
|
||||
|
||||
// make sure ldap.Account implements the auth.Account interface
|
||||
var _ auth.Account = (*Account)(nil)
|
||||
|
||||
// NewAccount creates a new in memory Account assigned to ldap AuthMethod.
|
||||
// WithFullName, WithEmail, WithDn, WithName and WithDescription are the only
|
||||
// valid options. All other options are ignored.
|
||||
func NewAccount(ctx context.Context, scopeId, authMethodId, loginName string, opt ...Option) (*Account, error) {
|
||||
const op = "ldap.NewAccount"
|
||||
switch {
|
||||
case scopeId == "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing scope id")
|
||||
case authMethodId == "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth method id")
|
||||
case loginName == "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing login name")
|
||||
}
|
||||
opts, err := getOpts(opt...)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
a := &Account{
|
||||
Account: &store.Account{
|
||||
ScopeId: scopeId,
|
||||
AuthMethodId: authMethodId,
|
||||
LoginName: loginName,
|
||||
Dn: opts.withDn,
|
||||
Name: opts.withName,
|
||||
Description: opts.withDescription,
|
||||
FullName: opts.withFullName,
|
||||
Email: opts.withEmail,
|
||||
MemberOfGroups: opts.withMemberOfGroups,
|
||||
},
|
||||
}
|
||||
if err := a.validate(ctx, op); err != nil {
|
||||
return nil, err // intentionally not wrapped.
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// validate the Account. On success, it will return nil.
|
||||
func (a *Account) validate(ctx context.Context, caller errors.Op) error {
|
||||
const op = "ldap.(Account).validate"
|
||||
switch {
|
||||
case caller == "":
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "missing caller")
|
||||
case a.ScopeId == "":
|
||||
return errors.New(ctx, errors.InvalidParameter, caller, "missing scope id")
|
||||
case a.AuthMethodId == "":
|
||||
return errors.New(ctx, errors.InvalidParameter, caller, "missing auth method id")
|
||||
case a.LoginName == "":
|
||||
return errors.New(ctx, errors.InvalidParameter, caller, "missing login name")
|
||||
case strings.ToLower(a.LoginName) != a.LoginName:
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "login name must be lower case")
|
||||
case a.Email != "" && len(a.Email) > 320:
|
||||
return errors.New(ctx, errors.InvalidParameter, caller, "email address is too long")
|
||||
case a.FullName != "" && len(a.FullName) > 512:
|
||||
return errors.New(ctx, errors.InvalidParameter, caller, "full name is too long")
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// AllocAccount makes an empty one in memory
|
||||
func AllocAccount() *Account {
|
||||
return &Account{
|
||||
Account: &store.Account{},
|
||||
}
|
||||
}
|
||||
|
||||
// clone an Account.
|
||||
func (a *Account) clone() *Account {
|
||||
cp := proto.Clone(a.Account)
|
||||
return &Account{
|
||||
Account: cp.(*store.Account),
|
||||
}
|
||||
}
|
||||
|
||||
// TableName returns the table name.
|
||||
func (a *Account) TableName() string {
|
||||
if a.tableName != "" {
|
||||
return a.tableName
|
||||
}
|
||||
return accountTableName
|
||||
}
|
||||
|
||||
// SetTableName sets the table name.
|
||||
func (a *Account) SetTableName(n string) {
|
||||
a.tableName = n
|
||||
}
|
||||
|
||||
// GetSubject returns the subject, which will always be empty as this type
|
||||
// doesn't currently support subject.
|
||||
func (a *Account) GetSubject() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// oplog will create oplog metadata for the Account.
|
||||
func (a *Account) oplog(ctx context.Context, opType oplog.OpType) (oplog.Metadata, error) {
|
||||
const op = "ldap.(Account).oplog"
|
||||
switch {
|
||||
case a == nil:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing account")
|
||||
case opType == oplog.OpType_OP_TYPE_UNSPECIFIED:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing op type")
|
||||
case a.PublicId == "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing public id")
|
||||
case a.ScopeId == "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing scope id")
|
||||
case a.AuthMethodId == "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth method id")
|
||||
}
|
||||
metadata := oplog.Metadata{
|
||||
"resource-public-id": []string{a.PublicId},
|
||||
"resource-type": []string{"ldap account"},
|
||||
"op-type": []string{opType.String()},
|
||||
"scope-id": []string{a.ScopeId},
|
||||
"auth-method-id": []string{a.AuthMethodId},
|
||||
}
|
||||
return metadata, nil
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/auth/ldap/store"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"golang.org/x/exp/slices"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const (
|
||||
acctAttributeMapTableName = "auth_ldap_account_attribute_map"
|
||||
)
|
||||
|
||||
// AccountToAttribute defines a type for: to account attributes
|
||||
type AccountToAttribute string
|
||||
|
||||
const (
|
||||
// ToEmailAttribute defines the valid email attribute name
|
||||
ToEmailAttribute AccountToAttribute = "email"
|
||||
// ToFullNameAttribute defines the valid full name attribute name
|
||||
ToFullNameAttribute AccountToAttribute = "fullName"
|
||||
)
|
||||
|
||||
// ConvertToAccountToAttribute will convert a string to an AccountToAttribute.
|
||||
// Useful within the ldap package and service packages which wish to
|
||||
// convert/validate a string into an AccountToAttribute
|
||||
func ConvertToAccountToAttribute(ctx context.Context, s string) (AccountToAttribute, error) {
|
||||
const op = "ldap.ConvertToAccountToAttribute"
|
||||
switch {
|
||||
case strings.EqualFold(s, string(ToEmailAttribute)):
|
||||
return ToEmailAttribute, nil
|
||||
case strings.EqualFold(s, string(ToFullNameAttribute)):
|
||||
return ToFullNameAttribute, nil
|
||||
default:
|
||||
return "", errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("%q is not a valid ToAccountAttribute value (%q, %q)", s, ToEmailAttribute, ToFullNameAttribute))
|
||||
}
|
||||
}
|
||||
|
||||
// AccountAttributeMap defines optional from/to account attribute maps.
|
||||
type AccountAttributeMap struct {
|
||||
*store.AccountAttributeMap
|
||||
tableName string
|
||||
}
|
||||
|
||||
// NewAccountAttributeMap creates a new one in memory
|
||||
func NewAccountAttributeMap(ctx context.Context, authMethodId, fromAttribute string, toAttribute AccountToAttribute) (*AccountAttributeMap, error) {
|
||||
const op = "ldap.NewAccountAttributeMap"
|
||||
aam := &AccountAttributeMap{
|
||||
AccountAttributeMap: &store.AccountAttributeMap{
|
||||
LdapMethodId: authMethodId,
|
||||
FromAttribute: fromAttribute,
|
||||
ToAttribute: string(toAttribute),
|
||||
},
|
||||
}
|
||||
if err := aam.validate(ctx, op); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return aam, nil
|
||||
}
|
||||
|
||||
// validate the AccountClaimMap. On success, it will return nil.
|
||||
func (aam *AccountAttributeMap) validate(ctx context.Context, caller errors.Op) error {
|
||||
if aam.LdapMethodId == "" {
|
||||
return errors.New(ctx, errors.InvalidParameter, caller, "missing ldap auth method id")
|
||||
}
|
||||
if aam.FromAttribute == "" {
|
||||
return errors.New(ctx, errors.InvalidParameter, caller, "missing from attribute")
|
||||
}
|
||||
if _, err := ConvertToAccountToAttribute(ctx, aam.ToAttribute); err != nil {
|
||||
return errors.Wrap(ctx, err, caller)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AllocAccountAttributeMap makes an empty one in memory
|
||||
func AllocAccountAttributeMap() AccountAttributeMap {
|
||||
return AccountAttributeMap{
|
||||
AccountAttributeMap: &store.AccountAttributeMap{},
|
||||
}
|
||||
}
|
||||
|
||||
// clone a AccountAttributeMap
|
||||
func (aam *AccountAttributeMap) clone() *AccountAttributeMap {
|
||||
cp := proto.Clone(aam.AccountAttributeMap)
|
||||
return &AccountAttributeMap{
|
||||
AccountAttributeMap: cp.(*store.AccountAttributeMap),
|
||||
}
|
||||
}
|
||||
|
||||
// TableName returns the table name.
|
||||
func (aam *AccountAttributeMap) TableName() string {
|
||||
if aam.tableName != "" {
|
||||
return aam.tableName
|
||||
}
|
||||
return acctAttributeMapTableName
|
||||
}
|
||||
|
||||
// SetTableName sets the table name.
|
||||
func (aam *AccountAttributeMap) SetTableName(n string) {
|
||||
aam.tableName = n
|
||||
}
|
||||
|
||||
// AttributeMap defines the To and From of an ldap attribute map
|
||||
type AttributeMap struct {
|
||||
To string
|
||||
From string
|
||||
}
|
||||
|
||||
// ParseAccountAttributeMaps will parse the inbound attribute maps
|
||||
func ParseAccountAttributeMaps(ctx context.Context, m ...string) ([]AttributeMap, error) {
|
||||
const op = "ldap.ParseAccountAttributeMaps"
|
||||
|
||||
am := make([]AttributeMap, 0, len(m))
|
||||
for _, s := range m {
|
||||
// Split into key/value which maps From/To
|
||||
parts := strings.SplitN(s, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("error parsing attribute map %q: format must be key=value", s))
|
||||
}
|
||||
from, to := parts[0], parts[1]
|
||||
toAttr, err := ConvertToAccountToAttribute(ctx, to)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
found := slices.ContainsFunc(am, func(m AttributeMap) bool {
|
||||
if m.To == to {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
if found {
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("duplicate map for %q attribute", toAttr))
|
||||
}
|
||||
am = append(am, AttributeMap{
|
||||
To: string(to),
|
||||
From: from,
|
||||
})
|
||||
}
|
||||
sort.Slice(am, func(i, j int) bool {
|
||||
return am[i].From < am[j].From
|
||||
})
|
||||
return am, nil
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/auth/ldap/store"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func TestNewAccountAttributeMap(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCtx := context.Background()
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
authMethodId string
|
||||
from string
|
||||
to AccountToAttribute
|
||||
want *AccountAttributeMap
|
||||
wantErrMatch *errors.Template
|
||||
wantErrContains string
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
ctx: testCtx,
|
||||
authMethodId: "test-auth-method-id",
|
||||
from: "mail",
|
||||
to: ToEmailAttribute,
|
||||
want: &AccountAttributeMap{
|
||||
AccountAttributeMap: &store.AccountAttributeMap{
|
||||
LdapMethodId: "test-auth-method-id",
|
||||
FromAttribute: "mail",
|
||||
ToAttribute: string(ToEmailAttribute),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing-auth-method-id",
|
||||
ctx: testCtx,
|
||||
from: "mail",
|
||||
to: ToEmailAttribute,
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing ldap auth method id",
|
||||
},
|
||||
{
|
||||
name: "missing-from",
|
||||
ctx: testCtx,
|
||||
authMethodId: "test-auth-method-id",
|
||||
to: ToEmailAttribute,
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing from attribute",
|
||||
},
|
||||
{
|
||||
name: "missing-to",
|
||||
ctx: testCtx,
|
||||
authMethodId: "test-auth-method-id",
|
||||
from: "mail",
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "\"\" is not a valid ToAccountAttribute value",
|
||||
},
|
||||
{
|
||||
name: "invalid-to",
|
||||
ctx: testCtx,
|
||||
authMethodId: "test-auth-method-id",
|
||||
from: "mail",
|
||||
to: "invalid-to",
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "\"invalid-to\" is not a valid ToAccountAttribute value",
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
got, err := NewAccountAttributeMap(tc.ctx, tc.authMethodId, tc.from, tc.to)
|
||||
if tc.wantErrMatch != nil {
|
||||
require.Error(err)
|
||||
assert.Empty(got)
|
||||
assert.Truef(errors.Match(tc.wantErrMatch, err), "unexpected error: %q", err.Error())
|
||||
if tc.wantErrContains != "" {
|
||||
assert.Contains(err.Error(), tc.wantErrContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
require.NotNil(got)
|
||||
assert.Equal(tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccountAttributeMap_Clone(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCtx := context.Background()
|
||||
t.Run("valid", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
orig, err := NewAccountAttributeMap(testCtx, "test-scope-id", "displayName", ToFullNameAttribute)
|
||||
require.NoError(err)
|
||||
cp := orig.clone()
|
||||
assert.True(proto.Equal(cp.AccountAttributeMap, orig.AccountAttributeMap))
|
||||
})
|
||||
t.Run("not-equal", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
orig, err := NewAccountAttributeMap(testCtx, "test-scope-id", "displayName", ToFullNameAttribute)
|
||||
require.NoError(err)
|
||||
orig2, err := NewAccountAttributeMap(testCtx, "test-scope-id", "displayName2", ToFullNameAttribute)
|
||||
require.NoError(err)
|
||||
|
||||
cp := orig.clone()
|
||||
assert.True(!proto.Equal(cp.AccountAttributeMap, orig2.AccountAttributeMap))
|
||||
})
|
||||
}
|
||||
|
||||
func TestAccountAttributeMap_SetTableName(t *testing.T) {
|
||||
t.Parallel()
|
||||
defaultTableName := acctAttributeMapTableName
|
||||
tests := []struct {
|
||||
name string
|
||||
setNameTo string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "new-name",
|
||||
setNameTo: "new-name",
|
||||
want: "new-name",
|
||||
},
|
||||
{
|
||||
name: "reset to default",
|
||||
setNameTo: "",
|
||||
want: defaultTableName,
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
def := AllocAccountAttributeMap()
|
||||
require.Equal(defaultTableName, def.TableName())
|
||||
m := AllocAccountAttributeMap()
|
||||
m.SetTableName(tc.setNameTo)
|
||||
assert.Equal(tc.want, m.TableName())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAccountAttributeMaps(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCtx := context.Background()
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
attrMaps []string
|
||||
want []AttributeMap
|
||||
wantErrMatch *errors.Template
|
||||
wantErrContains string
|
||||
}{
|
||||
{
|
||||
name: "dup-to-attribute",
|
||||
ctx: testCtx,
|
||||
attrMaps: []string{"from=email", "from=email"},
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "duplicate map for \"email\" attribute",
|
||||
},
|
||||
{
|
||||
name: "dup-from-attribute",
|
||||
ctx: testCtx,
|
||||
attrMaps: []string{"from=email", "from=fullName"},
|
||||
want: []AttributeMap{
|
||||
{To: "email", From: "from"},
|
||||
{To: "fullName", From: "from"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "two-equals",
|
||||
ctx: testCtx,
|
||||
attrMaps: []string{"from==email"},
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "\"=email\" is not a valid ToAccountAttribute",
|
||||
},
|
||||
{
|
||||
name: "invalid-parts",
|
||||
ctx: testCtx,
|
||||
attrMaps: []string{"from=e=mail"},
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "\"e=mail\" is not a valid ToAccountAttribute",
|
||||
},
|
||||
{
|
||||
name: "missing-separators",
|
||||
ctx: testCtx,
|
||||
attrMaps: []string{"from/email"},
|
||||
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "error parsing attribute map \"from/email\": format must be key=value",
|
||||
},
|
||||
{
|
||||
name: "simple",
|
||||
ctx: testCtx,
|
||||
attrMaps: []string{"from=email"},
|
||||
want: []AttributeMap{
|
||||
{To: "email", From: "from"},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
got, err := ParseAccountAttributeMaps(tc.ctx, tc.attrMaps...)
|
||||
if tc.wantErrMatch != nil {
|
||||
require.Error(err)
|
||||
assert.Empty(got)
|
||||
assert.True(errors.Match(tc.wantErrMatch, err))
|
||||
if tc.wantErrContains != "" {
|
||||
assert.Contains(err.Error(), tc.wantErrContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
require.NotEmpty(got)
|
||||
assert.Equal(tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func FuzzParseAccountAttributeMaps(f *testing.F) {
|
||||
reverseFn := func(am AttributeMap) string {
|
||||
return fmt.Sprintf("%s=%s", am.From, am.To)
|
||||
}
|
||||
testCtx := context.Background()
|
||||
f.Add("mail=email")
|
||||
f.Add("displayName=fullName")
|
||||
f.Fuzz(func(t *testing.T, m string) {
|
||||
am, err := ParseAccountAttributeMaps(testCtx, m)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(am) != 1 {
|
||||
return
|
||||
}
|
||||
reversed := reverseFn(am[0])
|
||||
if reversed != m {
|
||||
t.Errorf("account attribute roundtrip failed, input %q, output %q", m, reversed)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,417 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/globals"
|
||||
"github.com/hashicorp/boundary/internal/auth/ldap/store"
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/iam"
|
||||
"github.com/hashicorp/boundary/internal/oplog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func TestNewAccount(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCtx := context.Background()
|
||||
testAuthMethodId := fmt.Sprintf("%s_1", globals.LdapAuthMethodPrefix)
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
scopeId string
|
||||
authMethodId string
|
||||
loginName string
|
||||
opt []Option
|
||||
want *Account
|
||||
wantErrMatch *errors.Template
|
||||
wantErrContains string
|
||||
}{
|
||||
{
|
||||
name: "success-with-all-opts",
|
||||
ctx: testCtx,
|
||||
scopeId: "global",
|
||||
authMethodId: testAuthMethodId,
|
||||
loginName: "test-login-name",
|
||||
opt: []Option{
|
||||
WithName(testCtx, "test-name"),
|
||||
WithDescription(testCtx, "test-description"),
|
||||
WithEmail(testCtx, "alice@bob.com"),
|
||||
WithFullName(testCtx, "alice eve smith"),
|
||||
WithDn(testCtx, "uid=alice, ou=people, o=test org"),
|
||||
WithMemberOfGroups(testCtx, "test-group"),
|
||||
},
|
||||
want: &Account{
|
||||
Account: &store.Account{
|
||||
AuthMethodId: testAuthMethodId,
|
||||
ScopeId: "global",
|
||||
LoginName: "test-login-name",
|
||||
Name: "test-name",
|
||||
Description: "test-description",
|
||||
Email: "alice@bob.com",
|
||||
FullName: "alice eve smith",
|
||||
Dn: "uid=alice, ou=people, o=test org",
|
||||
MemberOfGroups: "[\"test-group\"]",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing-scope-id",
|
||||
ctx: testCtx,
|
||||
authMethodId: testAuthMethodId,
|
||||
loginName: "test-login-name",
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing scope id",
|
||||
},
|
||||
{
|
||||
name: "missing-auth-method-id",
|
||||
ctx: testCtx,
|
||||
scopeId: "global",
|
||||
loginName: "test-login-name",
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing auth method id",
|
||||
},
|
||||
{
|
||||
name: "missing-login-name",
|
||||
ctx: testCtx,
|
||||
scopeId: "global",
|
||||
authMethodId: testAuthMethodId,
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing login name",
|
||||
},
|
||||
{
|
||||
name: "validate-err",
|
||||
ctx: testCtx,
|
||||
scopeId: "global",
|
||||
authMethodId: testAuthMethodId,
|
||||
loginName: "test-login-name",
|
||||
opt: []Option{WithEmail(testCtx, strings.Repeat("-", 400))},
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "email address is too long",
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
got, err := NewAccount(tc.ctx, tc.scopeId, tc.authMethodId, tc.loginName, tc.opt...)
|
||||
if tc.wantErrMatch != nil {
|
||||
require.Error(err)
|
||||
assert.Nil(got)
|
||||
assert.True(errors.Match(tc.wantErrMatch, err))
|
||||
if tc.wantErrContains != "" {
|
||||
assert.Contains(err.Error(), tc.wantErrContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccount_clone(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCtx := context.TODO()
|
||||
testConn, _ := db.TestSetup(t, "postgres")
|
||||
testWrapper := db.TestWrapper(t)
|
||||
|
||||
t.Run("valid", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
org, _ := iam.TestScopes(t, iam.TestRepo(t, testConn, testWrapper))
|
||||
m := TestAuthMethod(t, testConn, testWrapper, org.PublicId, []string{"ldaps://ldap1"})
|
||||
orig, err := NewAccount(testCtx, m.ScopeId, m.PublicId, "alice", WithFullName(testCtx, "Alice Eve Smith"), WithEmail(testCtx, "alice@alice.com"))
|
||||
require.NoError(err)
|
||||
cp := orig.clone()
|
||||
assert.True(proto.Equal(cp.Account, orig.Account))
|
||||
})
|
||||
t.Run("not-equal", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
org, _ := iam.TestScopes(t, iam.TestRepo(t, testConn, testWrapper))
|
||||
m := TestAuthMethod(t, testConn, testWrapper, org.PublicId, []string{"ldaps://ldap1"})
|
||||
orig, err := NewAccount(testCtx, m.ScopeId, m.PublicId, "alice", WithFullName(testCtx, "Alice Eve Smith"), WithEmail(testCtx, "alice@alice.com"))
|
||||
require.NoError(err)
|
||||
orig2, err := NewAccount(testCtx, m.ScopeId, m.PublicId, "bob", WithFullName(testCtx, "Bob Eve Smith"), WithEmail(testCtx, "bob@alice.com"))
|
||||
require.NoError(err)
|
||||
|
||||
cp := orig.clone()
|
||||
assert.True(!proto.Equal(cp.Account, orig2.Account))
|
||||
})
|
||||
}
|
||||
|
||||
func TestAccount_SetTableName(t *testing.T) {
|
||||
t.Parallel()
|
||||
defaultTableName := accountTableName
|
||||
tests := []struct {
|
||||
name string
|
||||
setNameTo string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "new-name",
|
||||
setNameTo: "new-name",
|
||||
want: "new-name",
|
||||
},
|
||||
{
|
||||
name: "reset to default",
|
||||
setNameTo: "",
|
||||
want: defaultTableName,
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
def := AllocAccount()
|
||||
require.Equal(defaultTableName, def.TableName())
|
||||
m := AllocAccount()
|
||||
m.SetTableName(tc.setNameTo)
|
||||
assert.Equal(tc.want, m.TableName())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccount_oplog(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCtx := context.Background()
|
||||
testAcct, err := NewAccount(testCtx, "global", "test-id", "test-login-name")
|
||||
testAcct.PublicId = "test-public-id"
|
||||
require.NoError(t, err)
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
acct *Account
|
||||
opType oplog.OpType
|
||||
want oplog.Metadata
|
||||
wantErrMatch *errors.Template
|
||||
wantErrContains string
|
||||
}{
|
||||
{
|
||||
name: "create",
|
||||
ctx: testCtx,
|
||||
acct: testAcct,
|
||||
opType: oplog.OpType_OP_TYPE_CREATE,
|
||||
want: oplog.Metadata{
|
||||
"auth-method-id": {"test-id"},
|
||||
"resource-public-id": {"test-public-id"},
|
||||
"scope-id": {"global"},
|
||||
"op-type": {oplog.OpType_OP_TYPE_CREATE.String()},
|
||||
"resource-type": {"ldap account"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "update",
|
||||
ctx: testCtx,
|
||||
acct: testAcct,
|
||||
opType: oplog.OpType_OP_TYPE_UPDATE,
|
||||
want: oplog.Metadata{
|
||||
"auth-method-id": {"test-id"},
|
||||
"resource-public-id": {"test-public-id"},
|
||||
"scope-id": {"global"},
|
||||
"op-type": {oplog.OpType_OP_TYPE_UPDATE.String()},
|
||||
"resource-type": {"ldap account"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing-auth-method-id",
|
||||
ctx: testCtx,
|
||||
acct: func() *Account {
|
||||
cp := testAcct.clone()
|
||||
cp.AuthMethodId = ""
|
||||
return cp
|
||||
}(),
|
||||
opType: oplog.OpType_OP_TYPE_UPDATE,
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing auth method id",
|
||||
},
|
||||
{
|
||||
name: "missing-scope-id",
|
||||
ctx: testCtx,
|
||||
acct: func() *Account {
|
||||
cp := testAcct.clone()
|
||||
cp.ScopeId = ""
|
||||
return cp
|
||||
}(),
|
||||
opType: oplog.OpType_OP_TYPE_UPDATE,
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing scope id",
|
||||
},
|
||||
{
|
||||
name: "missing-public-id",
|
||||
ctx: testCtx,
|
||||
acct: func() *Account {
|
||||
cp := testAcct.clone()
|
||||
cp.PublicId = ""
|
||||
return cp
|
||||
}(),
|
||||
opType: oplog.OpType_OP_TYPE_UPDATE,
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing public id",
|
||||
},
|
||||
{
|
||||
name: "missing-op-type",
|
||||
ctx: testCtx,
|
||||
acct: testAcct,
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing op type",
|
||||
},
|
||||
{
|
||||
name: "missing-account",
|
||||
ctx: testCtx,
|
||||
opType: oplog.OpType_OP_TYPE_UPDATE,
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing account",
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
got, err := tc.acct.oplog(tc.ctx, tc.opType)
|
||||
if tc.wantErrMatch != nil {
|
||||
require.Error(err)
|
||||
assert.Nil(got)
|
||||
assert.True(errors.Match(tc.wantErrMatch, err))
|
||||
if tc.wantErrContains != "" {
|
||||
assert.Contains(err.Error(), tc.wantErrContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAccount_GetAuthMethodId(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.Empty(t, AllocAccount().GetAuthMethodId())
|
||||
}
|
||||
|
||||
func TestAccount_GetSubject(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert.Empty(t, AllocAccount().GetSubject())
|
||||
}
|
||||
|
||||
func TestAccount_validate(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCtx := context.Background()
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
caller errors.Op
|
||||
acct *Account
|
||||
wantErrMatch *errors.Template
|
||||
wantErrContains string
|
||||
}{
|
||||
{
|
||||
name: "missing-caller",
|
||||
ctx: testCtx,
|
||||
acct: func() *Account {
|
||||
a, err := NewAccount(testCtx, "global", "test-auth-method-id", "test-login-name")
|
||||
require.NoError(t, err)
|
||||
return a
|
||||
}(),
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing caller",
|
||||
},
|
||||
{
|
||||
name: "missing-scope-id",
|
||||
ctx: testCtx,
|
||||
caller: "test",
|
||||
acct: func() *Account {
|
||||
a, err := NewAccount(testCtx, "global", "test-auth-method-id", "test-login-name")
|
||||
require.NoError(t, err)
|
||||
a.ScopeId = ""
|
||||
return a
|
||||
}(),
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing scope id",
|
||||
},
|
||||
{
|
||||
name: "missing-auth-method-id",
|
||||
ctx: testCtx,
|
||||
caller: "test",
|
||||
acct: func() *Account {
|
||||
a, err := NewAccount(testCtx, "global", "test-auth-method-id", "test-login-name")
|
||||
require.NoError(t, err)
|
||||
a.AuthMethodId = ""
|
||||
return a
|
||||
}(),
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing auth method id",
|
||||
},
|
||||
{
|
||||
name: "missing-login-name",
|
||||
ctx: testCtx,
|
||||
caller: "test",
|
||||
acct: func() *Account {
|
||||
a, err := NewAccount(testCtx, "global", "test-auth-method-id", "test-login-name")
|
||||
require.NoError(t, err)
|
||||
a.LoginName = ""
|
||||
return a
|
||||
}(),
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing login name",
|
||||
},
|
||||
{
|
||||
name: "email-too-long",
|
||||
ctx: testCtx,
|
||||
caller: "test",
|
||||
acct: func() *Account {
|
||||
a, err := NewAccount(testCtx, "global", "test-auth-method-id", "test-login-name")
|
||||
require.NoError(t, err)
|
||||
a.Email = strings.Repeat("-", 321)
|
||||
return a
|
||||
}(),
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "email address is too long",
|
||||
},
|
||||
{
|
||||
name: "full-name-too-long",
|
||||
ctx: testCtx,
|
||||
caller: "test",
|
||||
acct: func() *Account {
|
||||
a, err := NewAccount(testCtx, "global", "test-auth-method-id", "test-login-name")
|
||||
require.NoError(t, err)
|
||||
a.FullName = strings.Repeat("-", 513)
|
||||
return a
|
||||
}(),
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "full name is too long",
|
||||
},
|
||||
{
|
||||
name: "login-name-not-lower-case",
|
||||
ctx: testCtx,
|
||||
caller: "test",
|
||||
acct: func() *Account {
|
||||
a, err := NewAccount(testCtx, "global", "test-auth-method-id", "test-login-name")
|
||||
require.NoError(t, err)
|
||||
a.LoginName = "Test-Login-Name"
|
||||
return a
|
||||
}(),
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "login name must be lower case",
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
err := tc.acct.validate(tc.ctx, tc.caller)
|
||||
if tc.wantErrMatch != nil {
|
||||
require.Error(err)
|
||||
assert.True(errors.Match(tc.wantErrMatch, err))
|
||||
if tc.wantErrContains != "" {
|
||||
assert.Contains(err.Error(), tc.wantErrContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,319 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/auth/ldap/store"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/oplog"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// authMethodTableName defines an AuthMethod's table name.
|
||||
const authMethodTableName = "auth_ldap_method"
|
||||
|
||||
// AuthMethod contains an LDAP auth method configuration. It is owned by a
|
||||
// scope. AuthMethods MUST have at least one Url. AuthMethods MAY one or zero:
|
||||
// UserEntrySearchConf, a GroupEntrySearchConf, BindCredential. AuthMethods
|
||||
// may have zero to many: Accounts, Certificates,
|
||||
type AuthMethod struct {
|
||||
*store.AuthMethod
|
||||
tableName string
|
||||
}
|
||||
|
||||
// NewAuthMethod creates a new in memory AuthMethod assigned to a scopeId. The
|
||||
// new auth method will have an OperationalState of Inactive.
|
||||
//
|
||||
// Supports the options: WithUrls, WithName, WithDescription, WithStartTLS,
|
||||
// WithInsecureTLS, WithDiscoverDN, WithAnonGroupSearch, WithUpnDomain,
|
||||
// WithUserSearchConf, WithGroupSearchConf, WithCertificates, WithBindCredential
|
||||
// are the only valid options and all other options are ignored.
|
||||
func NewAuthMethod(ctx context.Context, scopeId string, opt ...Option) (*AuthMethod, error) {
|
||||
const op = "ldap.NewAuthMethod"
|
||||
switch {
|
||||
case scopeId == "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing scope id")
|
||||
}
|
||||
opts, err := getOpts(opt...)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
|
||||
a := &AuthMethod{
|
||||
AuthMethod: &store.AuthMethod{
|
||||
ScopeId: scopeId,
|
||||
Name: opts.withName,
|
||||
Description: opts.withDescription,
|
||||
OperationalState: string(opts.withOperationalState), // if no option is specified, a new auth method is initially inactive
|
||||
Urls: opts.withUrls,
|
||||
StartTls: opts.withStartTls,
|
||||
InsecureTls: opts.withInsecureTls,
|
||||
DiscoverDn: opts.withDiscoverDn,
|
||||
AnonGroupSearch: opts.withAnonGroupSearch,
|
||||
UpnDomain: opts.withUpnDomain,
|
||||
UserDn: opts.withUserDn,
|
||||
UserAttr: opts.withUserAttr,
|
||||
UserFilter: opts.withUserFilter,
|
||||
EnableGroups: opts.withEnableGroups,
|
||||
UseTokenGroups: opts.withUseTokenGroups,
|
||||
GroupDn: opts.withGroupDn,
|
||||
GroupAttr: opts.withGroupAttr,
|
||||
GroupFilter: opts.withGroupFilter,
|
||||
BindDn: opts.withBindDn,
|
||||
BindPassword: opts.withBindPassword,
|
||||
Certificates: opts.withCertificates,
|
||||
ClientCertificate: opts.withClientCertificate,
|
||||
ClientCertificateKey: opts.withClientCertificateKey,
|
||||
},
|
||||
}
|
||||
if len(opts.withAccountAttributeMap) > 0 {
|
||||
a.AccountAttributeMaps = make([]string, 0, len(opts.withAccountAttributeMap))
|
||||
for k, v := range opts.withAccountAttributeMap {
|
||||
a.AccountAttributeMaps = append(a.AccountAttributeMaps, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
}
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// AllocAuthMethod makes an empty one in memory
|
||||
func AllocAuthMethod() AuthMethod {
|
||||
return AuthMethod{
|
||||
AuthMethod: &store.AuthMethod{},
|
||||
}
|
||||
}
|
||||
|
||||
// clone an AuthMethod
|
||||
func (am *AuthMethod) clone() *AuthMethod {
|
||||
cp := proto.Clone(am.AuthMethod)
|
||||
return &AuthMethod{
|
||||
AuthMethod: cp.(*store.AuthMethod),
|
||||
}
|
||||
}
|
||||
|
||||
// TableName returns the table name (func is required by gorm)
|
||||
func (am *AuthMethod) TableName() string {
|
||||
if am.tableName != "" {
|
||||
return am.tableName
|
||||
}
|
||||
return authMethodTableName
|
||||
}
|
||||
|
||||
// SetTableName sets the table name (func is required by oplog)
|
||||
func (am *AuthMethod) SetTableName(n string) {
|
||||
am.tableName = n
|
||||
}
|
||||
|
||||
// oplog will create oplog metadata for the AuthMethod.
|
||||
func (am *AuthMethod) oplog(ctx context.Context, opType oplog.OpType) (oplog.Metadata, error) {
|
||||
const op = "ldap.(AuthMethod).oplog"
|
||||
switch {
|
||||
case am == nil:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth method")
|
||||
case opType == oplog.OpType_OP_TYPE_UNSPECIFIED:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing op type")
|
||||
case am.PublicId == "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing public id")
|
||||
case am.ScopeId == "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing scope id")
|
||||
}
|
||||
metadata := oplog.Metadata{
|
||||
"resource-public-id": []string{am.PublicId},
|
||||
"resource-type": []string{"ldap auth method"},
|
||||
"op-type": []string{opType.String()},
|
||||
"scope-id": []string{am.ScopeId},
|
||||
}
|
||||
return metadata, nil
|
||||
}
|
||||
|
||||
type convertedValues struct {
|
||||
Urls []any
|
||||
Certs []any
|
||||
UserEntrySearchConf any
|
||||
GroupEntrySearchConf any
|
||||
ClientCertificate any
|
||||
BindCredential any
|
||||
AccountAttributeMaps []any
|
||||
}
|
||||
|
||||
// convertValueObjects converts the embedded value objects. It will return an
|
||||
// error if the AuthMethod's public id is not set.
|
||||
func (am *AuthMethod) convertValueObjects(ctx context.Context) (*convertedValues, error) {
|
||||
const op = "ldap.(AuthMethod).convertValueObjects"
|
||||
if am.PublicId == "" {
|
||||
return nil, errors.New(ctx, errors.InvalidPublicId, op, "missing public id")
|
||||
}
|
||||
var err error
|
||||
converted := &convertedValues{}
|
||||
|
||||
if converted.Urls, err = am.convertUrls(ctx); err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
if converted.Certs, err = am.convertCertificates(ctx); err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
if am.UserDn != "" || am.UserAttr != "" || am.UserFilter != "" {
|
||||
if converted.UserEntrySearchConf, err = am.convertUserEntrySearchConf(ctx); err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
}
|
||||
if am.GroupDn != "" || am.GroupAttr != "" || am.GroupFilter != "" {
|
||||
if converted.GroupEntrySearchConf, err = am.convertGroupEntrySearchConf(ctx); err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
}
|
||||
if am.ClientCertificate != "" || am.ClientCertificateKey != nil {
|
||||
if converted.ClientCertificate, err = am.convertClientCertificate(ctx); err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
}
|
||||
if am.BindDn != "" || am.BindPassword != "" {
|
||||
if converted.BindCredential, err = am.convertBindCredential(ctx); err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
}
|
||||
if converted.AccountAttributeMaps, err = am.convertAccountAttributeMaps(ctx); err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
|
||||
return converted, nil
|
||||
}
|
||||
|
||||
// convertCertificates converts any embedded URLs from []string
|
||||
// to []any where each slice element is a *Url. It will return an error if the
|
||||
// AuthMethod's public id is not set.
|
||||
func (am *AuthMethod) convertUrls(ctx context.Context) ([]any, error) {
|
||||
const op = "ldap.(AuthMethod).convertUrls"
|
||||
if am.PublicId == "" {
|
||||
return nil, errors.New(ctx, errors.InvalidPublicId, op, "missing public id")
|
||||
}
|
||||
newValObjs := make([]any, 0, len(am.Urls))
|
||||
for priority, u := range am.Urls {
|
||||
parsed, err := url.Parse(u)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
obj, err := NewUrl(ctx, am.PublicId, priority+1, parsed)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
newValObjs = append(newValObjs, obj)
|
||||
}
|
||||
return newValObjs, nil
|
||||
}
|
||||
|
||||
// convertCertificates converts any embedded certificates from []string
|
||||
// to []any where each slice element is a *Certificate. It will return an error
|
||||
// if the AuthMethod's public id is not set.
|
||||
func (am *AuthMethod) convertCertificates(ctx context.Context) ([]any, error) {
|
||||
const op = "ldap.(AuthMethod).convertCertificates"
|
||||
if am.PublicId == "" {
|
||||
return nil, errors.New(ctx, errors.InvalidPublicId, op, "missing public id")
|
||||
}
|
||||
newValObjs := make([]any, 0, len(am.Certificates))
|
||||
for _, cert := range am.Certificates {
|
||||
obj, err := NewCertificate(ctx, am.PublicId, cert)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
newValObjs = append(newValObjs, obj)
|
||||
}
|
||||
return newValObjs, nil
|
||||
}
|
||||
|
||||
// convertUserEntrySearchConf converts an embedded user entry search fields
|
||||
// into an any type. It will return an error if the AuthMethod's public id is
|
||||
// not set.
|
||||
func (am *AuthMethod) convertUserEntrySearchConf(ctx context.Context) (any, error) {
|
||||
const op = "ldap.(AuthMethod).convertUserEntrySearchConf"
|
||||
if am.PublicId == "" {
|
||||
return nil, errors.New(ctx, errors.InvalidPublicId, op, "missing public id")
|
||||
}
|
||||
c, err := NewUserEntrySearchConf(ctx, am.PublicId, WithUserDn(ctx, am.UserDn), WithUserAttr(ctx, am.UserAttr), WithUserFilter(ctx, am.UserFilter))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// convertGroupEntrySearchConf converts an embedded group entry search fields
|
||||
// into an any type. It will return an error if the AuthMethod's public id is
|
||||
// not set.
|
||||
func (am *AuthMethod) convertGroupEntrySearchConf(ctx context.Context) (any, error) {
|
||||
const op = "ldap.(AuthMethod).convertGroupEntrySearchConf"
|
||||
if am.PublicId == "" {
|
||||
return nil, errors.New(ctx, errors.InvalidPublicId, op, "missing public id")
|
||||
}
|
||||
c, err := NewGroupEntrySearchConf(ctx, am.PublicId, WithGroupDn(ctx, am.GroupDn), WithGroupAttr(ctx, am.GroupAttr), WithGroupFilter(ctx, am.GroupFilter))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// convertClientCertificate converts an embedded client certificate entry into
|
||||
// an any type. It will return an error if the AuthMethod's public id is not
|
||||
// set.
|
||||
func (am *AuthMethod) convertClientCertificate(ctx context.Context) (any, error) {
|
||||
const op = "ldap.(AuthMethod).convertClientCertificate"
|
||||
if am.PublicId == "" {
|
||||
return nil, errors.New(ctx, errors.InvalidPublicId, op, "missing auth method id")
|
||||
}
|
||||
cc, err := NewClientCertificate(ctx, am.PublicId, am.ClientCertificateKey, am.ClientCertificate)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
return cc, nil
|
||||
}
|
||||
|
||||
// convertBindCredential converts an embedded bind credential entry into
|
||||
// an any type. It will return an error if the AuthMethod's public id is not
|
||||
// set.
|
||||
func (am *AuthMethod) convertBindCredential(ctx context.Context) (any, error) {
|
||||
const op = "ldap.(AuthMethod).convertBindCredentials"
|
||||
if am.PublicId == "" {
|
||||
return nil, errors.New(ctx, errors.InvalidPublicId, op, "missing auth method id")
|
||||
}
|
||||
bc, err := NewBindCredential(ctx, am.PublicId, am.BindDn, []byte(am.BindPassword))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
return bc, nil
|
||||
}
|
||||
|
||||
// convertAccountAttributeMaps converts the embedded account attribute maps from
|
||||
// []string to []interface{} where each slice element is a *AccountAttributeMap. It
|
||||
// will return an error if the AuthMethod's public id is not set or it can
|
||||
// convert the account attribute maps.
|
||||
func (am *AuthMethod) convertAccountAttributeMaps(ctx context.Context) ([]any, error) {
|
||||
const op = "ldap.(AuthMethod).convertAccountAttributeMaps"
|
||||
if am.PublicId == "" {
|
||||
return nil, errors.New(ctx, errors.InvalidPublicId, op, "missing public id")
|
||||
}
|
||||
newInterfaces := make([]any, 0, len(am.AccountAttributeMaps))
|
||||
const (
|
||||
from = 0
|
||||
to = 1
|
||||
)
|
||||
acms, err := ParseAccountAttributeMaps(ctx, am.AccountAttributeMaps...)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
for _, m := range acms {
|
||||
toClaim, err := ConvertToAccountToAttribute(ctx, m.To)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
obj, err := NewAccountAttributeMap(ctx, am.PublicId, m.From, toClaim)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
newInterfaces = append(newInterfaces, obj)
|
||||
}
|
||||
return newInterfaces, nil
|
||||
}
|
||||
@@ -0,0 +1,594 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"net/url"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/auth/ldap/store"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/oplog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func TestNewAuthMethod(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCtx := context.Background()
|
||||
testCert, testCertEncoded := TestGenerateCA(t, "localhost")
|
||||
_, testPrivKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
require.NoError(t, err)
|
||||
derPrivKey, err := x509.MarshalPKCS8PrivateKey(testPrivKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
scopeId string
|
||||
urls []*url.URL
|
||||
opts []Option
|
||||
want *AuthMethod
|
||||
wantErr bool
|
||||
wantErrCode errors.Code
|
||||
wantErrContains string
|
||||
}{
|
||||
{
|
||||
name: "valid-all-opts",
|
||||
ctx: testCtx,
|
||||
scopeId: "global",
|
||||
urls: TestConvertToUrls(t, "ldaps://alice.com"), // converted to an option
|
||||
opts: []Option{
|
||||
WithName(testCtx, "test-name"),
|
||||
WithDescription(testCtx, "test-description"),
|
||||
WithStartTLS(testCtx),
|
||||
WithInsecureTLS(testCtx),
|
||||
WithDiscoverDn(testCtx),
|
||||
WithAnonGroupSearch(testCtx),
|
||||
WithUpnDomain(testCtx, "alice.com"),
|
||||
WithUserDn(testCtx, "user-dn"),
|
||||
WithUserAttr(testCtx, "user-attr"),
|
||||
WithUserFilter(testCtx, "user-filter"),
|
||||
WithGroupDn(testCtx, "group-dn"),
|
||||
WithEnableGroups(testCtx),
|
||||
WithGroupAttr(testCtx, "group-attr"),
|
||||
WithGroupFilter(testCtx, "group-filter"),
|
||||
WithBindCredential(testCtx, "bind-dn", "bind-password"),
|
||||
WithCertificates(testCtx, testCert),
|
||||
WithClientCertificate(testCtx, derPrivKey, testCert), // not a client cert but good enough for this test.
|
||||
WithAccountAttributeMap(testCtx, map[string]AccountToAttribute{"mail": "email"}),
|
||||
},
|
||||
want: &AuthMethod{
|
||||
AuthMethod: &store.AuthMethod{
|
||||
ScopeId: "global",
|
||||
Urls: []string{"ldaps://alice.com"},
|
||||
OperationalState: string(InactiveState),
|
||||
Name: "test-name",
|
||||
Description: "test-description",
|
||||
StartTls: true,
|
||||
InsecureTls: true,
|
||||
DiscoverDn: true,
|
||||
AnonGroupSearch: true,
|
||||
UpnDomain: "alice.com",
|
||||
UserDn: "user-dn",
|
||||
UserAttr: "user-attr",
|
||||
UserFilter: "user-filter",
|
||||
EnableGroups: true,
|
||||
GroupDn: "group-dn",
|
||||
GroupAttr: "group-attr",
|
||||
GroupFilter: "group-filter",
|
||||
BindDn: "bind-dn",
|
||||
BindPassword: "bind-password",
|
||||
Certificates: []string{testCertEncoded},
|
||||
ClientCertificate: testCertEncoded,
|
||||
ClientCertificateKey: derPrivKey,
|
||||
AccountAttributeMaps: []string{"mail=email"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid-no-opts",
|
||||
ctx: testCtx,
|
||||
scopeId: "global",
|
||||
urls: TestConvertToUrls(t, "ldaps://alice.com"),
|
||||
want: &AuthMethod{
|
||||
AuthMethod: &store.AuthMethod{
|
||||
ScopeId: "global",
|
||||
Urls: []string{"ldaps://alice.com"},
|
||||
OperationalState: string(InactiveState),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing-scope",
|
||||
ctx: testCtx,
|
||||
urls: TestConvertToUrls(t, "ldaps://alice.com"),
|
||||
wantErr: true,
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
wantErrContains: "missing scope id",
|
||||
},
|
||||
{
|
||||
name: "invalid-urls",
|
||||
ctx: testCtx,
|
||||
scopeId: "global",
|
||||
urls: func() []*url.URL {
|
||||
parsed, err := url.Parse("https://alice.com")
|
||||
require.NoError(t, err)
|
||||
return []*url.URL{parsed}
|
||||
}(),
|
||||
wantErr: true,
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
wantErrContains: `https scheme in url "https://alice.com" is not either ldap or ldaps`,
|
||||
},
|
||||
{
|
||||
name: "opt-error",
|
||||
ctx: testCtx,
|
||||
scopeId: "global",
|
||||
urls: TestConvertToUrls(t, "ldaps://alice.com"),
|
||||
opts: []Option{WithBindCredential(testCtx, "dn", "")},
|
||||
wantErr: true,
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
wantErrContains: "ldap.WithBindCredential: missing password",
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
tc.opts = append(tc.opts, WithUrls(tc.ctx, tc.urls...))
|
||||
am, err := NewAuthMethod(tc.ctx, tc.scopeId, tc.opts...)
|
||||
if tc.wantErr {
|
||||
require.Error(err)
|
||||
if tc.wantErrCode != errors.Unknown {
|
||||
assert.True(errors.Match(errors.T(tc.wantErrCode), err))
|
||||
}
|
||||
if tc.wantErrContains != "" {
|
||||
assert.Contains(err.Error(), tc.wantErrContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tc.want, am)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthMethod_SetTableName(t *testing.T) {
|
||||
t.Parallel()
|
||||
defaultTableName := authMethodTableName
|
||||
tests := []struct {
|
||||
name string
|
||||
setNameTo string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "new-name",
|
||||
setNameTo: "new-name",
|
||||
want: "new-name",
|
||||
},
|
||||
{
|
||||
name: "reset to default",
|
||||
setNameTo: "",
|
||||
want: defaultTableName,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
def := AllocAuthMethod()
|
||||
require.Equal(defaultTableName, def.TableName())
|
||||
m := AllocAuthMethod()
|
||||
m.SetTableName(tt.setNameTo)
|
||||
assert.Equal(tt.want, m.TableName())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthMethod_clone(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCtx := context.Background()
|
||||
t.Run("valid", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
testCert, _ := TestGenerateCA(t, "localhost")
|
||||
_, testPrivKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
require.NoError(err)
|
||||
derPrivKey, err := x509.MarshalPKCS8PrivateKey(testPrivKey)
|
||||
require.NoError(err)
|
||||
am, err := NewAuthMethod(
|
||||
testCtx,
|
||||
"global",
|
||||
WithUrls(testCtx, TestConvertToUrls(t, "ldaps://alice.com")...),
|
||||
WithStartTLS(testCtx),
|
||||
WithInsecureTLS(testCtx),
|
||||
WithDiscoverDn(testCtx),
|
||||
WithAnonGroupSearch(testCtx),
|
||||
WithUpnDomain(testCtx, "alice.com"),
|
||||
WithUserDn(testCtx, "user-dn"),
|
||||
WithUserAttr(testCtx, "user-attr"),
|
||||
WithUserFilter(testCtx, "user-filter"),
|
||||
WithGroupDn(testCtx, "group-dn"),
|
||||
WithGroupAttr(testCtx, "group-attr"),
|
||||
WithGroupFilter(testCtx, "group-filter"),
|
||||
WithBindCredential(testCtx, "bind-dn", "bind-password"),
|
||||
WithCertificates(testCtx, testCert),
|
||||
WithClientCertificate(testCtx, derPrivKey, testCert), // not a client cert but good enough for this test.
|
||||
)
|
||||
require.NoError(err)
|
||||
cp := am.clone()
|
||||
assert.True(proto.Equal(cp.AuthMethod, am.AuthMethod))
|
||||
})
|
||||
t.Run("not-equal", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
am, err := NewAuthMethod(testCtx, "global", WithUrls(testCtx, TestConvertToUrls(t, "ldaps://alice.com")...))
|
||||
require.NoError(err)
|
||||
am2, err := NewAuthMethod(testCtx, "global", WithUrls(testCtx, TestConvertToUrls(t, "ldaps://bob.com")...))
|
||||
require.NoError(err)
|
||||
|
||||
cp := am.clone()
|
||||
assert.True(!proto.Equal(cp.AuthMethod, am2.AuthMethod))
|
||||
})
|
||||
}
|
||||
|
||||
func TestAuthMethod_oplog(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCtx := context.Background()
|
||||
testAm, err := NewAuthMethod(testCtx, "global", WithUrls(testCtx, TestConvertToUrls(t, "ldap://ldap1")...))
|
||||
testAm.PublicId = "test-public-id"
|
||||
require.NoError(t, err)
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
am *AuthMethod
|
||||
opType oplog.OpType
|
||||
want oplog.Metadata
|
||||
wantErrMatch *errors.Template
|
||||
wantErrContains string
|
||||
}{
|
||||
{
|
||||
name: "create",
|
||||
ctx: testCtx,
|
||||
am: testAm,
|
||||
opType: oplog.OpType_OP_TYPE_CREATE,
|
||||
want: oplog.Metadata{
|
||||
"resource-public-id": {"test-public-id"},
|
||||
"scope-id": {"global"},
|
||||
"op-type": {oplog.OpType_OP_TYPE_CREATE.String()},
|
||||
"resource-type": {"ldap auth method"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "update",
|
||||
ctx: testCtx,
|
||||
am: testAm,
|
||||
opType: oplog.OpType_OP_TYPE_UPDATE,
|
||||
want: oplog.Metadata{
|
||||
"resource-public-id": {"test-public-id"},
|
||||
"scope-id": {"global"},
|
||||
"op-type": {oplog.OpType_OP_TYPE_UPDATE.String()},
|
||||
"resource-type": {"ldap auth method"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing-scope-id",
|
||||
ctx: testCtx,
|
||||
am: func() *AuthMethod {
|
||||
cp := testAm.clone()
|
||||
cp.ScopeId = ""
|
||||
return cp
|
||||
}(),
|
||||
opType: oplog.OpType_OP_TYPE_UPDATE,
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing scope id",
|
||||
},
|
||||
{
|
||||
name: "missing-public-id",
|
||||
ctx: testCtx,
|
||||
am: func() *AuthMethod {
|
||||
cp := testAm.clone()
|
||||
cp.PublicId = ""
|
||||
return cp
|
||||
}(),
|
||||
opType: oplog.OpType_OP_TYPE_UPDATE,
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing public id",
|
||||
},
|
||||
{
|
||||
name: "missing-op-type",
|
||||
ctx: testCtx,
|
||||
am: testAm,
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing op type",
|
||||
},
|
||||
{
|
||||
name: "missing-auth-method",
|
||||
ctx: testCtx,
|
||||
opType: oplog.OpType_OP_TYPE_UPDATE,
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing auth method",
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
got, err := tc.am.oplog(tc.ctx, tc.opType)
|
||||
if tc.wantErrMatch != nil {
|
||||
require.Error(err)
|
||||
assert.Nil(got)
|
||||
assert.True(errors.Match(tc.wantErrMatch, err))
|
||||
if tc.wantErrContains != "" {
|
||||
assert.Contains(err.Error(), tc.wantErrContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_convertValueObjects(t *testing.T) {
|
||||
testCtx := context.TODO()
|
||||
testPublicId := "test-id"
|
||||
testLdapServers := []string{"ldaps://ldap1.alice.com", "ldaps://ldap2.alice.com"}
|
||||
_, pem := TestGenerateCA(t, "localhost")
|
||||
testCerts := []string{pem}
|
||||
c, err := NewCertificate(testCtx, testPublicId, pem)
|
||||
require.NoError(t, err)
|
||||
testCertificates := []any{c}
|
||||
|
||||
testUrls := make([]any, 0, len(testLdapServers))
|
||||
for priority, uu := range TestConvertToUrls(t, testLdapServers...) {
|
||||
u, err := NewUrl(testCtx, testPublicId, priority+1, uu)
|
||||
require.NoError(t, err)
|
||||
testUrls = append(testUrls, u)
|
||||
}
|
||||
|
||||
testAttrMaps := []string{"email_address=email", "display_name=fullName"}
|
||||
testAccountAttributeMaps := make([]any, 0, len(testAttrMaps))
|
||||
acms, err := ParseAccountAttributeMaps(testCtx, testAttrMaps...)
|
||||
require.NoError(t, err)
|
||||
for _, m := range acms {
|
||||
toAttribute, err := ConvertToAccountToAttribute(testCtx, m.To)
|
||||
require.NoError(t, err)
|
||||
obj, err := NewAccountAttributeMap(testCtx, testPublicId, m.From, toAttribute)
|
||||
require.NoError(t, err)
|
||||
testAccountAttributeMaps = append(testAccountAttributeMaps, obj)
|
||||
}
|
||||
|
||||
testUserSearchConf, err := NewUserEntrySearchConf(testCtx, testPublicId, WithUserDn(testCtx, "user-dn"), WithUserAttr(testCtx, "user-attr"))
|
||||
require.NoError(t, err)
|
||||
|
||||
testGroupSearchConf, err := NewGroupEntrySearchConf(testCtx, testPublicId, WithGroupDn(testCtx, "group-dn"), WithGroupAttr(testCtx, "group-attr"))
|
||||
require.NoError(t, err)
|
||||
|
||||
_, testClientCertEncoded := TestGenerateCA(t, "client-cert-host")
|
||||
_, testPrivKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
require.NoError(t, err)
|
||||
|
||||
testClientCertKey, err := x509.MarshalPKCS8PrivateKey(testPrivKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
testClientCertificate, err := NewClientCertificate(testCtx, testPublicId, testClientCertKey, testClientCertEncoded)
|
||||
require.NoError(t, err)
|
||||
|
||||
testBindCredential, err := NewBindCredential(testCtx, testPublicId, "bind-dn", []byte("bind-password"))
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
am *AuthMethod
|
||||
wantValues *convertedValues
|
||||
wantErrMatch *errors.Template
|
||||
wantErrContains string
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
am: &AuthMethod{
|
||||
AuthMethod: &store.AuthMethod{
|
||||
PublicId: testPublicId,
|
||||
Certificates: testCerts,
|
||||
Urls: testLdapServers,
|
||||
UserDn: "user-dn",
|
||||
UserAttr: "user-attr",
|
||||
GroupDn: "group-dn",
|
||||
GroupAttr: "group-attr",
|
||||
ClientCertificateKey: testClientCertKey,
|
||||
ClientCertificate: testClientCertEncoded,
|
||||
BindDn: "bind-dn",
|
||||
BindPassword: "bind-password",
|
||||
AccountAttributeMaps: testAttrMaps,
|
||||
},
|
||||
},
|
||||
wantValues: &convertedValues{
|
||||
Certs: testCertificates,
|
||||
Urls: testUrls,
|
||||
UserEntrySearchConf: testUserSearchConf,
|
||||
GroupEntrySearchConf: testGroupSearchConf,
|
||||
ClientCertificate: testClientCertificate,
|
||||
BindCredential: testBindCredential,
|
||||
AccountAttributeMaps: testAccountAttributeMaps,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing-public-id",
|
||||
am: &AuthMethod{
|
||||
AuthMethod: &store.AuthMethod{
|
||||
Certificates: testCerts,
|
||||
Urls: testLdapServers,
|
||||
UserDn: "user-dn",
|
||||
UserAttr: "user-attr",
|
||||
GroupDn: "group-dn",
|
||||
GroupAttr: "group-attr",
|
||||
ClientCertificateKey: testClientCertKey,
|
||||
ClientCertificate: testClientCertEncoded,
|
||||
BindDn: "bind-dn",
|
||||
BindPassword: "bind-password",
|
||||
},
|
||||
},
|
||||
wantErrMatch: errors.T(errors.InvalidPublicId),
|
||||
},
|
||||
{
|
||||
name: "invalid-to-account-attr-map",
|
||||
am: &AuthMethod{
|
||||
AuthMethod: &store.AuthMethod{
|
||||
PublicId: testPublicId,
|
||||
AccountAttributeMaps: []string{"displayName=invalid-to-attr"},
|
||||
},
|
||||
},
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "not a valid ToAccountAttribute value",
|
||||
},
|
||||
{
|
||||
name: "invalid-account-attr-map-format",
|
||||
am: &AuthMethod{
|
||||
AuthMethod: &store.AuthMethod{
|
||||
PublicId: testPublicId,
|
||||
AccountAttributeMaps: []string{"not-valid"},
|
||||
},
|
||||
},
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "format must be key=value",
|
||||
},
|
||||
{
|
||||
name: "invalid-cert",
|
||||
am: &AuthMethod{
|
||||
AuthMethod: &store.AuthMethod{
|
||||
PublicId: testPublicId,
|
||||
Certificates: []string{TestInvalidPem},
|
||||
},
|
||||
},
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "failed to parse certificate",
|
||||
},
|
||||
{
|
||||
name: "invalid-url-scheme",
|
||||
am: &AuthMethod{
|
||||
AuthMethod: &store.AuthMethod{
|
||||
PublicId: testPublicId,
|
||||
Urls: []string{"https://ldap1"},
|
||||
},
|
||||
},
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "scheme \"https\" is not ldap or ldaps",
|
||||
},
|
||||
{
|
||||
name: "invalid-url-starts-with-space",
|
||||
am: &AuthMethod{
|
||||
AuthMethod: &store.AuthMethod{
|
||||
PublicId: testPublicId,
|
||||
Urls: []string{" ldaps://ldap1"},
|
||||
},
|
||||
},
|
||||
wantErrMatch: errors.T(errors.Unknown),
|
||||
wantErrContains: "first path segment in URL cannot contain colon",
|
||||
},
|
||||
{
|
||||
name: "invalid-client-cert",
|
||||
am: &AuthMethod{
|
||||
AuthMethod: &store.AuthMethod{
|
||||
PublicId: testPublicId,
|
||||
ClientCertificateKey: testClientCertKey,
|
||||
ClientCertificate: TestInvalidPem,
|
||||
},
|
||||
},
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "failed to parse certificate",
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
values, err := tc.am.convertValueObjects(testCtx)
|
||||
if tc.wantErrMatch != nil {
|
||||
require.Error(err)
|
||||
assert.Truef(errors.Match(tc.wantErrMatch, err), "wanted err %q and got: %+v", tc.wantErrMatch.Code, err)
|
||||
if tc.wantErrContains != "" {
|
||||
assert.Contains(err.Error(), tc.wantErrContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
testSortConverted(t, tc.wantValues)
|
||||
testSortConverted(t, values)
|
||||
assert.Equal(tc.wantValues, values)
|
||||
})
|
||||
}
|
||||
t.Run("missing-public-id", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
wantErrMatch := errors.T(errors.InvalidPublicId)
|
||||
am := &AuthMethod{
|
||||
AuthMethod: &store.AuthMethod{
|
||||
Certificates: testCerts,
|
||||
Urls: testLdapServers,
|
||||
UserDn: "user-dn",
|
||||
UserAttr: "user-attr",
|
||||
GroupDn: "group-dn",
|
||||
GroupAttr: "group-attr",
|
||||
ClientCertificateKey: testClientCertKey,
|
||||
ClientCertificate: testClientCertEncoded,
|
||||
BindDn: "bind-dn",
|
||||
BindPassword: "bind-password",
|
||||
AccountAttributeMaps: testAttrMaps,
|
||||
},
|
||||
}
|
||||
convertedCerts, err := am.convertCertificates(testCtx)
|
||||
require.Error(err)
|
||||
assert.Nil(convertedCerts)
|
||||
assert.Truef(errors.Match(wantErrMatch, err), "wanted err %q and got: %+v", wantErrMatch.Code, err)
|
||||
|
||||
convertedUrls, err := am.convertUrls(testCtx)
|
||||
require.Error(err)
|
||||
assert.Nil(convertedUrls)
|
||||
assert.Truef(errors.Match(wantErrMatch, err), "wanted err %q and got: %+v", wantErrMatch.Code, err)
|
||||
|
||||
convertedMaps, err := am.convertAccountAttributeMaps(testCtx)
|
||||
require.Error(err)
|
||||
assert.Nil(convertedMaps)
|
||||
assert.Truef(errors.Match(wantErrMatch, err), "wanted err %q and got: %+v", wantErrMatch.Code, err)
|
||||
|
||||
convertedUserSearchConf, err := am.convertUserEntrySearchConf(testCtx)
|
||||
require.Error(err)
|
||||
assert.Nil(convertedUserSearchConf)
|
||||
assert.Truef(errors.Match(wantErrMatch, err), "wanted err %q and got: %+v", wantErrMatch.Code, err)
|
||||
|
||||
convertedGroupSearchConf, err := am.convertGroupEntrySearchConf(testCtx)
|
||||
require.Error(err)
|
||||
assert.Nil(convertedGroupSearchConf)
|
||||
assert.Truef(errors.Match(wantErrMatch, err), "wanted err %q and got: %+v", wantErrMatch.Code, err)
|
||||
|
||||
convertedClientCertificate, err := am.convertClientCertificate(testCtx)
|
||||
require.Error(err)
|
||||
assert.Nil(convertedClientCertificate)
|
||||
assert.Truef(errors.Match(wantErrMatch, err), "wanted err %q and got: %+v", wantErrMatch.Code, err)
|
||||
|
||||
convertedBindCredential, err := am.convertBindCredential(testCtx)
|
||||
require.Error(err)
|
||||
assert.Nil(convertedBindCredential)
|
||||
assert.Truef(errors.Match(wantErrMatch, err), "wanted err %q and got: %+v", wantErrMatch.Code, err)
|
||||
})
|
||||
}
|
||||
|
||||
type converted []any
|
||||
|
||||
func (a converted) Len() int { return len(a) }
|
||||
func (a converted) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a converted) Less(i, j int) bool {
|
||||
switch a[i].(type) {
|
||||
case *Url:
|
||||
return a[i].(*Url).GetServerUrl() < a[j].(*Url).GetServerUrl()
|
||||
case *Certificate:
|
||||
return a[i].(*Certificate).GetCert() < a[j].(*Certificate).GetCert()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func testSortConverted(t *testing.T, c *convertedValues) {
|
||||
sort.Sort(converted(c.Urls))
|
||||
sort.Sort(converted(c.Certs))
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/auth/ldap/store"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
|
||||
"github.com/hashicorp/go-kms-wrapping/v2/extras/structwrapping"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const bindCredentialTableName = "auth_ldap_bind_credential"
|
||||
|
||||
// BindCredential represent optional parameters which allow Boundary to bind
|
||||
// (aka authenticate) using the credentials provided when searching for the user
|
||||
// entry used to authenticate the end user.
|
||||
type BindCredential struct {
|
||||
*store.BindCredential
|
||||
tableName string
|
||||
}
|
||||
|
||||
// NewBindCredential creates a new in memory BindCredential. No options are currently supported.
|
||||
func NewBindCredential(ctx context.Context, authMethodId string, dn string, password []byte, _ ...Option) (*BindCredential, error) {
|
||||
const op = "ldap.NewBindCredential"
|
||||
switch {
|
||||
case authMethodId == "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth method id")
|
||||
case dn == "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing dn")
|
||||
case len(password) == 0:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing password")
|
||||
}
|
||||
return &BindCredential{
|
||||
BindCredential: &store.BindCredential{
|
||||
LdapMethodId: authMethodId,
|
||||
Dn: dn,
|
||||
Password: password,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// allocBindCredential makes an empty one in memory
|
||||
func allocBindCredential() *BindCredential {
|
||||
return &BindCredential{
|
||||
BindCredential: &store.BindCredential{},
|
||||
}
|
||||
}
|
||||
|
||||
// clone a bind credential
|
||||
func (bc *BindCredential) clone() *BindCredential {
|
||||
cp := proto.Clone(bc.BindCredential)
|
||||
return &BindCredential{
|
||||
BindCredential: cp.(*store.BindCredential),
|
||||
}
|
||||
}
|
||||
|
||||
// TableName returns the table name
|
||||
func (bc *BindCredential) TableName() string {
|
||||
if bc.tableName != "" {
|
||||
return bc.tableName
|
||||
}
|
||||
return bindCredentialTableName
|
||||
}
|
||||
|
||||
// SetTableName sets the table name.
|
||||
func (bc *BindCredential) SetTableName(n string) {
|
||||
bc.tableName = n
|
||||
}
|
||||
|
||||
// encrypt the bind credential before writing it to the database
|
||||
func (bc *BindCredential) encrypt(ctx context.Context, cipher wrapping.Wrapper) error {
|
||||
const op = "ldap.(BindCredential).encrypt"
|
||||
if cipher == nil {
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "missing cipher")
|
||||
}
|
||||
if err := structwrapping.WrapStruct(ctx, cipher, bc.BindCredential); err != nil {
|
||||
return errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt))
|
||||
}
|
||||
var err error
|
||||
if bc.KeyId, err = cipher.KeyId(ctx); err != nil {
|
||||
return errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt), errors.WithMsg("failed to read cipher key id"))
|
||||
}
|
||||
if bc.PasswordHmac, err = hmacField(ctx, cipher, bc.Password, bc.LdapMethodId); err != nil {
|
||||
return errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt), errors.WithMsg("failed to hmac password"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// decrypt the bind credential after reading it from the database
|
||||
func (bc *BindCredential) decrypt(ctx context.Context, cipher wrapping.Wrapper) error {
|
||||
const op = "ldap.(BindCredential).decrypt"
|
||||
if cipher == nil {
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "missing cipher")
|
||||
}
|
||||
if err := structwrapping.UnwrapStruct(ctx, cipher, bc.BindCredential); err != nil {
|
||||
return errors.Wrap(ctx, err, op, errors.WithCode(errors.Decrypt))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/auth/ldap/store"
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/kms"
|
||||
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func TestNewBindCredential(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCtx := context.Background()
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
authMethodId string
|
||||
dn string
|
||||
password []byte
|
||||
want *BindCredential
|
||||
wantErr bool
|
||||
wantErrCode errors.Code
|
||||
wantErrContains string
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
ctx: testCtx,
|
||||
authMethodId: "test-id",
|
||||
dn: "dn",
|
||||
password: []byte("password"),
|
||||
want: &BindCredential{
|
||||
BindCredential: &store.BindCredential{
|
||||
LdapMethodId: "test-id",
|
||||
Dn: "dn",
|
||||
Password: []byte("password"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing-auth-method-id",
|
||||
ctx: testCtx,
|
||||
dn: "dn",
|
||||
password: []byte("password"),
|
||||
wantErr: true,
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
wantErrContains: "missing auth method id",
|
||||
},
|
||||
{
|
||||
name: "missing-dn",
|
||||
ctx: testCtx,
|
||||
authMethodId: "test-id",
|
||||
password: []byte("password"),
|
||||
wantErr: true,
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
wantErrContains: "missing dn",
|
||||
},
|
||||
{
|
||||
name: "missing-password",
|
||||
ctx: testCtx,
|
||||
authMethodId: "test-id",
|
||||
dn: "dn",
|
||||
wantErr: true,
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
wantErrContains: "missing password",
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
got, err := NewBindCredential(tc.ctx, tc.authMethodId, tc.dn, tc.password)
|
||||
if tc.wantErr {
|
||||
require.Error(err)
|
||||
assert.Nil(got)
|
||||
if tc.wantErrCode != errors.Unknown {
|
||||
assert.True(errors.Match(errors.T(tc.wantErrCode), err))
|
||||
}
|
||||
if tc.wantErrContains != "" {
|
||||
assert.Contains(err.Error(), tc.wantErrContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBindCredential_SetTableName(t *testing.T) {
|
||||
t.Parallel()
|
||||
defaultTableName := bindCredentialTableName
|
||||
tests := []struct {
|
||||
name string
|
||||
setNameTo string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "new-name",
|
||||
setNameTo: "new-name",
|
||||
want: "new-name",
|
||||
},
|
||||
{
|
||||
name: "reset to default",
|
||||
setNameTo: "",
|
||||
want: defaultTableName,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
def := allocBindCredential()
|
||||
require.Equal(defaultTableName, def.TableName())
|
||||
m := allocBindCredential()
|
||||
m.SetTableName(tt.setNameTo)
|
||||
assert.Equal(tt.want, m.TableName())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBindCredential_clone(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCtx := context.Background()
|
||||
t.Run("valid", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
bc, err := NewBindCredential(testCtx, "test-id", "dn", []byte("password"))
|
||||
require.NoError(err)
|
||||
cp := bc.clone()
|
||||
assert.True(proto.Equal(cp.BindCredential, bc.BindCredential))
|
||||
})
|
||||
t.Run("not-equal", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
bc, err := NewBindCredential(testCtx, "test-id", "dn2", []byte("password"))
|
||||
require.NoError(err)
|
||||
|
||||
bc2, err := NewBindCredential(testCtx, "test-id", "dn", []byte("password2"))
|
||||
require.NoError(err)
|
||||
|
||||
cp := bc.clone()
|
||||
assert.True(!proto.Equal(cp.BindCredential, bc2.BindCredential))
|
||||
})
|
||||
}
|
||||
|
||||
func TestBindCredential_encrypt_decrypt(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCtx := context.Background()
|
||||
testWrapper := db.TestWrapper(t)
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
cipher wrapping.Wrapper
|
||||
bc *BindCredential
|
||||
wantErr bool
|
||||
wantErrCode errors.Code
|
||||
wantErrContains string
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
ctx: testCtx,
|
||||
cipher: testWrapper,
|
||||
bc: func() *BindCredential {
|
||||
testBindCred, err := NewBindCredential(testCtx, "test-id", "dn", []byte("password"))
|
||||
require.NoError(t, err)
|
||||
return testBindCred
|
||||
}(),
|
||||
},
|
||||
{
|
||||
name: "missing-cipher",
|
||||
ctx: testCtx,
|
||||
bc: func() *BindCredential {
|
||||
testBindCred, err := NewBindCredential(testCtx, "test-id", "dn", []byte("password"))
|
||||
require.NoError(t, err)
|
||||
return testBindCred
|
||||
}(),
|
||||
wantErr: true,
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
wantErrContains: "missing cipher",
|
||||
},
|
||||
{
|
||||
name: "encrypt-err",
|
||||
ctx: testCtx,
|
||||
cipher: &kms.MockWrapper{
|
||||
Wrapper: testWrapper,
|
||||
EncryptErr: fmt.Errorf("test encrypt error"),
|
||||
},
|
||||
bc: func() *BindCredential {
|
||||
testBindCred, err := NewBindCredential(testCtx, "test-id", "dn", []byte("password"))
|
||||
require.NoError(t, err)
|
||||
return testBindCred
|
||||
}(),
|
||||
wantErr: true,
|
||||
wantErrCode: errors.Encrypt,
|
||||
wantErrContains: "test encrypt error",
|
||||
},
|
||||
{
|
||||
name: "keyId-err",
|
||||
ctx: testCtx,
|
||||
cipher: &kms.MockWrapper{
|
||||
Wrapper: testWrapper,
|
||||
KeyIdErr: fmt.Errorf("test key id error"),
|
||||
},
|
||||
bc: func() *BindCredential {
|
||||
testBindCred, err := NewBindCredential(testCtx, "test-id", "dn", []byte("password"))
|
||||
require.NoError(t, err)
|
||||
return testBindCred
|
||||
}(),
|
||||
wantErr: true,
|
||||
wantErrCode: errors.Encrypt,
|
||||
wantErrContains: "test key id error",
|
||||
},
|
||||
{
|
||||
name: "unknown-wrapper-type-err",
|
||||
ctx: testCtx,
|
||||
cipher: &kms.MockWrapper{
|
||||
Wrapper: testWrapper,
|
||||
},
|
||||
bc: func() *BindCredential {
|
||||
testBindCred, err := NewBindCredential(testCtx, "test-id", "dn", []byte("password"))
|
||||
require.NoError(t, err)
|
||||
return testBindCred
|
||||
}(),
|
||||
wantErr: true,
|
||||
wantErrCode: errors.Encrypt,
|
||||
wantErrContains: "failed to hmac password",
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
require.Empty(tc.bc.CtPassword)
|
||||
require.Empty(tc.bc.PasswordHmac)
|
||||
|
||||
err := tc.bc.encrypt(tc.ctx, tc.cipher)
|
||||
if tc.wantErr {
|
||||
require.Error(err)
|
||||
if tc.wantErrCode != errors.Unknown {
|
||||
assert.True(errors.Match(errors.T(tc.wantErrCode), err))
|
||||
}
|
||||
if tc.wantErrContains != "" {
|
||||
assert.Contains(err.Error(), tc.wantErrContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.NotEmpty(tc.bc.GetPasswordHmac())
|
||||
assert.NotEmpty(tc.bc.GetCtPassword())
|
||||
|
||||
origPass := make([]byte, len(tc.bc.Password))
|
||||
copy(origPass, tc.bc.Password)
|
||||
tc.bc.Password = nil
|
||||
require.NoError(tc.bc.decrypt(tc.ctx, tc.cipher))
|
||||
assert.NotEmpty(tc.bc.Password)
|
||||
assert.Equal(origPass, tc.bc.Password)
|
||||
})
|
||||
}
|
||||
t.Run("decrypt-missing-cipher", func(t *testing.T) {
|
||||
testBindCred, err := NewBindCredential(testCtx, "test-id", "dn", []byte("password"))
|
||||
require.NoError(t, err)
|
||||
err = testBindCred.decrypt(testCtx, nil)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "missing cipher")
|
||||
})
|
||||
t.Run("decrypt-err", func(t *testing.T) {
|
||||
w := &kms.MockWrapper{
|
||||
Wrapper: testWrapper,
|
||||
DecryptErr: fmt.Errorf("test decrypt error"),
|
||||
}
|
||||
testBindCred, err := NewBindCredential(testCtx, "test-id", "dn", []byte("password"))
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, testBindCred.encrypt(testCtx, testWrapper))
|
||||
err = testBindCred.decrypt(testCtx, w)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "test decrypt error")
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/auth/ldap/store"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const certificateTableName = "auth_ldap_certificate"
|
||||
|
||||
// Certificate defines a certificate to use as part of a trust root when
|
||||
// connecting to an auth method's LDAP server. It is assigned to an LDAP
|
||||
// AuthMethod and updates/deletes to that AuthMethod are cascaded to its
|
||||
// Certificates. Certificates are value objects of an AuthMethod, therefore
|
||||
// there's no need for oplog metadata, since only the AuthMethod will have
|
||||
// metadata because it's the root aggregate.
|
||||
type Certificate struct {
|
||||
*store.Certificate
|
||||
tableName string
|
||||
}
|
||||
|
||||
// NewCertificate creates a new in memory certificate assigned to and LDAP auth
|
||||
// method.
|
||||
func NewCertificate(ctx context.Context, authMethodId string, certificatePem string) (*Certificate, error) {
|
||||
const op = "ldap.NewCertificate"
|
||||
// validate() will check the parameters.
|
||||
c := &Certificate{
|
||||
Certificate: &store.Certificate{
|
||||
LdapMethodId: authMethodId,
|
||||
Cert: certificatePem,
|
||||
},
|
||||
}
|
||||
if err := c.validate(ctx, op); err != nil {
|
||||
return nil, err // intentionally, not wrapping err
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// validate the Certificate and on success return nil
|
||||
func (c *Certificate) validate(ctx context.Context, caller errors.Op) error {
|
||||
switch {
|
||||
case c.LdapMethodId == "":
|
||||
return errors.New(ctx, errors.InvalidParameter, caller, "missing ldap auth method id")
|
||||
case c.Cert == "":
|
||||
return errors.New(ctx, errors.InvalidParameter, caller, "missing certificate")
|
||||
default:
|
||||
blk, _ := pem.Decode([]byte(c.Cert))
|
||||
if blk == nil {
|
||||
return errors.New(ctx, errors.InvalidParameter, caller, "failed to parse certificate: invalid PEM encoding")
|
||||
}
|
||||
if _, err := x509.ParseCertificate(blk.Bytes); err != nil {
|
||||
return errors.New(ctx, errors.InvalidParameter, caller, fmt.Sprintf("failed to parse certificate: invalid block: %s", err.Error()), errors.WithWrap(err))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// allocCertificate makes an empty one in memory
|
||||
func allocCertificate() Certificate {
|
||||
return Certificate{
|
||||
Certificate: &store.Certificate{},
|
||||
}
|
||||
}
|
||||
|
||||
// clone a Certificate
|
||||
func (c *Certificate) clone() *Certificate {
|
||||
cp := proto.Clone(c.Certificate)
|
||||
return &Certificate{
|
||||
Certificate: cp.(*store.Certificate),
|
||||
}
|
||||
}
|
||||
|
||||
// TableName returns the table name.
|
||||
func (c *Certificate) TableName() string {
|
||||
if c.tableName != "" {
|
||||
return c.tableName
|
||||
}
|
||||
return certificateTableName
|
||||
}
|
||||
|
||||
// SetTableName sets the table name.
|
||||
func (c *Certificate) SetTableName(n string) {
|
||||
c.tableName = n
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/auth/ldap/store"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func TestNewCertificate(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCtx := context.Background()
|
||||
_, testCertEncoded := TestGenerateCA(t, "localhost")
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
authMethodId string
|
||||
certPem string
|
||||
want *Certificate
|
||||
wantErr bool
|
||||
wantErrCode errors.Code
|
||||
wantErrContains string
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
ctx: testCtx,
|
||||
authMethodId: "test-id",
|
||||
certPem: testCertEncoded,
|
||||
want: &Certificate{
|
||||
Certificate: &store.Certificate{
|
||||
LdapMethodId: "test-id",
|
||||
Cert: testCertEncoded,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing-auth-method-id",
|
||||
ctx: testCtx,
|
||||
certPem: testCertEncoded,
|
||||
wantErr: true,
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
wantErrContains: "missing ldap auth method id",
|
||||
},
|
||||
{
|
||||
name: "missing-cert",
|
||||
ctx: testCtx,
|
||||
authMethodId: "test-id",
|
||||
wantErr: true,
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
wantErrContains: "missing certificate",
|
||||
},
|
||||
{
|
||||
name: "invalid-cert",
|
||||
ctx: testCtx,
|
||||
authMethodId: "test-id",
|
||||
certPem: "not-a-cert",
|
||||
wantErr: true,
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
wantErrContains: "failed to parse certificate: invalid PEM encoding",
|
||||
},
|
||||
{
|
||||
name: "invalid-block",
|
||||
ctx: testCtx,
|
||||
authMethodId: "test-id",
|
||||
certPem: TestInvalidPem,
|
||||
wantErr: true,
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
wantErrContains: "failed to parse certificate: invalid block",
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
gotCert, err := NewCertificate(tc.ctx, tc.authMethodId, tc.certPem)
|
||||
if tc.wantErr {
|
||||
require.Error(err)
|
||||
assert.Nil(gotCert)
|
||||
if tc.wantErrCode != errors.Unknown {
|
||||
assert.True(errors.Match(errors.T(tc.wantErrCode), err))
|
||||
}
|
||||
if tc.wantErrContains != "" {
|
||||
assert.Contains(err.Error(), tc.wantErrContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCertificate_SetTableName(t *testing.T) {
|
||||
t.Parallel()
|
||||
defaultTableName := certificateTableName
|
||||
tests := []struct {
|
||||
name string
|
||||
setNameTo string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "new-name",
|
||||
setNameTo: "new-name",
|
||||
want: "new-name",
|
||||
},
|
||||
{
|
||||
name: "reset to default",
|
||||
setNameTo: "",
|
||||
want: defaultTableName,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
def := allocCertificate()
|
||||
require.Equal(defaultTableName, def.TableName())
|
||||
m := allocCertificate()
|
||||
m.SetTableName(tt.setNameTo)
|
||||
assert.Equal(tt.want, m.TableName())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCertificate_clone(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCtx := context.Background()
|
||||
t.Run("valid", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
_, testCertEncoded := TestGenerateCA(t, "localhost")
|
||||
c, err := NewCertificate(testCtx, "test-id", testCertEncoded)
|
||||
require.NoError(err)
|
||||
cp := c.clone()
|
||||
assert.True(proto.Equal(cp.Certificate, c.Certificate))
|
||||
})
|
||||
t.Run("not-equal", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
_, testCertEncoded := TestGenerateCA(t, "localhost")
|
||||
c, err := NewCertificate(testCtx, "test-id", testCertEncoded)
|
||||
require.NoError(err)
|
||||
|
||||
_, testCertEncoded2 := TestGenerateCA(t, "alice.com")
|
||||
c2, err := NewCertificate(testCtx, "test-id", testCertEncoded2)
|
||||
require.NoError(err)
|
||||
|
||||
cp := c.clone()
|
||||
assert.True(!proto.Equal(cp.Certificate, c2.Certificate))
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
)
|
||||
|
||||
// EncodeCertificates will encode a number of x509 certificates to PEMs.
|
||||
func EncodeCertificates(ctx context.Context, certs ...*x509.Certificate) ([]string, error) {
|
||||
const op = "ldap.EncodeCertificates"
|
||||
if len(certs) == 0 {
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "no certs provided")
|
||||
}
|
||||
var pems []string
|
||||
for _, cert := range certs {
|
||||
if cert == nil {
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "nil cert")
|
||||
}
|
||||
var buffer bytes.Buffer
|
||||
err := pem.Encode(&buffer, &pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: cert.Raw,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "failed to encode cert: "+err.Error(), errors.WithWrap(err))
|
||||
}
|
||||
pems = append(pems, buffer.String())
|
||||
}
|
||||
return pems, nil
|
||||
}
|
||||
|
||||
// ParseCertificates will parse a number of certificates PEMs to x509s.
|
||||
func ParseCertificates(ctx context.Context, pems ...string) ([]*x509.Certificate, error) {
|
||||
const op = "ldap.ParseCertificates"
|
||||
if len(pems) == 0 {
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "no PEMs provided")
|
||||
}
|
||||
var certs []*x509.Certificate
|
||||
for _, p := range pems {
|
||||
if p == "" {
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "empty certificate PEM")
|
||||
}
|
||||
block, _ := pem.Decode([]byte(p))
|
||||
if block == nil {
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "failed to parse certificate: invalid PEM encoding")
|
||||
}
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("failed to parse certificate: invalid block: %s", err.Error()), errors.WithWrap(err))
|
||||
}
|
||||
certs = append(certs, cert)
|
||||
}
|
||||
return certs, nil
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestEncodeCertificates(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func() ([]*x509.Certificate, []string)
|
||||
wantErr bool
|
||||
wantErrCode errors.Code
|
||||
wantErrContains string
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
setup: func() ([]*x509.Certificate, []string) {
|
||||
c1, p1 := TestGenerateCA(t, "localhost")
|
||||
c2, p2 := TestGenerateCA(t, "alice.com")
|
||||
|
||||
return []*x509.Certificate{c1, c2}, []string{p1, p2}
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "empty-cert",
|
||||
setup: func() ([]*x509.Certificate, []string) {
|
||||
_, p1 := TestGenerateCA(t, "localhost")
|
||||
c2, p2 := TestGenerateCA(t, "alice.com")
|
||||
|
||||
return []*x509.Certificate{nil, c2}, []string{p1, p2}
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
wantErrContains: "nil cert",
|
||||
},
|
||||
{
|
||||
name: "nil-certs",
|
||||
setup: func() ([]*x509.Certificate, []string) {
|
||||
_, p1 := TestGenerateCA(t, "localhost")
|
||||
_, p2 := TestGenerateCA(t, "alice.com")
|
||||
|
||||
return nil, []string{p1, p2}
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
wantErrContains: "no certs provided",
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
certs, pems := tc.setup()
|
||||
got, err := EncodeCertificates(ctx, certs...)
|
||||
if tc.wantErr {
|
||||
require.Error(err)
|
||||
assert.True(errors.Match(errors.T(tc.wantErrCode), err))
|
||||
if tc.wantErrContains != "" {
|
||||
assert.Contains(err.Error(), tc.wantErrContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(pems, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseCertificates(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func() ([]*x509.Certificate, []string)
|
||||
wantErr bool
|
||||
wantErrCode errors.Code
|
||||
wantErrContains string
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
setup: func() ([]*x509.Certificate, []string) {
|
||||
c1, p1 := TestGenerateCA(t, "localhost")
|
||||
c2, p2 := TestGenerateCA(t, "alice.com")
|
||||
|
||||
return []*x509.Certificate{c1, c2}, []string{p1, p2}
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "empty-pem",
|
||||
setup: func() ([]*x509.Certificate, []string) {
|
||||
c1, _ := TestGenerateCA(t, "localhost")
|
||||
c2, p2 := TestGenerateCA(t, "alice.com")
|
||||
|
||||
return []*x509.Certificate{c1, c2}, []string{"", p2}
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "nil-pem",
|
||||
setup: func() ([]*x509.Certificate, []string) {
|
||||
c1, _ := TestGenerateCA(t, "localhost")
|
||||
c2, _ := TestGenerateCA(t, "alice.com")
|
||||
|
||||
return []*x509.Certificate{c1, c2}, nil
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
},
|
||||
{
|
||||
name: "invalid-block",
|
||||
setup: func() ([]*x509.Certificate, []string) {
|
||||
c1, _ := TestGenerateCA(t, "localhost")
|
||||
return []*x509.Certificate{c1}, []string{TestInvalidPem}
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
wantErrContains: "failed to parse certificate: invalid block",
|
||||
},
|
||||
{
|
||||
name: "invalid-pem",
|
||||
setup: func() ([]*x509.Certificate, []string) {
|
||||
c1, _ := TestGenerateCA(t, "localhost")
|
||||
return []*x509.Certificate{c1}, []string{"not-encoded"}
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
wantErrContains: "failed to parse certificate: invalid PEM encoding",
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
certs, pems := tc.setup()
|
||||
got, err := ParseCertificates(ctx, pems...)
|
||||
if tc.wantErr {
|
||||
require.Error(err)
|
||||
assert.True(errors.Match(errors.T(tc.wantErrCode), err))
|
||||
if tc.wantErrContains != "" {
|
||||
assert.Contains(err.Error(), tc.wantErrContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(certs, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/auth/ldap/store"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
|
||||
"github.com/hashicorp/go-kms-wrapping/v2/extras/structwrapping"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const clientCertificateTableName = "auth_ldap_client_certificate"
|
||||
|
||||
// ClientCertificate represents a set of optional configuration fields used for
|
||||
// specifying a mTLS client cert for LDAP connections. ClientCertificates are
|
||||
// value objects of an AuthMethod, therefore there's no need for oplog metadata,
|
||||
// since only the AuthMethod will have metadata because it's the root aggregate.
|
||||
type ClientCertificate struct {
|
||||
*store.ClientCertificate
|
||||
tableName string
|
||||
}
|
||||
|
||||
// NewClientCertificate creates a new in memory ClientCertificate. No options
|
||||
// are currently supported. PrivKey must be in PKCS #8, ASN.1 DER form. certPem
|
||||
// must be in ASN.1 DER form encoded as PEM.
|
||||
func NewClientCertificate(ctx context.Context, authMethodId string, privKey []byte, certPem string, _ ...Option) (*ClientCertificate, error) {
|
||||
const op = "ldap.NewClientCertificate"
|
||||
switch {
|
||||
case authMethodId == "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth method id")
|
||||
case len(privKey) == 0:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing key")
|
||||
case certPem == "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing certificate")
|
||||
}
|
||||
if _, err := x509.ParsePKCS8PrivateKey(privKey); err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("failed to parse key in PKCS #8, ASN.1 DER form"), errors.WithCode(errors.InvalidParameter))
|
||||
}
|
||||
blk, _ := pem.Decode([]byte(certPem))
|
||||
if blk == nil {
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "failed to parse certificate: invalid PEM encoding")
|
||||
}
|
||||
if _, err := x509.ParseCertificate(blk.Bytes); err != nil {
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("failed to parse certificate: invalid block: %s", err.Error()), errors.WithWrap(err))
|
||||
}
|
||||
return &ClientCertificate{
|
||||
ClientCertificate: &store.ClientCertificate{
|
||||
LdapMethodId: authMethodId,
|
||||
CertificateKey: privKey,
|
||||
Certificate: []byte(certPem),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// allocClientCertificate makes an empty one in memory
|
||||
func allocClientCertificate() *ClientCertificate {
|
||||
return &ClientCertificate{
|
||||
ClientCertificate: &store.ClientCertificate{},
|
||||
}
|
||||
}
|
||||
|
||||
// clone a ClientCertificate
|
||||
func (cc *ClientCertificate) clone() *ClientCertificate {
|
||||
cp := proto.Clone(cc.ClientCertificate)
|
||||
return &ClientCertificate{
|
||||
ClientCertificate: cp.(*store.ClientCertificate),
|
||||
}
|
||||
}
|
||||
|
||||
// TableName returns the table name
|
||||
func (cc *ClientCertificate) TableName() string {
|
||||
if cc.tableName != "" {
|
||||
return cc.tableName
|
||||
}
|
||||
return clientCertificateTableName
|
||||
}
|
||||
|
||||
// SetTableName sets the table name.
|
||||
func (cc *ClientCertificate) SetTableName(n string) {
|
||||
cc.tableName = n
|
||||
}
|
||||
|
||||
// encrypt the client certificate before writing it to the database
|
||||
func (cc *ClientCertificate) encrypt(ctx context.Context, cipher wrapping.Wrapper) error {
|
||||
const op = "ldap.(ClientCertificate).encrypt"
|
||||
if cipher == nil {
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "missing cipher")
|
||||
}
|
||||
if err := structwrapping.WrapStruct(ctx, cipher, cc.ClientCertificate); err != nil {
|
||||
return errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt))
|
||||
}
|
||||
var err error
|
||||
if cc.KeyId, err = cipher.KeyId(ctx); err != nil {
|
||||
return errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt), errors.WithMsg("failed to read cipher key id"))
|
||||
}
|
||||
if cc.CertificateKeyHmac, err = hmacField(ctx, cipher, cc.CertificateKey, cc.LdapMethodId); err != nil {
|
||||
return errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt), errors.WithMsg("failed to hmac client certificate"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// decrypt the client certificate after reading it from the database
|
||||
func (cc *ClientCertificate) decrypt(ctx context.Context, cipher wrapping.Wrapper) error {
|
||||
const op = "ldap.(ClientCertificate).decrypt"
|
||||
if cipher == nil {
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "missing cipher")
|
||||
}
|
||||
if err := structwrapping.UnwrapStruct(ctx, cipher, cc.ClientCertificate); err != nil {
|
||||
return errors.Wrap(ctx, err, op, errors.WithCode(errors.Decrypt))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/auth/ldap/store"
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/kms"
|
||||
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func TestNewClientCertificate(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCtx := context.Background()
|
||||
_, testCertEncoded := TestGenerateCA(t, "localhost")
|
||||
_, testPrivKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
require.NoError(t, err)
|
||||
derPrivKey, err := x509.MarshalPKCS8PrivateKey(testPrivKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
authMethodId string
|
||||
certKey []byte
|
||||
cert string
|
||||
want *ClientCertificate
|
||||
wantErr bool
|
||||
wantErrCode errors.Code
|
||||
wantErrContains string
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
ctx: testCtx,
|
||||
authMethodId: "test-id",
|
||||
cert: testCertEncoded,
|
||||
certKey: derPrivKey,
|
||||
want: &ClientCertificate{
|
||||
ClientCertificate: &store.ClientCertificate{
|
||||
LdapMethodId: "test-id",
|
||||
Certificate: []byte(testCertEncoded),
|
||||
CertificateKey: derPrivKey,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing-auth-method-id",
|
||||
ctx: testCtx,
|
||||
cert: testCertEncoded,
|
||||
certKey: derPrivKey,
|
||||
wantErr: true,
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
wantErrContains: "missing auth method id",
|
||||
},
|
||||
{
|
||||
name: "missing-cert",
|
||||
ctx: testCtx,
|
||||
authMethodId: "test-id",
|
||||
certKey: derPrivKey,
|
||||
wantErr: true,
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
wantErrContains: "missing certificate",
|
||||
},
|
||||
{
|
||||
name: "missing-key",
|
||||
ctx: testCtx,
|
||||
authMethodId: "test-id",
|
||||
cert: testCertEncoded,
|
||||
wantErr: true,
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
wantErrContains: "missing key",
|
||||
},
|
||||
{
|
||||
name: "invalid-key",
|
||||
ctx: testCtx,
|
||||
authMethodId: "test-id",
|
||||
cert: testCertEncoded,
|
||||
certKey: []byte("invalid-key"),
|
||||
wantErr: true,
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
wantErrContains: "failed to parse key in PKCS #8, ASN.1 DER form",
|
||||
},
|
||||
{
|
||||
name: "invalid-block",
|
||||
ctx: testCtx,
|
||||
authMethodId: "test-id",
|
||||
cert: TestInvalidPem,
|
||||
certKey: derPrivKey,
|
||||
wantErr: true,
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
wantErrContains: "failed to parse certificate: invalid block",
|
||||
},
|
||||
{
|
||||
name: "invalid-pem",
|
||||
ctx: testCtx,
|
||||
authMethodId: "test-id",
|
||||
cert: "not-encoded",
|
||||
certKey: derPrivKey,
|
||||
wantErr: true,
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
wantErrContains: "failed to parse certificate: invalid PEM encoding",
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
got, err := NewClientCertificate(tc.ctx, tc.authMethodId, tc.certKey, tc.cert)
|
||||
if tc.wantErr {
|
||||
require.Error(err)
|
||||
assert.Nil(got)
|
||||
if tc.wantErrCode != errors.Unknown {
|
||||
assert.True(errors.Match(errors.T(tc.wantErrCode), err))
|
||||
}
|
||||
if tc.wantErrContains != "" {
|
||||
assert.Contains(err.Error(), tc.wantErrContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientCertificate_SetTableName(t *testing.T) {
|
||||
t.Parallel()
|
||||
defaultTableName := clientCertificateTableName
|
||||
tests := []struct {
|
||||
name string
|
||||
setNameTo string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "new-name",
|
||||
setNameTo: "new-name",
|
||||
want: "new-name",
|
||||
},
|
||||
{
|
||||
name: "reset to default",
|
||||
setNameTo: "",
|
||||
want: defaultTableName,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
def := allocClientCertificate()
|
||||
require.Equal(defaultTableName, def.TableName())
|
||||
m := allocClientCertificate()
|
||||
m.SetTableName(tt.setNameTo)
|
||||
assert.Equal(tt.want, m.TableName())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientCertificate_clone(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCtx := context.Background()
|
||||
t.Run("valid", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
_, pem := TestGenerateCA(t, "localhost")
|
||||
_, privKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
require.NoError(err)
|
||||
derEncodedKey, err := x509.MarshalPKCS8PrivateKey(privKey)
|
||||
require.NoError(err)
|
||||
|
||||
cc, err := NewClientCertificate(testCtx, "test-id", derEncodedKey, pem)
|
||||
require.NoError(err)
|
||||
cp := cc.clone()
|
||||
assert.True(proto.Equal(cp.ClientCertificate, cc.ClientCertificate))
|
||||
})
|
||||
t.Run("not-equal", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
_, pem := TestGenerateCA(t, "localhost")
|
||||
_, privKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
require.NoError(err)
|
||||
derEncodedKey, err := x509.MarshalPKCS8PrivateKey(privKey)
|
||||
require.NoError(err)
|
||||
cc, err := NewClientCertificate(testCtx, "test-id", derEncodedKey, pem)
|
||||
require.NoError(err)
|
||||
|
||||
_, pem2 := TestGenerateCA(t, "localhost")
|
||||
_, privKey2, err := ed25519.GenerateKey(rand.Reader)
|
||||
require.NoError(err)
|
||||
derEncodedKey2, err := x509.MarshalPKCS8PrivateKey(privKey2)
|
||||
require.NoError(err)
|
||||
cc2, err := NewClientCertificate(testCtx, "test-id", derEncodedKey2, pem2)
|
||||
require.NoError(err)
|
||||
|
||||
cp := cc.clone()
|
||||
assert.True(!proto.Equal(cp.ClientCertificate, cc2.ClientCertificate))
|
||||
})
|
||||
}
|
||||
|
||||
func TestClientCertificate_encrypt_decrypt(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCtx := context.Background()
|
||||
testWrapper := db.TestWrapper(t)
|
||||
_, testCertEncoded := TestGenerateCA(t, "localhost")
|
||||
_, testPrivKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
require.NoError(t, err)
|
||||
derPrivKey, err := x509.MarshalPKCS8PrivateKey(testPrivKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
cipher wrapping.Wrapper
|
||||
cc *ClientCertificate
|
||||
wantErr bool
|
||||
wantErrCode errors.Code
|
||||
wantErrContains string
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
ctx: testCtx,
|
||||
cipher: testWrapper,
|
||||
cc: func() *ClientCertificate {
|
||||
testClientCert, err := NewClientCertificate(testCtx, "test-auth-method-id", derPrivKey, testCertEncoded)
|
||||
require.NoError(t, err)
|
||||
return testClientCert
|
||||
}(),
|
||||
},
|
||||
{
|
||||
name: "missing-cipher",
|
||||
ctx: testCtx,
|
||||
cc: func() *ClientCertificate {
|
||||
testClientCert, err := NewClientCertificate(testCtx, "test-auth-method-id", derPrivKey, testCertEncoded)
|
||||
require.NoError(t, err)
|
||||
return testClientCert
|
||||
}(),
|
||||
wantErr: true,
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
wantErrContains: "missing cipher",
|
||||
},
|
||||
{
|
||||
name: "encrypt-err",
|
||||
ctx: testCtx,
|
||||
cipher: &kms.MockWrapper{
|
||||
Wrapper: testWrapper,
|
||||
EncryptErr: fmt.Errorf("test encrypt error"),
|
||||
},
|
||||
cc: func() *ClientCertificate {
|
||||
testClientCert, err := NewClientCertificate(testCtx, "test-auth-method-id", derPrivKey, testCertEncoded)
|
||||
require.NoError(t, err)
|
||||
return testClientCert
|
||||
}(),
|
||||
wantErr: true,
|
||||
wantErrCode: errors.Encrypt,
|
||||
wantErrContains: "test encrypt error",
|
||||
},
|
||||
{
|
||||
name: "keyId-err",
|
||||
ctx: testCtx,
|
||||
cipher: &kms.MockWrapper{
|
||||
Wrapper: testWrapper,
|
||||
KeyIdErr: fmt.Errorf("test key id error"),
|
||||
},
|
||||
cc: func() *ClientCertificate {
|
||||
testClientCert, err := NewClientCertificate(testCtx, "test-auth-method-id", derPrivKey, testCertEncoded)
|
||||
require.NoError(t, err)
|
||||
return testClientCert
|
||||
}(),
|
||||
wantErr: true,
|
||||
wantErrCode: errors.Encrypt,
|
||||
wantErrContains: "test key id error",
|
||||
},
|
||||
{
|
||||
name: "keyBytes-err",
|
||||
ctx: testCtx,
|
||||
cipher: &kms.MockWrapper{
|
||||
Wrapper: testWrapper,
|
||||
// KeyBytesErr: fmt.Errorf("test key bytes error"),
|
||||
},
|
||||
cc: func() *ClientCertificate {
|
||||
testClientCert, err := NewClientCertificate(testCtx, "test-auth-method-id", derPrivKey, testCertEncoded)
|
||||
require.NoError(t, err)
|
||||
return testClientCert
|
||||
}(),
|
||||
wantErr: true,
|
||||
wantErrCode: errors.Encrypt,
|
||||
wantErrContains: "failed to hmac client certificate",
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
require.Empty(tc.cc.CtCertificateKey)
|
||||
require.Empty(tc.cc.CertificateKeyHmac)
|
||||
|
||||
err := tc.cc.encrypt(tc.ctx, tc.cipher)
|
||||
if tc.wantErr {
|
||||
require.Error(err)
|
||||
if tc.wantErrCode != errors.Unknown {
|
||||
assert.True(errors.Match(errors.T(tc.wantErrCode), err))
|
||||
}
|
||||
if tc.wantErrContains != "" {
|
||||
assert.Contains(err.Error(), tc.wantErrContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.NotEmpty(tc.cc.GetCertificateKeyHmac())
|
||||
assert.NotEmpty(tc.cc.GetCtCertificateKey())
|
||||
|
||||
origKey := make([]byte, len(tc.cc.CertificateKey))
|
||||
copy(origKey, tc.cc.CertificateKey)
|
||||
tc.cc.CertificateKey = nil
|
||||
require.NoError(tc.cc.decrypt(tc.ctx, tc.cipher))
|
||||
assert.NotEmpty(tc.cc.CertificateKey)
|
||||
assert.Equal(origKey, tc.cc.CertificateKey)
|
||||
})
|
||||
}
|
||||
t.Run("decrypt-missing-cipher", func(t *testing.T) {
|
||||
testBindCred, err := NewClientCertificate(testCtx, "test-id", derPrivKey, testCertEncoded)
|
||||
require.NoError(t, err)
|
||||
err = testBindCred.decrypt(testCtx, nil)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "missing cipher")
|
||||
})
|
||||
t.Run("decrypt-err", func(t *testing.T) {
|
||||
w := &kms.MockWrapper{
|
||||
Wrapper: testWrapper,
|
||||
DecryptErr: fmt.Errorf("test decrypt error"),
|
||||
}
|
||||
testBindCred, err := NewClientCertificate(testCtx, "test-id", derPrivKey, testCertEncoded)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, testBindCred.encrypt(testCtx, testWrapper))
|
||||
err = testBindCred.decrypt(testCtx, w)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "test decrypt error")
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/auth/ldap/store"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const groupEntrySearchConfTableName = "auth_ldap_group_entry_search"
|
||||
|
||||
// GroupEntrySearchConf represent a set of optional configuration fields used to
|
||||
// search for group entries. It is assigned to an LDAP AuthMethod and
|
||||
// updates/deletes to that AuthMethod are cascaded to its GroupEntrySearchConf.
|
||||
// GroupEntrySearchConf are value objects of an AuthMethod, therefore there's no
|
||||
// need for oplog metadata, since only the AuthMethod will have metadata because
|
||||
// it's the root aggregate.
|
||||
type GroupEntrySearchConf struct {
|
||||
*store.GroupEntrySearchConf
|
||||
tableName string
|
||||
}
|
||||
|
||||
// NewGroupEntrySearchConf creates a new in memory NewGroupEntrySearchConf.
|
||||
// Supported options are: WithGroupDn, WithGroupAttr, WithGroupFilter and all
|
||||
// other options are ignored.
|
||||
func NewGroupEntrySearchConf(ctx context.Context, authMethodId string, opt ...Option) (*GroupEntrySearchConf, error) {
|
||||
const op = "ldap.NewGroupEntrySearchConf"
|
||||
opts, err := getOpts(opt...)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
switch {
|
||||
case authMethodId == "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth method id")
|
||||
case opts.withGroupDn == "" && opts.withGroupAttr == "" && opts.withGroupFilter == "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "you must supply either dn, attr, or filter")
|
||||
}
|
||||
return &GroupEntrySearchConf{
|
||||
GroupEntrySearchConf: &store.GroupEntrySearchConf{
|
||||
LdapMethodId: authMethodId,
|
||||
GroupDn: opts.withGroupDn,
|
||||
GroupAttr: opts.withGroupAttr,
|
||||
GroupFilter: opts.withGroupFilter,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// allocGroupEntrySearchConf makes an empty one in memory
|
||||
func allocGroupEntrySearchConf() *GroupEntrySearchConf {
|
||||
return &GroupEntrySearchConf{
|
||||
GroupEntrySearchConf: &store.GroupEntrySearchConf{},
|
||||
}
|
||||
}
|
||||
|
||||
// clone a GroupEntrySearchConf
|
||||
func (gc *GroupEntrySearchConf) clone() *GroupEntrySearchConf {
|
||||
cp := proto.Clone(gc.GroupEntrySearchConf)
|
||||
return &GroupEntrySearchConf{
|
||||
GroupEntrySearchConf: cp.(*store.GroupEntrySearchConf),
|
||||
}
|
||||
}
|
||||
|
||||
// TableName returns the table name
|
||||
func (gc *GroupEntrySearchConf) TableName() string {
|
||||
if gc.tableName != "" {
|
||||
return gc.tableName
|
||||
}
|
||||
return groupEntrySearchConfTableName
|
||||
}
|
||||
|
||||
// SetTableName sets the table name.
|
||||
func (gc *GroupEntrySearchConf) SetTableName(n string) {
|
||||
gc.tableName = n
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/auth/ldap/store"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func TestNewGroupEntrySearchConf(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCtx := context.Background()
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
authMethodId string
|
||||
opts []Option
|
||||
want *GroupEntrySearchConf
|
||||
wantErr bool
|
||||
wantErrCode errors.Code
|
||||
wantErrContains string
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
ctx: testCtx,
|
||||
authMethodId: "test-id",
|
||||
opts: []Option{
|
||||
WithGroupDn(testCtx, "dn"),
|
||||
WithGroupAttr(testCtx, "attr"),
|
||||
WithGroupFilter(testCtx, "filter"),
|
||||
},
|
||||
want: &GroupEntrySearchConf{
|
||||
GroupEntrySearchConf: &store.GroupEntrySearchConf{
|
||||
LdapMethodId: "test-id",
|
||||
GroupDn: "dn",
|
||||
GroupAttr: "attr",
|
||||
GroupFilter: "filter",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "just-dn",
|
||||
ctx: testCtx,
|
||||
authMethodId: "test-id",
|
||||
opts: []Option{
|
||||
WithGroupDn(testCtx, "dn"),
|
||||
},
|
||||
want: &GroupEntrySearchConf{
|
||||
GroupEntrySearchConf: &store.GroupEntrySearchConf{
|
||||
LdapMethodId: "test-id",
|
||||
GroupDn: "dn",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "just-attr",
|
||||
ctx: testCtx,
|
||||
authMethodId: "test-id",
|
||||
opts: []Option{
|
||||
WithGroupAttr(testCtx, "attr"),
|
||||
},
|
||||
want: &GroupEntrySearchConf{
|
||||
GroupEntrySearchConf: &store.GroupEntrySearchConf{
|
||||
LdapMethodId: "test-id",
|
||||
GroupAttr: "attr",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "just-filter",
|
||||
ctx: testCtx,
|
||||
authMethodId: "test-id",
|
||||
opts: []Option{
|
||||
WithGroupFilter(testCtx, "filter"),
|
||||
},
|
||||
want: &GroupEntrySearchConf{
|
||||
GroupEntrySearchConf: &store.GroupEntrySearchConf{
|
||||
LdapMethodId: "test-id",
|
||||
GroupFilter: "filter",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing-auth-method-id",
|
||||
ctx: testCtx,
|
||||
opts: []Option{WithGroupDn(testCtx, "dn")},
|
||||
wantErr: true,
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
wantErrContains: "missing auth method id",
|
||||
},
|
||||
{
|
||||
name: "no-opts",
|
||||
ctx: testCtx,
|
||||
authMethodId: "test-id",
|
||||
wantErr: true,
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
wantErrContains: "you must supply either dn, attr, or filter",
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
got, err := NewGroupEntrySearchConf(tc.ctx, tc.authMethodId, tc.opts...)
|
||||
if tc.wantErr {
|
||||
require.Error(err)
|
||||
assert.Nil(got)
|
||||
if tc.wantErrCode != errors.Unknown {
|
||||
assert.True(errors.Match(errors.T(tc.wantErrCode), err))
|
||||
}
|
||||
if tc.wantErrContains != "" {
|
||||
assert.Contains(err.Error(), tc.wantErrContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupEntrySearchConf_SetTableName(t *testing.T) {
|
||||
t.Parallel()
|
||||
defaultTableName := groupEntrySearchConfTableName
|
||||
tests := []struct {
|
||||
name string
|
||||
setNameTo string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "new-name",
|
||||
setNameTo: "new-name",
|
||||
want: "new-name",
|
||||
},
|
||||
{
|
||||
name: "reset to default",
|
||||
setNameTo: "",
|
||||
want: defaultTableName,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
def := allocGroupEntrySearchConf()
|
||||
require.Equal(defaultTableName, def.TableName())
|
||||
m := allocGroupEntrySearchConf()
|
||||
m.SetTableName(tt.setNameTo)
|
||||
assert.Equal(tt.want, m.TableName())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupEntrySearchConf_clone(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCtx := context.Background()
|
||||
t.Run("valid", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
uc, err := NewGroupEntrySearchConf(testCtx, "test-id", WithGroupDn(testCtx, "dn"))
|
||||
require.NoError(err)
|
||||
cp := uc.clone()
|
||||
assert.True(proto.Equal(cp.GroupEntrySearchConf, uc.GroupEntrySearchConf))
|
||||
})
|
||||
t.Run("not-equal", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
uc, err := NewGroupEntrySearchConf(testCtx, "test-id", WithGroupDn(testCtx, "dn"))
|
||||
require.NoError(err)
|
||||
|
||||
uc2, err := NewGroupEntrySearchConf(testCtx, "test-id", WithGroupDn(testCtx, "dn2"))
|
||||
require.NoError(err)
|
||||
|
||||
cp := uc.clone()
|
||||
assert.True(!proto.Equal(cp.GroupEntrySearchConf, uc2.GroupEntrySearchConf))
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hashicorp/boundary/globals"
|
||||
"github.com/hashicorp/boundary/internal/auth"
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/types/subtypes"
|
||||
)
|
||||
|
||||
func init() {
|
||||
if err := subtypes.Register(auth.Domain, Subtype, globals.LdapAuthMethodPrefix, globals.LdapAccountPrefix, globals.LdapManagedGroupPrefix); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
Subtype = subtypes.Subtype("ldap")
|
||||
)
|
||||
|
||||
func newAuthMethodId(ctx context.Context) (string, error) {
|
||||
const op = "ldap.newAuthMethodId"
|
||||
id, err := db.NewPublicId(globals.LdapAuthMethodPrefix)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(ctx, err, op)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func newAccountId(ctx context.Context, authMethodId, loginName string) (string, error) {
|
||||
const op = "ldap.newAccountId"
|
||||
// there's a unique index on: auth method id + login name
|
||||
id, err := db.NewPublicId(globals.LdapAccountPrefix, db.WithPrngValues([]string{authMethodId, loginName}))
|
||||
if err != nil {
|
||||
return "", errors.Wrap(ctx, err, op)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func newManagedGroupId(ctx context.Context) (string, error) {
|
||||
const op = "ldap.newManagedGroupId"
|
||||
id, err := db.NewPublicId(globals.LdapManagedGroupPrefix)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(ctx, err, op)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/auth/ldap/store"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/oplog"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// managedGroupTableName defines the default table name for a Managed Group
|
||||
const managedGroupTableName = "auth_ldap_managed_group"
|
||||
|
||||
// ManagedGroup contains an LDAP managed group. It is assigned to an LDAP AuthMethod
|
||||
// and updates/deletes to that AuthMethod are cascaded to its Managed Groups.
|
||||
type ManagedGroup struct {
|
||||
*store.ManagedGroup
|
||||
tableName string
|
||||
}
|
||||
|
||||
// NewManagedGroup creates a new in memory ManagedGroup assigned to LDAP
|
||||
// AuthMethod. Supported options are WithName and WithDescription.
|
||||
func NewManagedGroup(ctx context.Context, authMethodId string, groupNames []string, opt ...Option) (*ManagedGroup, error) {
|
||||
const op = "ldap.NewManagedGroup"
|
||||
switch {
|
||||
case authMethodId == "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth method id")
|
||||
case len(groupNames) == 0:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing group names")
|
||||
}
|
||||
opts, err := getOpts(opt...)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
n, err := json.Marshal(groupNames)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to marshal group names"))
|
||||
}
|
||||
mg := &ManagedGroup{
|
||||
ManagedGroup: &store.ManagedGroup{
|
||||
AuthMethodId: authMethodId,
|
||||
Name: opts.withName,
|
||||
Description: opts.withDescription,
|
||||
GroupNames: string(n),
|
||||
},
|
||||
}
|
||||
return mg, nil
|
||||
}
|
||||
|
||||
// AllocManagedGroup makes an empty one in memory
|
||||
func AllocManagedGroup() *ManagedGroup {
|
||||
return &ManagedGroup{
|
||||
ManagedGroup: &store.ManagedGroup{},
|
||||
}
|
||||
}
|
||||
|
||||
// clone a ManagedGroup.
|
||||
func (mg *ManagedGroup) clone() *ManagedGroup {
|
||||
cp := proto.Clone(mg.ManagedGroup)
|
||||
return &ManagedGroup{
|
||||
ManagedGroup: cp.(*store.ManagedGroup),
|
||||
}
|
||||
}
|
||||
|
||||
// TableName returns the table name.
|
||||
func (mg *ManagedGroup) TableName() string {
|
||||
if mg.tableName != "" {
|
||||
return mg.tableName
|
||||
}
|
||||
return managedGroupTableName
|
||||
}
|
||||
|
||||
// SetTableName sets the table name.
|
||||
func (mg *ManagedGroup) SetTableName(n string) {
|
||||
mg.tableName = n
|
||||
}
|
||||
|
||||
// oplog will create oplog metadata for the ManagedGroup.
|
||||
func (mg *ManagedGroup) oplog(ctx context.Context, opType oplog.OpType, authMethodScopeId string) (oplog.Metadata, error) {
|
||||
const op = "ldap.(ManagedGroup).oplog"
|
||||
switch {
|
||||
case mg == nil:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing managed group")
|
||||
case opType == oplog.OpType_OP_TYPE_UNSPECIFIED:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing op type")
|
||||
case mg.PublicId == "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing public id")
|
||||
case mg.AuthMethodId == "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth method id")
|
||||
case authMethodScopeId == "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing scope id")
|
||||
}
|
||||
metadata := oplog.Metadata{
|
||||
"resource-public-id": []string{mg.PublicId},
|
||||
"resource-type": []string{"ldap managed group"},
|
||||
"op-type": []string{opType.String()},
|
||||
"scope-id": []string{authMethodScopeId},
|
||||
"auth-method-id": []string{mg.AuthMethodId},
|
||||
}
|
||||
return metadata, nil
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/auth/ldap/store"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/oplog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewManagedGroup(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCtx := context.Background()
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
authMethodId string
|
||||
groupNames []string
|
||||
opt []Option
|
||||
want *ManagedGroup
|
||||
wantErrMatch *errors.Template
|
||||
wantErrContains string
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
ctx: testCtx,
|
||||
authMethodId: "test-auth-method-id",
|
||||
groupNames: []string{"admin"},
|
||||
opt: []Option{WithName(testCtx, "success"), WithDescription(testCtx, "description")},
|
||||
want: &ManagedGroup{
|
||||
ManagedGroup: &store.ManagedGroup{
|
||||
Name: "success",
|
||||
Description: "description",
|
||||
AuthMethodId: "test-auth-method-id",
|
||||
GroupNames: TestEncodedGrpNames(t, "admin"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing-auth-method-id",
|
||||
ctx: testCtx,
|
||||
groupNames: []string{"admin"},
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing auth method id",
|
||||
},
|
||||
{
|
||||
name: "missing-group-names",
|
||||
ctx: testCtx,
|
||||
authMethodId: "test-auth-method-id",
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing group names",
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
got, err := NewManagedGroup(tc.ctx, tc.authMethodId, tc.groupNames, tc.opt...)
|
||||
if tc.wantErrMatch != nil {
|
||||
require.Error(err)
|
||||
assert.Nil(got)
|
||||
assert.True(errors.Match(tc.wantErrMatch, err))
|
||||
if tc.wantErrContains != "" {
|
||||
assert.Contains(err.Error(), tc.wantErrContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagedGroup_SetTableName(t *testing.T) {
|
||||
t.Parallel()
|
||||
defaultTableName := managedGroupTableName
|
||||
tests := []struct {
|
||||
name string
|
||||
setNameTo string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "new-name",
|
||||
setNameTo: "new-name",
|
||||
want: "new-name",
|
||||
},
|
||||
{
|
||||
name: "reset to default",
|
||||
setNameTo: "",
|
||||
want: defaultTableName,
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
def := AllocManagedGroup()
|
||||
require.Equal(defaultTableName, def.TableName())
|
||||
m := AllocManagedGroup()
|
||||
m.SetTableName(tc.setNameTo)
|
||||
assert.Equal(tc.want, m.TableName())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagedGroup_oplog(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCtx := context.Background()
|
||||
testMg, err := NewManagedGroup(testCtx, "test-id", []string{"admin"})
|
||||
testMg.PublicId = "test-public-id"
|
||||
require.NoError(t, err)
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
mg *ManagedGroup
|
||||
opType oplog.OpType
|
||||
scopeId string
|
||||
want oplog.Metadata
|
||||
wantErrMatch *errors.Template
|
||||
wantErrContains string
|
||||
}{
|
||||
{
|
||||
name: "create",
|
||||
ctx: testCtx,
|
||||
mg: testMg,
|
||||
opType: oplog.OpType_OP_TYPE_CREATE,
|
||||
scopeId: "global",
|
||||
want: oplog.Metadata{
|
||||
"auth-method-id": {"test-id"},
|
||||
"resource-public-id": {"test-public-id"},
|
||||
"scope-id": {"global"},
|
||||
"op-type": {oplog.OpType_OP_TYPE_CREATE.String()},
|
||||
"resource-type": {"ldap managed group"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "update",
|
||||
ctx: testCtx,
|
||||
mg: testMg,
|
||||
opType: oplog.OpType_OP_TYPE_UPDATE,
|
||||
scopeId: "global",
|
||||
want: oplog.Metadata{
|
||||
"auth-method-id": {"test-id"},
|
||||
"resource-public-id": {"test-public-id"},
|
||||
"scope-id": {"global"},
|
||||
"op-type": {oplog.OpType_OP_TYPE_UPDATE.String()},
|
||||
"resource-type": {"ldap managed group"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing-auth-method-id",
|
||||
ctx: testCtx,
|
||||
mg: func() *ManagedGroup {
|
||||
cp := testMg.clone()
|
||||
cp.AuthMethodId = ""
|
||||
return cp
|
||||
}(),
|
||||
opType: oplog.OpType_OP_TYPE_UPDATE,
|
||||
scopeId: "global",
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing auth method id",
|
||||
},
|
||||
{
|
||||
name: "missing-scope-id",
|
||||
ctx: testCtx,
|
||||
mg: testMg,
|
||||
opType: oplog.OpType_OP_TYPE_UPDATE,
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing scope id",
|
||||
},
|
||||
{
|
||||
name: "missing-public-id",
|
||||
ctx: testCtx,
|
||||
mg: func() *ManagedGroup {
|
||||
cp := testMg.clone()
|
||||
cp.PublicId = ""
|
||||
return cp
|
||||
}(),
|
||||
opType: oplog.OpType_OP_TYPE_UPDATE,
|
||||
scopeId: "global",
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing public id",
|
||||
},
|
||||
{
|
||||
name: "missing-op-type",
|
||||
ctx: testCtx,
|
||||
mg: testMg,
|
||||
scopeId: "global",
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing op type",
|
||||
},
|
||||
{
|
||||
name: "missing-managed-group",
|
||||
ctx: testCtx,
|
||||
opType: oplog.OpType_OP_TYPE_UPDATE,
|
||||
scopeId: "global",
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing managed group",
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
got, err := tc.mg.oplog(tc.ctx, tc.opType, tc.scopeId)
|
||||
if tc.wantErrMatch != nil {
|
||||
require.Error(err)
|
||||
assert.Nil(got)
|
||||
assert.True(errors.Match(tc.wantErrMatch, err))
|
||||
if tc.wantErrContains != "" {
|
||||
assert.Contains(err.Error(), tc.wantErrContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,365 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
)
|
||||
|
||||
type options struct {
|
||||
withName string
|
||||
withDescription string
|
||||
withFullName string
|
||||
withEmail string
|
||||
withDn string
|
||||
withStartTls bool
|
||||
withInsecureTls bool
|
||||
withDiscoverDn bool
|
||||
withAnonGroupSearch bool
|
||||
withEnableGroups bool
|
||||
withUseTokenGroups bool
|
||||
withUpnDomain string
|
||||
withUserDn string
|
||||
withUserAttr string
|
||||
withUserFilter string
|
||||
withGroupDn string
|
||||
withGroupAttr string
|
||||
withGroupFilter string
|
||||
withCertificates []string
|
||||
withBindDn string
|
||||
withBindPassword string
|
||||
withClientCertificate string
|
||||
withClientCertificateKey []byte
|
||||
withLimit int
|
||||
withUnauthenticatedUser bool
|
||||
withOrderByCreateTime bool
|
||||
ascending bool
|
||||
withOperationalState AuthMethodState
|
||||
withAccountAttributeMap map[string]AccountToAttribute
|
||||
withMemberOfGroups string
|
||||
withUrls []string
|
||||
}
|
||||
|
||||
// Option - how options are passed as args
|
||||
type Option func(*options) error
|
||||
|
||||
func getDefaultOptions() options {
|
||||
return options{
|
||||
withOperationalState: InactiveState,
|
||||
}
|
||||
}
|
||||
|
||||
func getOpts(opt ...Option) (options, error) {
|
||||
opts := getDefaultOptions()
|
||||
|
||||
for _, o := range opt {
|
||||
if err := o(&opts); err != nil {
|
||||
return opts, err
|
||||
}
|
||||
}
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
// WithUrls provides optional urls for the auth method.
|
||||
func WithUrls(ctx context.Context, urls ...*url.URL) Option {
|
||||
const op = "ldap.WithUrls"
|
||||
return func(o *options) error {
|
||||
o.withUrls = make([]string, 0, len(urls))
|
||||
for _, u := range urls {
|
||||
switch u.Scheme {
|
||||
case "ldap", "ldaps":
|
||||
default:
|
||||
return errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("%s scheme in url %q is not either ldap or ldaps", u.Scheme, u.String()))
|
||||
}
|
||||
o.withUrls = append(o.withUrls, u.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithEmail provides an optional email address for the account.
|
||||
func WithEmail(_ context.Context, email string) Option {
|
||||
return func(o *options) error {
|
||||
o.withEmail = email
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithFullName provides an optional full name for the account.
|
||||
func WithFullName(_ context.Context, n string) Option {
|
||||
return func(o *options) error {
|
||||
o.withFullName = n
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithDn provides an optional distinguished name
|
||||
func WithDn(ctx context.Context, dn string) Option {
|
||||
const op = "ldap.WithDn"
|
||||
return func(o *options) error {
|
||||
o.withDn = dn
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithName provides an optional name.
|
||||
func WithName(_ context.Context, n string) Option {
|
||||
return func(o *options) error {
|
||||
o.withName = n
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithDescription provides an optional description.
|
||||
func WithDescription(_ context.Context, desc string) Option {
|
||||
return func(o *options) error {
|
||||
o.withDescription = desc
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithStartTLS optionally enables a StartTLS command after establishing an
|
||||
// unencrypted connection.
|
||||
func WithStartTLS(_ context.Context) Option {
|
||||
return func(o *options) error {
|
||||
o.withStartTls = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithEnableGroups optionally enables an authenticated user's groups will be
|
||||
// found during authentication.
|
||||
func WithEnableGroups(_ context.Context) Option {
|
||||
return func(o *options) error {
|
||||
o.withEnableGroups = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithUseTokenGroups optionally enables the use the Active Directory
|
||||
// tokenGroups constructed attribute of the user to find the group memberships.
|
||||
// This will find all security groups including nested ones,
|
||||
func WithUseTokenGroups(_ context.Context) Option {
|
||||
return func(o *options) error {
|
||||
o.withUseTokenGroups = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithInsecureTLS optional specifies to skip LDAP server SSL certificate
|
||||
// validation - insecure and use with caution
|
||||
func WithInsecureTLS(_ context.Context) Option {
|
||||
return func(o *options) error {
|
||||
o.withInsecureTls = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithDiscoverDn optionally specifies to use anon bind to discover the bind DN
|
||||
// of a user.
|
||||
func WithDiscoverDn(_ context.Context) Option {
|
||||
return func(o *options) error {
|
||||
o.withDiscoverDn = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithAnonGroupSearch optionally specifies to use anon bind when performing LDAP
|
||||
// group searches
|
||||
func WithAnonGroupSearch(_ context.Context) Option {
|
||||
return func(o *options) error {
|
||||
o.withAnonGroupSearch = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithUpnDomain optionally specifies the userPrincipalDomain used to construct
|
||||
// the UPN string for the authenticating user. The constructed UPN will appear
|
||||
// as [username]@UPNDomain Example: example.com, which will cause Boundary to
|
||||
// bind as username@example.com when authenticating the user.
|
||||
func WithUpnDomain(_ context.Context, domain string) Option {
|
||||
return func(o *options) error {
|
||||
o.withUpnDomain = domain
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithUserDn optionally specifies a user dn used to search for user entries.
|
||||
func WithUserDn(_ context.Context, dn string) Option {
|
||||
return func(o *options) error {
|
||||
o.withUserDn = dn
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithUserAttr optionally specifies a user attr used to search for user entries.
|
||||
func WithUserAttr(_ context.Context, attr string) Option {
|
||||
return func(o *options) error {
|
||||
o.withUserAttr = attr
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithUserFilter optionally specifies a user filter used to search for user entries.
|
||||
func WithUserFilter(_ context.Context, filter string) Option {
|
||||
return func(o *options) error {
|
||||
o.withUserFilter = filter
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithGroupDn optionally specifies a group dn used to search for group entries.
|
||||
func WithGroupDn(_ context.Context, dn string) Option {
|
||||
return func(o *options) error {
|
||||
o.withGroupDn = dn
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithGroupAttr optionally specifies a group attr used to search for group entries.
|
||||
func WithGroupAttr(_ context.Context, attr string) Option {
|
||||
return func(o *options) error {
|
||||
o.withGroupAttr = attr
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithGroupFilter optionally specifies a group filter used to search for group entries.
|
||||
func WithGroupFilter(_ context.Context, filter string) Option {
|
||||
return func(o *options) error {
|
||||
o.withGroupFilter = filter
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithBindCredential optionally specifies a set of optional configuration
|
||||
// parameters which allow Boundary to bind (aka authenticate) using the
|
||||
// credentials provided when searching for the user entry used to authenticate
|
||||
// the end user.
|
||||
func WithBindCredential(ctx context.Context, dn, password string) Option {
|
||||
const op = "ldap.WithBindCredential"
|
||||
return func(o *options) error {
|
||||
switch {
|
||||
case dn == "" && password == "":
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "missing both dn and password")
|
||||
case dn != "" && password == "":
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "missing password")
|
||||
case dn == "" && password != "":
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "missing dn")
|
||||
}
|
||||
o.withBindDn = dn
|
||||
o.withBindPassword = password
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithCertificates provides optional certificates.
|
||||
func WithCertificates(ctx context.Context, certs ...*x509.Certificate) Option {
|
||||
const op = "ldap.WithCertificates"
|
||||
return func(o *options) error {
|
||||
if len(certs) > 0 {
|
||||
o.withCertificates = make([]string, 0, len(certs))
|
||||
pem, err := EncodeCertificates(ctx, certs...)
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
o.withCertificates = append(o.withCertificates, pem...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithClientCertificate provides optional configuration fields used for
|
||||
// specifying a mTLS client cert for LDAP connections.
|
||||
func WithClientCertificate(ctx context.Context, privKey []byte, cert *x509.Certificate) Option {
|
||||
const op = "ldap.WithClientCertificate"
|
||||
return func(o *options) error {
|
||||
if privKey != nil || cert != nil {
|
||||
switch {
|
||||
case cert == nil:
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "missing certificate")
|
||||
case len(privKey) == 0:
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "missing private key")
|
||||
}
|
||||
if _, err := x509.ParsePKCS8PrivateKey(privKey); err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
o.withClientCertificateKey = privKey
|
||||
pem, err := EncodeCertificates(ctx, cert)
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
if len(pem) != 1 {
|
||||
return errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("too many client certificates (%d)", len(pem)))
|
||||
}
|
||||
o.withClientCertificate = pem[0]
|
||||
}
|
||||
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(_ context.Context, 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(_ context.Context, 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(_ context.Context, 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(_ context.Context, state AuthMethodState) Option {
|
||||
return func(o *options) error {
|
||||
o.withOperationalState = state
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithAccountAttributeMap provides an option for specifying an Account Attribute map.
|
||||
func WithAccountAttributeMap(_ context.Context, aam map[string]AccountToAttribute) Option {
|
||||
return func(o *options) error {
|
||||
o.withAccountAttributeMap = aam
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithMemberOfGroups provides an option for specifying a list of group names
|
||||
func WithMemberOfGroups(ctx context.Context, groupName ...string) Option {
|
||||
const op = "ldap.WithMemberOfGroups"
|
||||
return func(o *options) error {
|
||||
mg, err := json.Marshal(groupName)
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
o.withMemberOfGroups = string(mg)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_getOpts(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCtx := context.Background()
|
||||
t.Run("WithName", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
opts, err := getOpts(WithName(testCtx, "test"))
|
||||
require.NoError(t, err)
|
||||
testOpts := getDefaultOptions()
|
||||
assert.NotEqual(opts, testOpts)
|
||||
testOpts.withName = "test"
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
t.Run("WithDescription", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
opts, err := getOpts(WithDescription(testCtx, "test"))
|
||||
require.NoError(t, err)
|
||||
testOpts := getDefaultOptions()
|
||||
assert.NotEqual(opts, testOpts)
|
||||
testOpts.withDescription = "test"
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
t.Run("WithUrls", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
opts, err := getOpts(WithUrls(testCtx, TestConvertToUrls(t, "ldaps://ldap1")...))
|
||||
require.NoError(t, err)
|
||||
testOpts := getDefaultOptions()
|
||||
assert.NotEqual(opts, testOpts)
|
||||
testOpts.withUrls = []string{"ldaps://ldap1"}
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
t.Run("WithAccountAttributeMap", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
opts, err := getOpts(WithAccountAttributeMap(testCtx, map[string]AccountToAttribute{
|
||||
"mail": ToEmailAttribute,
|
||||
}))
|
||||
require.NoError(t, err)
|
||||
testOpts := getDefaultOptions()
|
||||
assert.NotEqual(opts, testOpts)
|
||||
testOpts.withAccountAttributeMap = map[string]AccountToAttribute{
|
||||
"mail": ToEmailAttribute,
|
||||
}
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
t.Run("WithStartTLS", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
opts, err := getOpts(WithStartTLS(testCtx))
|
||||
require.NoError(t, err)
|
||||
testOpts := getDefaultOptions()
|
||||
assert.NotEqual(opts, testOpts)
|
||||
testOpts.withStartTls = true
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
t.Run("WithInsecureTLS", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
opts, err := getOpts(WithInsecureTLS(testCtx))
|
||||
require.NoError(t, err)
|
||||
testOpts := getDefaultOptions()
|
||||
assert.NotEqual(opts, testOpts)
|
||||
testOpts.withInsecureTls = true
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
t.Run("WithDiscoverDn", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
opts, err := getOpts(WithDiscoverDn(testCtx))
|
||||
require.NoError(t, err)
|
||||
testOpts := getDefaultOptions()
|
||||
assert.NotEqual(opts, testOpts)
|
||||
testOpts.withDiscoverDn = true
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
t.Run("WithAnonGroupSearch", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
opts, err := getOpts(WithAnonGroupSearch(testCtx))
|
||||
require.NoError(t, err)
|
||||
testOpts := getDefaultOptions()
|
||||
assert.NotEqual(opts, testOpts)
|
||||
testOpts.withAnonGroupSearch = true
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
t.Run("WithEnableGroups", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
opts, err := getOpts(WithEnableGroups(testCtx))
|
||||
require.NoError(t, err)
|
||||
testOpts := getDefaultOptions()
|
||||
assert.NotEqual(opts, testOpts)
|
||||
testOpts.withEnableGroups = true
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
t.Run("WithUseTokenGroups", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
opts, err := getOpts(WithUseTokenGroups(testCtx))
|
||||
require.NoError(t, err)
|
||||
testOpts := getDefaultOptions()
|
||||
assert.NotEqual(opts, testOpts)
|
||||
testOpts.withUseTokenGroups = true
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
t.Run("WithUpnDomain", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
opts, err := getOpts(WithUpnDomain(testCtx, "domain.com"))
|
||||
require.NoError(t, err)
|
||||
testOpts := getDefaultOptions()
|
||||
assert.NotEqual(opts, testOpts)
|
||||
testOpts.withUpnDomain = "domain.com"
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
t.Run("WitUserDn", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
opts, err := getOpts(WithUserDn(testCtx, "dn"))
|
||||
require.NoError(t, err)
|
||||
testOpts := getDefaultOptions()
|
||||
assert.NotEqual(opts, testOpts)
|
||||
testOpts.withUserDn = "dn"
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
t.Run("WitUserAttr", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
opts, err := getOpts(WithUserAttr(testCtx, "attr"))
|
||||
require.NoError(t, err)
|
||||
testOpts := getDefaultOptions()
|
||||
assert.NotEqual(opts, testOpts)
|
||||
testOpts.withUserAttr = "attr"
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
t.Run("WitUserFilter", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
opts, err := getOpts(WithUserFilter(testCtx, "filter"))
|
||||
require.NoError(t, err)
|
||||
testOpts := getDefaultOptions()
|
||||
assert.NotEqual(opts, testOpts)
|
||||
testOpts.withUserFilter = "filter"
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
t.Run("WitGroupDn", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
opts, err := getOpts(WithGroupDn(testCtx, "dn"))
|
||||
require.NoError(t, err)
|
||||
testOpts := getDefaultOptions()
|
||||
assert.NotEqual(opts, testOpts)
|
||||
testOpts.withGroupDn = "dn"
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
t.Run("WithGroupAttr", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
opts, err := getOpts(WithGroupAttr(testCtx, "attr"))
|
||||
require.NoError(t, err)
|
||||
testOpts := getDefaultOptions()
|
||||
assert.NotEqual(opts, testOpts)
|
||||
testOpts.withGroupAttr = "attr"
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
t.Run("WithGroupFilter", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
opts, err := getOpts(WithGroupFilter(testCtx, "filter"))
|
||||
require.NoError(t, err)
|
||||
testOpts := getDefaultOptions()
|
||||
assert.NotEqual(opts, testOpts)
|
||||
testOpts.withGroupFilter = "filter"
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
t.Run("WithBindCredential", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
opts, err := getOpts(WithBindCredential(testCtx, "dn", "password"))
|
||||
require.NoError(t, err)
|
||||
testOpts := getDefaultOptions()
|
||||
assert.NotEqual(opts, testOpts)
|
||||
testOpts.withBindDn = "dn"
|
||||
testOpts.withBindPassword = "password"
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
t.Run("WithBindCredential-missing-dn", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
opts, err := getOpts(WithBindCredential(testCtx, "", "password"))
|
||||
require.Error(t, err)
|
||||
assert.Empty(opts.withBindDn)
|
||||
assert.Empty(opts.withBindPassword)
|
||||
})
|
||||
t.Run("WithBindCredential-missing-password", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
opts, err := getOpts(WithBindCredential(testCtx, "dn", ""))
|
||||
require.Error(t, err)
|
||||
assert.Empty(opts.withBindDn)
|
||||
assert.Empty(opts.withBindPassword)
|
||||
})
|
||||
t.Run("WithBindCredential-missing-dn-and-password", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
opts, err := getOpts(WithBindCredential(testCtx, "", ""))
|
||||
require.Error(t, err)
|
||||
assert.Empty(opts.withBindDn)
|
||||
})
|
||||
t.Run("WithCertificates", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
testCert, _ := TestGenerateCA(t, "localhost")
|
||||
testCert2, _ := TestGenerateCA(t, "127.0.0.1")
|
||||
|
||||
opts, err := getOpts(WithCertificates(testCtx, testCert, testCert2))
|
||||
require.NoError(t, err)
|
||||
testOpts := getDefaultOptions()
|
||||
encodedCerts, err := EncodeCertificates(testCtx, testCert, testCert2)
|
||||
require.NoError(t, err)
|
||||
testOpts.withCertificates = encodedCerts
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
t.Run("WithClientCertificate", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
testCert, testCertEncoded := TestGenerateCA(t, "localhost")
|
||||
_, testPrivKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
require.NoError(t, err)
|
||||
derPrivKey, err := x509.MarshalPKCS8PrivateKey(testPrivKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
opts, err := getOpts(WithClientCertificate(testCtx, derPrivKey, testCert))
|
||||
require.NoError(t, err)
|
||||
testOpts := getDefaultOptions()
|
||||
testOpts.withClientCertificate = testCertEncoded
|
||||
testOpts.withClientCertificateKey = derPrivKey
|
||||
assert.Equal(opts, testOpts)
|
||||
|
||||
// missing privKey
|
||||
_, err = getOpts(WithClientCertificate(testCtx, nil, testCert))
|
||||
require.Error(t, err)
|
||||
assert.Contains(err.Error(), "missing private key")
|
||||
|
||||
// missing cert
|
||||
_, err = getOpts(WithClientCertificate(testCtx, derPrivKey, nil))
|
||||
require.Error(t, err)
|
||||
assert.Contains(err.Error(), "missing certificate")
|
||||
|
||||
// bad privKey
|
||||
_, err = getOpts(WithClientCertificate(testCtx, []byte("not-a-kay"), testCert))
|
||||
require.Error(t, err)
|
||||
assert.Contains(err.Error(), "asn1: structure error")
|
||||
})
|
||||
t.Run("WithLimit", func(t *testing.T) {
|
||||
opts, err := getOpts(WithLimit(testCtx, 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(testCtx, 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(testCtx, 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(testCtx, ActivePublicState))
|
||||
require.NoError(t, err)
|
||||
testOpts := getDefaultOptions()
|
||||
testOpts.withOperationalState = ActivePublicState
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
t.Run("WithDn", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
opts, err := getOpts(WithDn(testCtx, "test"))
|
||||
require.NoError(t, err)
|
||||
testOpts := getDefaultOptions()
|
||||
assert.NotEqual(opts, testOpts)
|
||||
testOpts.withDn = "test"
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
t.Run("WithFullName", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
opts, err := getOpts(WithFullName(testCtx, "test"))
|
||||
require.NoError(t, err)
|
||||
testOpts := getDefaultOptions()
|
||||
assert.NotEqual(opts, testOpts)
|
||||
testOpts.withFullName = "test"
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
t.Run("WithEmail", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
opts, err := getOpts(WithEmail(testCtx, "test"))
|
||||
require.NoError(t, err)
|
||||
testOpts := getDefaultOptions()
|
||||
assert.NotEqual(opts, testOpts)
|
||||
testOpts.withEmail = "test"
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
t.Run("WithMemberOfGroups", func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
opts, err := getOpts(WithMemberOfGroups(testCtx, "test"))
|
||||
require.NoError(t, err)
|
||||
testOpts := getDefaultOptions()
|
||||
assert.NotEqual(opts, testOpts)
|
||||
testOpts.withMemberOfGroups = "[\"test\"]"
|
||||
assert.Equal(opts, testOpts)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/kms"
|
||||
"github.com/hashicorp/boundary/internal/util"
|
||||
)
|
||||
|
||||
// RepoFactory is a factory function that returns a repository and any error
|
||||
type RepoFactory func() (*Repository, error)
|
||||
|
||||
// Repository is the ldap repository
|
||||
type Repository struct {
|
||||
reader db.Reader
|
||||
writer db.Writer
|
||||
kms kms.GetWrapperer
|
||||
|
||||
// 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.GetWrapperer, 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 util.IsNil(kms) {
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/kms"
|
||||
"github.com/hashicorp/boundary/internal/oplog"
|
||||
"github.com/hashicorp/go-dbw"
|
||||
)
|
||||
|
||||
// CreateAccount inserts an Account, a, into the repository and returns a
|
||||
// new Account containing its PublicId. a is not changed. a must contain a
|
||||
// valid LdapMethodId and ScopeId. a must not contain a PublicId. The PublicId
|
||||
// is generated and assigned by this method. a must contain a valid LoginName.
|
||||
// a.LoginName must be unique for an a.AuthMethod.
|
||||
//
|
||||
// Both a.Name and a.Description are optional. If a.Name is set, it must be
|
||||
// unique within a.AuthMethodId.
|
||||
func (r *Repository) CreateAccount(ctx context.Context, a *Account, _ ...Option) (*Account, error) {
|
||||
const op = "ldap.(Repository).CreateAccount"
|
||||
switch {
|
||||
case a == nil:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing account")
|
||||
case a.Account == nil:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing embedded account")
|
||||
case a.PublicId != "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "public id must be empty")
|
||||
default:
|
||||
if err := a.validate(ctx, op); err != nil {
|
||||
return nil, err // err already wrapped
|
||||
}
|
||||
}
|
||||
id, err := newAccountId(ctx, a.AuthMethodId, a.LoginName)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
a.PublicId = id
|
||||
|
||||
oplogWrapper, err := r.kms.GetWrapper(ctx, a.ScopeId, kms.KeyPurposeOplog)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper"), errors.WithCode(errors.Encrypt))
|
||||
}
|
||||
|
||||
md, err := a.oplog(ctx, oplog.OpType_OP_TYPE_CREATE)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to generate account oplog metadata"))
|
||||
}
|
||||
var newAccount *Account
|
||||
_, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{},
|
||||
func(_ db.Reader, w db.Writer) error {
|
||||
newAccount = a.clone()
|
||||
if err := w.Create(ctx, newAccount, db.WithOplog(oplogWrapper, md)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.IsUniqueError(err):
|
||||
return nil, errors.New(ctx, errors.NotUnique, op, fmt.Sprintf(
|
||||
"in auth method %s: name %q already exists or login name %q already exists for auth method %q in scope %s",
|
||||
a.AuthMethodId, a.Name, a.LoginName, a.AuthMethodId, a.ScopeId))
|
||||
default:
|
||||
return nil, errors.Wrap(ctx, err, op, errors.WithMsg(a.AuthMethodId))
|
||||
}
|
||||
}
|
||||
return newAccount, nil
|
||||
}
|
||||
|
||||
// LookupAccount will look up an account in the repository. If the account is not
|
||||
// found, it will return nil, nil. All options are ignored.
|
||||
func (r *Repository) LookupAccount(ctx context.Context, withPublicId string, _ ...Option) (*Account, error) {
|
||||
const op = "ldap.(Repository).LookupAccount"
|
||||
if withPublicId == "" {
|
||||
return nil, errors.New(ctx, errors.InvalidPublicId, op, "missing public id")
|
||||
}
|
||||
a := AllocAccount()
|
||||
a.PublicId = withPublicId
|
||||
if err := r.reader.LookupByPublicId(ctx, a); err != nil {
|
||||
switch {
|
||||
case errors.IsNotFoundError(err):
|
||||
return nil, nil
|
||||
default:
|
||||
return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed for %s", withPublicId)))
|
||||
}
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// ListAccounts in an auth method and supports WithLimit option.
|
||||
func (r *Repository) ListAccounts(ctx context.Context, withAuthMethodId string, opt ...Option) ([]*Account, error) {
|
||||
const op = "ldap.(Repository).ListAccounts"
|
||||
if withAuthMethodId == "" {
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth method id")
|
||||
}
|
||||
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
|
||||
}
|
||||
var accts []*Account
|
||||
err = r.reader.SearchWhere(ctx, &accts, "auth_method_id = ?", []any{withAuthMethodId}, db.WithLimit(limit))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
return accts, nil
|
||||
}
|
||||
|
||||
// DeleteAccount deletes the account for the provided id from the repository returning a count of the
|
||||
// number of records deleted. All options are ignored.
|
||||
func (r *Repository) DeleteAccount(ctx context.Context, withPublicId string, _ ...Option) (int, error) {
|
||||
const op = "ldap.(Repository).DeleteAccount"
|
||||
switch {
|
||||
case withPublicId == "":
|
||||
return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing public id")
|
||||
}
|
||||
ac := AllocAccount()
|
||||
ac.PublicId = withPublicId
|
||||
|
||||
if err := r.reader.LookupById(ctx, ac); err != nil {
|
||||
return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("account not found"))
|
||||
}
|
||||
|
||||
oplogWrapper, err := r.kms.GetWrapper(ctx, ac.ScopeId, kms.KeyPurposeOplog)
|
||||
if err != nil {
|
||||
return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt), errors.WithMsg("unable to get oplog wrapper"))
|
||||
}
|
||||
metadata, err := ac.oplog(ctx, oplog.OpType_OP_TYPE_DELETE)
|
||||
if err != nil {
|
||||
return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to generate oplog metadata"))
|
||||
}
|
||||
|
||||
var rowsDeleted int
|
||||
_, err = r.writer.DoTx(
|
||||
ctx,
|
||||
db.StdRetryCnt,
|
||||
db.ExpBackoff{},
|
||||
func(_ db.Reader, w db.Writer) (err error) {
|
||||
dAc := ac.clone()
|
||||
rowsDeleted, err = w.Delete(ctx, dAc, db.WithOplog(oplogWrapper, metadata))
|
||||
switch {
|
||||
case err != nil:
|
||||
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to delete ldap account"))
|
||||
case rowsDeleted > 1:
|
||||
return errors.New(ctx, errors.MultipleRecords, op, "more than 1 resource would have been deleted")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(withPublicId))
|
||||
}
|
||||
|
||||
return rowsDeleted, nil
|
||||
}
|
||||
|
||||
// UpdateAccount updates the repository entry for a.PublicId with the
|
||||
// values in a for the fields listed in fieldMaskPaths. It returns a new
|
||||
// Account containing the updated values and a count of the number of
|
||||
// records updated. a is not changed.
|
||||
//
|
||||
// a must contain a valid PublicId. Only a.Name and a.Description can be
|
||||
// updated. If a.Name is set to a non-empty string, it must be unique within
|
||||
// a.AuthMethodId.
|
||||
//
|
||||
// An attribute of a will be set to NULL in the database if the attribute
|
||||
// in a is the zero value and it is included in fieldMaskPaths.
|
||||
func (r *Repository) UpdateAccount(ctx context.Context, scopeId string, a *Account, version uint32, fieldMaskPaths []string, opt ...Option) (*Account, int, error) {
|
||||
const op = "ldap.(Repository).UpdateAccount"
|
||||
switch {
|
||||
case a == nil:
|
||||
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing Account")
|
||||
case scopeId == "":
|
||||
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing scope id")
|
||||
case version == 0:
|
||||
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing version")
|
||||
case a.Account == nil:
|
||||
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing embedded Account")
|
||||
case a.PublicId == "":
|
||||
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidPublicId, op, "missing public id")
|
||||
}
|
||||
for _, f := range fieldMaskPaths {
|
||||
switch {
|
||||
case strings.EqualFold(NameField, f):
|
||||
case strings.EqualFold(DescriptionField, f):
|
||||
default:
|
||||
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidFieldMask, op, f)
|
||||
}
|
||||
}
|
||||
var dbMask, nullFields []string
|
||||
dbMask, nullFields = dbw.BuildUpdatePaths(
|
||||
map[string]any{
|
||||
NameField: a.Name,
|
||||
DescriptionField: a.Description,
|
||||
},
|
||||
fieldMaskPaths,
|
||||
nil,
|
||||
)
|
||||
if len(dbMask) == 0 && len(nullFields) == 0 {
|
||||
return nil, db.NoRowsAffected, errors.New(ctx, errors.EmptyFieldMask, op, "missing field mask")
|
||||
}
|
||||
|
||||
oplogWrapper, err := r.kms.GetWrapper(ctx, scopeId, kms.KeyPurposeOplog)
|
||||
if err != nil {
|
||||
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt),
|
||||
errors.WithMsg(("unable to get oplog wrapper")))
|
||||
}
|
||||
|
||||
metadata, err := a.oplog(ctx, oplog.OpType_OP_TYPE_UPDATE)
|
||||
if err != nil {
|
||||
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to generate oplog metadata"))
|
||||
}
|
||||
|
||||
var rowsUpdated int
|
||||
var returnedAccount *Account
|
||||
_, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{},
|
||||
func(_ db.Reader, w db.Writer) error {
|
||||
returnedAccount = a.clone()
|
||||
var err error
|
||||
rowsUpdated, err = w.Update(ctx, returnedAccount, dbMask, nullFields, db.WithOplog(oplogWrapper, metadata), db.WithVersion(&version))
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
if rowsUpdated > 1 {
|
||||
return errors.New(ctx, errors.MultipleRecords, op, "more than 1 resource would have been updated")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.IsUniqueError(err):
|
||||
return nil, db.NoRowsAffected, errors.New(ctx, errors.NotUnique, op,
|
||||
fmt.Sprintf("name %s already exists: %s", a.Name, a.PublicId))
|
||||
default:
|
||||
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(a.PublicId))
|
||||
}
|
||||
}
|
||||
|
||||
return returnedAccount, rowsUpdated, nil
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,171 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/kms"
|
||||
"github.com/hashicorp/boundary/internal/oplog"
|
||||
)
|
||||
|
||||
// CreateAuthMethod creates am (*AuthMethod) in the repo along with its
|
||||
// associated embedded optional value objects (urls, certs, client certs, bind
|
||||
// creds, user search conf and group search conf) and returns the newly created
|
||||
// AuthMethod (with its PublicId set)
|
||||
//
|
||||
// The AuthMethod's public id and version must be empty (zero values).
|
||||
//
|
||||
// All options are ignored.
|
||||
func (r *Repository) CreateAuthMethod(ctx context.Context, am *AuthMethod, _ ...Option) (*AuthMethod, error) {
|
||||
const op = "ldap.(Repository).CreateAuthMethod"
|
||||
switch {
|
||||
case am == nil:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth method")
|
||||
case am.PublicId != "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "public id must be empty")
|
||||
case am.Version != 0:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "version must be empty")
|
||||
case am.ScopeId == "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing scope id")
|
||||
case !validState(am.OperationalState):
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("invalid state: %q", am.OperationalState))
|
||||
case len(am.Urls) == 0:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing urls (there must be at least one)")
|
||||
}
|
||||
|
||||
var err error
|
||||
am.PublicId, err = newAuthMethodId(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
|
||||
cv, err := am.convertValueObjects(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
|
||||
dbWrapper, err := r.kms.GetWrapper(context.Background(), am.ScopeId, kms.KeyPurposeDatabase)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get database wrapper"))
|
||||
}
|
||||
|
||||
if cv.BindCredential != nil {
|
||||
bc, ok := cv.BindCredential.(*BindCredential)
|
||||
if !ok {
|
||||
return nil, errors.New(ctx, errors.Internal, op, fmt.Sprintf("invalid type (%T) is not a bind credential", cv.BindCredential))
|
||||
}
|
||||
if err := bc.encrypt(ctx, dbWrapper); err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("failed to encrypt bind credential"))
|
||||
}
|
||||
}
|
||||
if cv.ClientCertificate != nil {
|
||||
cc, ok := cv.ClientCertificate.(*ClientCertificate)
|
||||
if !ok {
|
||||
return nil, errors.New(ctx, errors.Internal, op, fmt.Sprintf("invalid type (%T) is not a client certificate", cv.ClientCertificate))
|
||||
}
|
||||
if err := cc.encrypt(ctx, dbWrapper); err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("failed to encrypt client certificate"))
|
||||
}
|
||||
}
|
||||
|
||||
oplogWrapper, err := r.kms.GetWrapper(ctx, am.ScopeId, kms.KeyPurposeOplog)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper"))
|
||||
}
|
||||
|
||||
var returnedAuthMethod *AuthMethod
|
||||
_, err = r.writer.DoTx(
|
||||
ctx,
|
||||
db.StdRetryCnt,
|
||||
db.ExpBackoff{},
|
||||
func(r db.Reader, w db.Writer) error {
|
||||
var ()
|
||||
msgs := make([]*oplog.Message, 0, 4)
|
||||
ticket, err := w.GetTicket(ctx, am)
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket"))
|
||||
}
|
||||
|
||||
returnedAuthMethod = am.clone()
|
||||
var amOplogMsg oplog.Message
|
||||
if err := w.Create(ctx, returnedAuthMethod, db.NewOplogMsg(&amOplogMsg)); err != nil {
|
||||
return err
|
||||
}
|
||||
msgs = append(msgs, &amOplogMsg)
|
||||
|
||||
urlOplogMsgs := make([]*oplog.Message, 0, len(cv.Urls))
|
||||
if err := w.CreateItems(ctx, cv.Urls, db.NewOplogMsgs(&urlOplogMsgs)); err != nil {
|
||||
return err
|
||||
}
|
||||
msgs = append(msgs, urlOplogMsgs...)
|
||||
|
||||
if len(cv.Certs) > 0 {
|
||||
certOplogMsgs := make([]*oplog.Message, 0, len(cv.Certs))
|
||||
if err := w.CreateItems(ctx, cv.Certs, db.NewOplogMsgs(&certOplogMsgs)); err != nil {
|
||||
return err
|
||||
}
|
||||
msgs = append(msgs, certOplogMsgs...)
|
||||
}
|
||||
if cv.UserEntrySearchConf != nil {
|
||||
var ucOplogMsg oplog.Message
|
||||
if err := w.Create(ctx, cv.UserEntrySearchConf, db.NewOplogMsg(&ucOplogMsg)); err != nil {
|
||||
return err
|
||||
}
|
||||
msgs = append(msgs, &ucOplogMsg)
|
||||
}
|
||||
if cv.GroupEntrySearchConf != nil {
|
||||
var gcOplogMsg oplog.Message
|
||||
if err := w.Create(ctx, cv.GroupEntrySearchConf, db.NewOplogMsg(&gcOplogMsg)); err != nil {
|
||||
return err
|
||||
}
|
||||
msgs = append(msgs, &gcOplogMsg)
|
||||
}
|
||||
if cv.ClientCertificate != nil {
|
||||
var ccOplogMsg oplog.Message
|
||||
if err := w.Create(ctx, cv.ClientCertificate, db.NewOplogMsg(&ccOplogMsg)); err != nil {
|
||||
return err
|
||||
}
|
||||
msgs = append(msgs, &ccOplogMsg)
|
||||
}
|
||||
if cv.BindCredential != nil {
|
||||
var bcOplogMsg oplog.Message
|
||||
if err := w.Create(ctx, cv.BindCredential, db.NewOplogMsg(&bcOplogMsg)); err != nil {
|
||||
return err
|
||||
}
|
||||
msgs = append(msgs, &bcOplogMsg)
|
||||
}
|
||||
if len(cv.AccountAttributeMaps) > 0 {
|
||||
attrMapsOplogMsgs := make([]*oplog.Message, 0, len(cv.AccountAttributeMaps))
|
||||
if err := w.CreateItems(ctx, cv.AccountAttributeMaps, db.NewOplogMsgs(&attrMapsOplogMsgs)); err != nil {
|
||||
return err
|
||||
}
|
||||
msgs = append(msgs, attrMapsOplogMsgs...)
|
||||
}
|
||||
md, err := am.oplog(ctx, oplog.OpType_OP_TYPE_CREATE)
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to generate oplog metadata"))
|
||||
}
|
||||
if err := w.WriteOplogEntryWith(ctx, oplogWrapper, ticket, md, msgs); err != nil {
|
||||
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to write oplog"))
|
||||
}
|
||||
return nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
found, err := r.getAuthMethods(ctx, am.GetPublicId(), nil)
|
||||
switch {
|
||||
case err != nil:
|
||||
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("failed to lookup created auth method %q", am.GetPublicId()))
|
||||
case len(found) != 1:
|
||||
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("found %d auth methods with public id of %q and expected 1", len(found), am.GetPublicId()))
|
||||
default:
|
||||
return found[0], nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/iam"
|
||||
"github.com/hashicorp/boundary/internal/kms"
|
||||
"github.com/hashicorp/boundary/internal/oplog"
|
||||
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/testing/protocmp"
|
||||
)
|
||||
|
||||
func TestRepository_CreateAuthMethod(t *testing.T) {
|
||||
testConn, _ := db.TestSetup(t, "postgres")
|
||||
testRw := db.New(testConn)
|
||||
testWrapper := db.TestWrapper(t)
|
||||
testKms := kms.TestKms(t, testConn, testWrapper)
|
||||
testCtx := context.Background()
|
||||
org, _ := iam.TestScopes(t, iam.TestRepo(t, testConn, testWrapper))
|
||||
testCert, _ := TestGenerateCA(t, "localhost")
|
||||
testCert2, _ := TestGenerateCA(t, "localhost")
|
||||
_, testPrivKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
require.NoError(t, err)
|
||||
derPrivKey, err := x509.MarshalPKCS8PrivateKey(testPrivKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
testAm, err := NewAuthMethod(
|
||||
testCtx,
|
||||
org.PublicId,
|
||||
WithUrls(testCtx, TestConvertToUrls(t, "ldaps://ldap1", "ldap://ldap2")...),
|
||||
WithName(testCtx, "test-name"),
|
||||
WithDescription(testCtx, "test-description"),
|
||||
WithStartTLS(testCtx),
|
||||
WithInsecureTLS(testCtx),
|
||||
WithDiscoverDn(testCtx),
|
||||
WithAnonGroupSearch(testCtx),
|
||||
WithUpnDomain(testCtx, "alice.com"),
|
||||
WithUserDn(testCtx, "user-dn"),
|
||||
WithUserAttr(testCtx, "user-attr"),
|
||||
WithUserFilter(testCtx, "user-filter"),
|
||||
WithEnableGroups(testCtx),
|
||||
WithUseTokenGroups(testCtx),
|
||||
WithGroupDn(testCtx, "group-dn"),
|
||||
WithGroupAttr(testCtx, "group-attr"),
|
||||
WithGroupFilter(testCtx, "group-filter"),
|
||||
WithBindCredential(testCtx, "bind-dn", "bind-password"),
|
||||
WithCertificates(testCtx, testCert, testCert2),
|
||||
WithClientCertificate(testCtx, derPrivKey, testCert), // not a client cert but good enough for this test.
|
||||
WithAccountAttributeMap(testCtx, map[string]AccountToAttribute{
|
||||
"mail": ToEmailAttribute,
|
||||
}),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
kms kms.GetWrapperer
|
||||
setup func(*testing.T) *AuthMethod
|
||||
opt []Option
|
||||
wantErrMatch *errors.Template
|
||||
wantErrContains string
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
kms: testKms,
|
||||
setup: func(t *testing.T) *AuthMethod {
|
||||
return testAm.clone()
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "bind-cred-encrypt-err",
|
||||
kms: &mockGetWrapperer{
|
||||
returnDbWrapper: &kms.MockWrapper{
|
||||
EncryptErr: errors.New(testCtx, errors.Encrypt, "test", "bind-cred-encrypt-err"),
|
||||
},
|
||||
},
|
||||
setup: func(t *testing.T) *AuthMethod {
|
||||
return testAm.clone()
|
||||
},
|
||||
wantErrMatch: errors.T(errors.Unknown),
|
||||
wantErrContains: "bind-cred-encrypt-err",
|
||||
},
|
||||
{
|
||||
name: "get-db-wrapper-err",
|
||||
kms: &mockGetWrapperer{
|
||||
getErr: errors.New(testCtx, errors.Encrypt, "test", "get-db-wrapper-err"),
|
||||
},
|
||||
setup: func(t *testing.T) *AuthMethod {
|
||||
return testAm.clone()
|
||||
},
|
||||
wantErrMatch: errors.T(errors.Encrypt),
|
||||
wantErrContains: "unable to get database wrapper",
|
||||
},
|
||||
{
|
||||
name: "get-oplog-wrapper-err",
|
||||
kms: &mockGetWrapperer{
|
||||
getErr: errors.New(testCtx, errors.Encrypt, "test", "get-db-wrapper-err"),
|
||||
returnDbWrapper: testWrapper,
|
||||
},
|
||||
setup: func(t *testing.T) *AuthMethod {
|
||||
return testAm.clone()
|
||||
},
|
||||
wantErrMatch: errors.T(errors.Encrypt),
|
||||
wantErrContains: "unable to get oplog wrapper",
|
||||
},
|
||||
{
|
||||
name: "bad-state",
|
||||
kms: testKms,
|
||||
setup: func(t *testing.T) *AuthMethod {
|
||||
am, err := NewAuthMethod(testCtx, org.PublicId, WithUrls(testCtx, TestConvertToUrls(t, "ldaps://ldap1")...))
|
||||
require.NoError(t, err)
|
||||
am.OperationalState = "not-a-valid-state"
|
||||
return am
|
||||
},
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "invalid state",
|
||||
},
|
||||
{
|
||||
name: "missing-auth-method",
|
||||
kms: testKms,
|
||||
setup: func(t *testing.T) *AuthMethod {
|
||||
return nil
|
||||
},
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing auth method",
|
||||
},
|
||||
{
|
||||
name: "missing-scope",
|
||||
kms: testKms,
|
||||
setup: func(t *testing.T) *AuthMethod {
|
||||
am, err := NewAuthMethod(testCtx, org.PublicId, WithUrls(testCtx, TestConvertToUrls(t, "ldaps://ldap1")...))
|
||||
require.NoError(t, err)
|
||||
am.ScopeId = ""
|
||||
return am
|
||||
},
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing scope id",
|
||||
},
|
||||
{
|
||||
name: "convert-err",
|
||||
kms: testKms,
|
||||
setup: func(t *testing.T) *AuthMethod {
|
||||
am, err := NewAuthMethod(testCtx, org.PublicId, WithUrls(testCtx, TestConvertToUrls(t, "ldaps://ldap1")...))
|
||||
require.NoError(t, err)
|
||||
am.BindDn = "bind-dn"
|
||||
return am
|
||||
},
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing password",
|
||||
},
|
||||
{
|
||||
name: "missing-urls",
|
||||
kms: testKms,
|
||||
setup: func(t *testing.T) *AuthMethod {
|
||||
am, err := NewAuthMethod(testCtx, org.PublicId, WithUrls(testCtx, TestConvertToUrls(t, "ldaps://ldap1")...))
|
||||
require.NoError(t, err)
|
||||
am.Urls = nil
|
||||
return am
|
||||
},
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing urls (there must be at least one)",
|
||||
},
|
||||
{
|
||||
name: "bad-public-id",
|
||||
kms: testKms,
|
||||
setup: func(t *testing.T) *AuthMethod {
|
||||
id, err := newAuthMethodId(testCtx)
|
||||
require.NoError(t, err)
|
||||
am := AllocAuthMethod()
|
||||
am.PublicId = id
|
||||
return &am
|
||||
},
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "public id must be empty",
|
||||
},
|
||||
{
|
||||
name: "bad-version",
|
||||
kms: testKms,
|
||||
setup: func(t *testing.T) *AuthMethod {
|
||||
am := AllocAuthMethod()
|
||||
am.Version = 22
|
||||
return &am
|
||||
},
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "version must be empty",
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
repo, err := NewRepository(testCtx, testRw, testRw, tc.kms)
|
||||
assert.NoError(err)
|
||||
require.NotNil(repo)
|
||||
am := tc.setup(t)
|
||||
got, err := repo.CreateAuthMethod(testCtx, am, tc.opt...)
|
||||
if tc.wantErrMatch != nil {
|
||||
require.Error(err)
|
||||
assert.Truef(errors.Match(tc.wantErrMatch, err), "want err code: %q got: %q", tc.wantErrMatch, err)
|
||||
assert.Nil(got)
|
||||
|
||||
if am != nil {
|
||||
err := db.TestVerifyOplog(t, testRw, am.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_CREATE), db.WithCreateNotBefore(10*time.Second))
|
||||
require.Errorf(err, "should not have found oplog entry for %s", am.PublicId)
|
||||
}
|
||||
|
||||
if tc.wantErrContains != "" {
|
||||
assert.Contains(err.Error(), tc.wantErrContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
am.PublicId = got.PublicId
|
||||
am.CreateTime = got.CreateTime
|
||||
am.UpdateTime = got.UpdateTime
|
||||
am.Version = got.Version
|
||||
am.BindPasswordHmac = got.BindPasswordHmac
|
||||
am.ClientCertificateKeyHmac = got.ClientCertificateKeyHmac
|
||||
TestSortAuthMethods(t, []*AuthMethod{am, got})
|
||||
assert.Truef(proto.Equal(am.AuthMethod, got.AuthMethod), "got %+v expected %+v", got.AuthMethod, am.AuthMethod)
|
||||
|
||||
err = db.TestVerifyOplog(t, testRw, am.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_CREATE), db.WithCreateNotBefore(10*time.Second))
|
||||
require.NoErrorf(err, "unexpected error verifying oplog entry: %s", err)
|
||||
|
||||
found, err := repo.LookupAuthMethod(testCtx, am.PublicId)
|
||||
require.NoError(err)
|
||||
found.CreateTime = got.CreateTime
|
||||
found.UpdateTime = got.UpdateTime
|
||||
found.Version = got.Version
|
||||
TestSortAuthMethods(t, []*AuthMethod{found, am})
|
||||
assert.Empty(cmp.Diff(found.AuthMethod, am.AuthMethod, protocmp.Transform()))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type mockGetWrapperer struct {
|
||||
// kms is the underlying kms which is used to provide the mock's default
|
||||
// behavior
|
||||
kms kms.GetWrapperer
|
||||
|
||||
// getErr is a mock value to return for the GetWrapper(...) operation
|
||||
getErr error
|
||||
|
||||
returnOplogWrapper wrapping.Wrapper
|
||||
returnDbWrapper wrapping.Wrapper
|
||||
}
|
||||
|
||||
func (m *mockGetWrapperer) GetWrapper(ctx context.Context, scopeId string, purpose kms.KeyPurpose, opt ...kms.Option) (wrapping.Wrapper, error) {
|
||||
switch {
|
||||
case purpose == kms.KeyPurposeOplog && m.returnOplogWrapper != nil:
|
||||
return m.returnOplogWrapper, nil
|
||||
case purpose == kms.KeyPurposeDatabase && m.returnDbWrapper != nil:
|
||||
return m.returnDbWrapper, nil
|
||||
case m.getErr != nil:
|
||||
return nil, m.getErr
|
||||
default:
|
||||
return m.kms.GetWrapper(ctx, scopeId, purpose, opt...)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/kms"
|
||||
"github.com/hashicorp/boundary/internal/oplog"
|
||||
)
|
||||
|
||||
// DeleteAuthMethod will delete the auth method from the repository. It is
|
||||
// idempotent so if the auth method was not found, return 0 (no rows affected)
|
||||
// and nil. No options are currently supported.
|
||||
func (r *Repository) DeleteAuthMethod(ctx context.Context, publicId string, _ ...Option) (int, error) {
|
||||
const op = "ldap.(Repository).DeleteAuthMethod"
|
||||
if publicId == "" {
|
||||
return db.NoRowsAffected, errors.New(ctx, errors.InvalidPublicId, op, "missing public id")
|
||||
}
|
||||
am, err := r.LookupAuthMethod(ctx, publicId)
|
||||
if err != nil {
|
||||
return db.NoRowsAffected, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
if am == nil {
|
||||
// already deleted and this is not an error.
|
||||
return db.NoRowsAffected, nil
|
||||
}
|
||||
|
||||
oplogWrapper, err := r.kms.GetWrapper(ctx, am.ScopeId, kms.KeyPurposeOplog)
|
||||
if err != nil {
|
||||
return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper"))
|
||||
}
|
||||
var rowsDeleted int
|
||||
_, err = r.writer.DoTx(
|
||||
ctx,
|
||||
db.StdRetryCnt,
|
||||
db.ExpBackoff{},
|
||||
func(_ db.Reader, w db.Writer) error {
|
||||
cp := am.clone()
|
||||
md, err := cp.oplog(ctx, oplog.OpType_OP_TYPE_DELETE)
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to generate oplog metadata"))
|
||||
}
|
||||
rowsDeleted, err = w.Delete(ctx, cp, db.WithOplog(oplogWrapper, md))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rowsDeleted > 1 {
|
||||
return errors.New(ctx, errors.MultipleRecords, op, "more than 1 auth method would have been deleted")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("unable to delete %s", publicId)))
|
||||
}
|
||||
return rowsDeleted, nil
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/iam"
|
||||
"github.com/hashicorp/boundary/internal/kms"
|
||||
"github.com/hashicorp/boundary/internal/oplog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRepository_DeleteAuthMethod(t *testing.T) {
|
||||
testConn, _ := db.TestSetup(t, "postgres")
|
||||
testRw := db.New(testConn)
|
||||
testWrapper := db.TestWrapper(t)
|
||||
testKms := kms.TestKms(t, testConn, testWrapper)
|
||||
testCtx := context.Background()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
reader db.Reader
|
||||
writer db.Writer
|
||||
kms kms.GetWrapperer
|
||||
authMethod *AuthMethod
|
||||
wantRowsDeleted int
|
||||
wantErrMatch *errors.Template
|
||||
wantErrContains string
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
reader: testRw,
|
||||
writer: testRw,
|
||||
kms: testKms,
|
||||
authMethod: func() *AuthMethod {
|
||||
org, _ := iam.TestScopes(t, iam.TestRepo(t, testConn, testWrapper))
|
||||
databaseWrapper, err := testKms.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase)
|
||||
require.NoError(t, err)
|
||||
return TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"}, WithOperationalState(testCtx, InactiveState))
|
||||
}(),
|
||||
wantRowsDeleted: 1,
|
||||
},
|
||||
{
|
||||
name: "no-public-id",
|
||||
reader: testRw,
|
||||
writer: testRw,
|
||||
kms: testKms,
|
||||
authMethod: func() *AuthMethod { am := AllocAuthMethod(); return &am }(),
|
||||
wantErrMatch: errors.T(errors.InvalidPublicId),
|
||||
wantErrContains: "missing public id",
|
||||
},
|
||||
{
|
||||
name: "not-found",
|
||||
reader: testRw,
|
||||
writer: testRw,
|
||||
kms: testKms,
|
||||
authMethod: func() *AuthMethod {
|
||||
am := AllocAuthMethod()
|
||||
var err error
|
||||
am.PublicId, err = newAuthMethodId(testCtx)
|
||||
require.NoError(t, err)
|
||||
return &am
|
||||
}(),
|
||||
},
|
||||
{
|
||||
name: "lookup-err",
|
||||
reader: func() db.Reader {
|
||||
conn, mock := db.TestSetupWithMock(t)
|
||||
mock.ExpectQuery(`SELECT`).WillReturnError(errors.New(context.Background(), errors.Internal, "test", "lookup-error"))
|
||||
return db.New(conn)
|
||||
}(),
|
||||
writer: testRw,
|
||||
kms: testKms,
|
||||
authMethod: func() *AuthMethod {
|
||||
org, _ := iam.TestScopes(t, iam.TestRepo(t, testConn, testWrapper))
|
||||
databaseWrapper, err := testKms.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase)
|
||||
require.NoError(t, err)
|
||||
return TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"}, WithOperationalState(testCtx, InactiveState))
|
||||
}(),
|
||||
wantErrMatch: errors.T(errors.Internal),
|
||||
wantErrContains: "lookup-err",
|
||||
},
|
||||
{
|
||||
name: "delete-err",
|
||||
reader: testRw,
|
||||
writer: func() db.Writer {
|
||||
conn, mock := db.TestSetupWithMock(t)
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectQuery(`SELECT`).WillReturnError(errors.New(context.Background(), errors.Internal, "test", "delete-error"))
|
||||
mock.ExpectRollback()
|
||||
return db.New(conn)
|
||||
}(),
|
||||
kms: testKms,
|
||||
authMethod: func() *AuthMethod {
|
||||
org, _ := iam.TestScopes(t, iam.TestRepo(t, testConn, testWrapper))
|
||||
databaseWrapper, err := testKms.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase)
|
||||
require.NoError(t, err)
|
||||
return TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"}, WithOperationalState(testCtx, InactiveState))
|
||||
}(),
|
||||
wantErrMatch: errors.T(errors.Internal),
|
||||
wantErrContains: "delete-err",
|
||||
},
|
||||
{
|
||||
name: "multiple-rows-err",
|
||||
reader: testRw,
|
||||
writer: func() db.Writer {
|
||||
conn, mock := db.TestSetupWithMock(t)
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectQuery(`SELECT`).WillReturnRows(sqlmock.NewRows([]string{"id", "version"}).AddRow("1", 1)) // oplog: get ticket
|
||||
mock.ExpectExec(`DELETE`).WillReturnResult(sqlmock.NewResult(0, 10))
|
||||
mock.ExpectQuery(`INSERT`).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow("1")) // oplog: insert metadata
|
||||
mock.ExpectQuery(`INSERT`).WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow("1")) // oplog: insert entry
|
||||
mock.ExpectExec(`UPDATE`).WillReturnResult(sqlmock.NewResult(0, 1)) // oplog: update ticket version
|
||||
mock.ExpectQuery(`SELECT`).WillReturnRows(sqlmock.NewRows([]string{"id", "version"}).AddRow("1", 1)) // oplog: get ticket again
|
||||
mock.ExpectRollback()
|
||||
return db.New(conn)
|
||||
}(),
|
||||
kms: testKms,
|
||||
authMethod: func() *AuthMethod {
|
||||
org, _ := iam.TestScopes(t, iam.TestRepo(t, testConn, testWrapper))
|
||||
databaseWrapper, err := testKms.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase)
|
||||
require.NoError(t, err)
|
||||
return TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"}, WithOperationalState(testCtx, InactiveState))
|
||||
}(),
|
||||
wantErrMatch: errors.T(errors.MultipleRecords),
|
||||
wantErrContains: "more than 1 auth method would have been deleted",
|
||||
},
|
||||
{
|
||||
name: "getWrapper-err",
|
||||
reader: testRw,
|
||||
writer: testRw,
|
||||
kms: &mockGetWrapperer{
|
||||
getErr: errors.New(testCtx, errors.Encrypt, "test", "get-db-wrapper-err"),
|
||||
},
|
||||
authMethod: func() *AuthMethod {
|
||||
org, _ := iam.TestScopes(t, iam.TestRepo(t, testConn, testWrapper))
|
||||
databaseWrapper, err := testKms.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase)
|
||||
require.NoError(t, err)
|
||||
return TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1"}, WithOperationalState(testCtx, InactiveState))
|
||||
}(),
|
||||
wantErrMatch: errors.T(errors.Encrypt),
|
||||
wantErrContains: "unable to get oplog wrapper",
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
repo, err := NewRepository(testCtx, tc.reader, tc.writer, tc.kms)
|
||||
require.NoError(err)
|
||||
deletedRows, err := repo.DeleteAuthMethod(testCtx, tc.authMethod.PublicId)
|
||||
if tc.wantErrMatch != nil {
|
||||
require.Error(err)
|
||||
assert.Truef(errors.Match(tc.wantErrMatch, err), "want err: %q got: %q", tc.wantErrMatch.Msg, err)
|
||||
if tc.wantErrContains != "" {
|
||||
assert.Contains(err.Error(), tc.wantErrContains)
|
||||
}
|
||||
|
||||
assert.Equalf(0, deletedRows, "expected 0 deleted rows and got %d", deletedRows)
|
||||
|
||||
err := db.TestVerifyOplog(t, testRw, tc.authMethod.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_DELETE), db.WithCreateNotBefore(10*time.Second))
|
||||
require.Errorf(err, "should not have found oplog entry for %s", tc.authMethod.PublicId)
|
||||
assert.Truef(errors.Match(errors.T(errors.RecordNotFound), err), "expected error code %s and got %s", errors.RecordNotFound, err)
|
||||
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equalf(tc.wantRowsDeleted, deletedRows, "expected rows deleted == %d and got %d", tc.wantRowsDeleted, deletedRows)
|
||||
|
||||
if tc.wantRowsDeleted > 0 {
|
||||
err = db.TestVerifyOplog(t, testRw, tc.authMethod.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_DELETE), db.WithCreateNotBefore(10*time.Second))
|
||||
require.NoErrorf(err, "unexpected error verifying oplog entry: %s", err)
|
||||
}
|
||||
found, err := repo.LookupAuthMethod(testCtx, tc.authMethod.PublicId)
|
||||
require.NoError(err)
|
||||
assert.Nil(found)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
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(ctx, 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.ClientCertificateKey != 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 key"))
|
||||
}
|
||||
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.EnableGroups = agg.EnableGroups
|
||||
am.UseTokenGroups = agg.UseTokenGroups
|
||||
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
|
||||
if agg.AccountAttributeMap != "" {
|
||||
am.AccountAttributeMaps = strings.Split(agg.AccountAttributeMap, aggregateDelimiter)
|
||||
}
|
||||
|
||||
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
|
||||
EnableGroups bool
|
||||
UseTokenGroups bool
|
||||
GroupDn string
|
||||
GroupAttr string
|
||||
GroupFilter string
|
||||
ClientCertificateKey []byte
|
||||
ClientCertificateKeyHmac []byte
|
||||
ClientCertificateKeyId string
|
||||
ClientCertificateCert []byte
|
||||
BindDn string
|
||||
BindPassword []byte
|
||||
BindPasswordHmac []byte
|
||||
BindKeyId string
|
||||
AccountAttributeMap string
|
||||
}
|
||||
|
||||
// TableName returns the table name for gorm
|
||||
func (agg *authMethodAgg) TableName() string { return "ldap_auth_method_with_value_obj" }
|
||||
@@ -0,0 +1,357 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/iam"
|
||||
"github.com/hashicorp/boundary/internal/kms"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRepository_LookupAuthMethod(t *testing.T) {
|
||||
testConn, _ := db.TestSetup(t, "postgres")
|
||||
testRw := db.New(testConn)
|
||||
testWrapper := db.TestWrapper(t)
|
||||
testKms := kms.TestKms(t, testConn, testWrapper)
|
||||
testCtx := context.Background()
|
||||
org, _ := iam.TestScopes(t, iam.TestRepo(t, testConn, testWrapper))
|
||||
orgDbWrapper, err := testKms.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase)
|
||||
require.NoError(t, err)
|
||||
amInactive := TestAuthMethod(t, testConn, orgDbWrapper, org.PublicId, []string{"ldaps://ldap1.alice.com"}, WithOperationalState(testCtx, InactiveState))
|
||||
amActivePriv := TestAuthMethod(t, testConn, orgDbWrapper, org.PublicId, []string{"ldaps://ldap2.alice.com"}, WithOperationalState(testCtx, ActivePrivateState))
|
||||
amActivePub := TestAuthMethod(t, testConn, orgDbWrapper, org.PublicId, []string{"ldaps://ldap3.alice.com"}, WithOperationalState(testCtx, ActivePublicState))
|
||||
amActivePub.IsPrimaryAuthMethod = true
|
||||
iam.TestSetPrimaryAuthMethod(t, iam.TestRepo(t, testConn, testWrapper), org, amActivePub.PublicId)
|
||||
|
||||
amId, err := newAuthMethodId(testCtx)
|
||||
require.NoError(t, err)
|
||||
tests := []struct {
|
||||
name string
|
||||
in string
|
||||
opt []Option
|
||||
want *AuthMethod
|
||||
wantIsPrimary bool
|
||||
wantErrMatch *errors.Template
|
||||
wantErrContains string
|
||||
}{
|
||||
{
|
||||
name: "missing-public-id",
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing public id",
|
||||
},
|
||||
{
|
||||
name: "invalid-opts",
|
||||
in: amActivePub.GetPublicId(),
|
||||
opt: []Option{WithBindCredential(testCtx, "", "")},
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing both dn and password",
|
||||
},
|
||||
{
|
||||
name: "non-existing-auth-method-id",
|
||||
in: amId,
|
||||
},
|
||||
{
|
||||
name: "existing-auth-method-id",
|
||||
in: amActivePriv.GetPublicId(),
|
||||
want: amActivePriv,
|
||||
},
|
||||
{
|
||||
name: "unauthenticated-user-not-found-using-active-priv",
|
||||
in: amActivePriv.GetPublicId(),
|
||||
opt: []Option{WithUnauthenticatedUser(testCtx, true)},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "unauthenticated-user-found-active-pub",
|
||||
in: amActivePub.GetPublicId(),
|
||||
opt: []Option{WithUnauthenticatedUser(testCtx, true)},
|
||||
want: amActivePub,
|
||||
wantIsPrimary: true,
|
||||
},
|
||||
{
|
||||
name: "unauthenticated-user-found-inactive",
|
||||
in: amInactive.GetPublicId(),
|
||||
opt: []Option{WithUnauthenticatedUser(testCtx, true)},
|
||||
want: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
repo, err := NewRepository(testCtx, testRw, testRw, testKms)
|
||||
assert.NoError(err)
|
||||
require.NotNil(repo)
|
||||
got, err := repo.LookupAuthMethod(testCtx, tc.in, tc.opt...)
|
||||
if tc.wantErrMatch != nil {
|
||||
require.Error(err)
|
||||
assert.Truef(errors.Match(tc.wantErrMatch, err), "want err code: %q got: %q", tc.wantErrMatch, err)
|
||||
assert.Nil(got)
|
||||
if tc.wantErrContains != "" {
|
||||
assert.Contains(err.Error(), tc.wantErrContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.EqualValues(tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepository_getAuthMethods(t *testing.T) {
|
||||
testConn, _ := db.TestSetup(t, "postgres")
|
||||
testRw := db.New(testConn)
|
||||
testWrapper := db.TestWrapper(t)
|
||||
testKms := kms.TestKms(t, testConn, testWrapper)
|
||||
testCtx := context.Background()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
setupFn func() (authMethodId string, scopeIds []string, want []*AuthMethod)
|
||||
opt []Option
|
||||
wantErrMatch *errors.Template
|
||||
wantErrContains string
|
||||
}{
|
||||
{
|
||||
name: "valid-multi-scopes",
|
||||
setupFn: func() (string, []string, []*AuthMethod) {
|
||||
org, _ := iam.TestScopes(t, iam.TestRepo(t, testConn, testWrapper))
|
||||
orgDBWrapper, err := testKms.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase)
|
||||
require.NoError(t, err)
|
||||
org2, _ := iam.TestScopes(t, iam.TestRepo(t, testConn, testWrapper))
|
||||
orgDBWrapper2, err := testKms.GetWrapper(context.Background(), org2.PublicId, kms.KeyPurposeDatabase)
|
||||
require.NoError(t, err)
|
||||
|
||||
// make a test auth method with all options
|
||||
am1a := TestAuthMethod(t, testConn, orgDBWrapper, org.PublicId, []string{"ldaps://ldap1.alice.com"}, WithOperationalState(testCtx, InactiveState))
|
||||
am1b := TestAuthMethod(t, testConn, orgDBWrapper, org.PublicId, []string{"ldaps://ldap2.alice.com"}, WithOperationalState(testCtx, InactiveState))
|
||||
am2 := TestAuthMethod(t, testConn, orgDBWrapper2, org2.PublicId, []string{"ldaps://ldap3.alice.com"}, WithOperationalState(testCtx, InactiveState))
|
||||
return "", []string{am1a.ScopeId, am1b.ScopeId, am2.ScopeId}, []*AuthMethod{am1a, am1b, am2}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid-single-scopes",
|
||||
setupFn: func() (string, []string, []*AuthMethod) {
|
||||
org, _ := iam.TestScopes(t, iam.TestRepo(t, testConn, testWrapper))
|
||||
orgDBWrapper, err := testKms.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase)
|
||||
require.NoError(t, err)
|
||||
am1a := TestAuthMethod(t, testConn, orgDBWrapper, org.PublicId, []string{"ldaps://ldap1.alice.com"}, WithOperationalState(testCtx, InactiveState))
|
||||
am1b := TestAuthMethod(t, testConn, orgDBWrapper, org.PublicId, []string{"ldaps://ldap2.alice.com"}, WithOperationalState(testCtx, InactiveState))
|
||||
|
||||
return "", []string{am1a.ScopeId}, []*AuthMethod{am1a, am1b}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid-auth-method-id-all-opts",
|
||||
setupFn: func() (string, []string, []*AuthMethod) {
|
||||
org, _ := iam.TestScopes(t, iam.TestRepo(t, testConn, testWrapper))
|
||||
orgDbWrapper, err := testKms.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase)
|
||||
require.NoError(t, err)
|
||||
testCert, _ := TestGenerateCA(t, "localhost")
|
||||
_, testPrivKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
require.NoError(t, err)
|
||||
derPrivKey, err := x509.MarshalPKCS8PrivateKey(testPrivKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
am := TestAuthMethod(t, testConn, orgDbWrapper, org.PublicId, []string{"ldaps://alice.com"},
|
||||
WithName(testCtx, "test-name"),
|
||||
WithDescription(testCtx, "test-description"),
|
||||
WithStartTLS(testCtx),
|
||||
WithInsecureTLS(testCtx),
|
||||
WithDiscoverDn(testCtx),
|
||||
WithAnonGroupSearch(testCtx),
|
||||
WithUpnDomain(testCtx, "alice.com"),
|
||||
WithUserDn(testCtx, "user-dn"),
|
||||
WithUserAttr(testCtx, "user-attr"),
|
||||
WithUserFilter(testCtx, "user-filter"),
|
||||
WithEnableGroups(testCtx),
|
||||
WithUseTokenGroups(testCtx),
|
||||
WithGroupDn(testCtx, "group-dn"),
|
||||
WithGroupAttr(testCtx, "group-attr"),
|
||||
WithGroupFilter(testCtx, "group-filter"),
|
||||
WithBindCredential(testCtx, "bind-dn", "bind-password"),
|
||||
WithCertificates(testCtx, testCert),
|
||||
WithClientCertificate(testCtx, derPrivKey, testCert), // not a client cert but good enough for this test.)
|
||||
WithAccountAttributeMap(testCtx, map[string]AccountToAttribute{"mail": ToEmailAttribute, "displayName": ToFullNameAttribute}),
|
||||
)
|
||||
return am.PublicId, nil, []*AuthMethod{am}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "with-limits",
|
||||
setupFn: func() (string, []string, []*AuthMethod) {
|
||||
org, _ := iam.TestScopes(t, iam.TestRepo(t, testConn, testWrapper))
|
||||
orgDBWrapper, err := testKms.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase)
|
||||
require.NoError(t, err)
|
||||
am1a := TestAuthMethod(t, testConn, orgDBWrapper, org.PublicId, []string{"ldaps://alice.com"})
|
||||
_ = TestAuthMethod(t, testConn, orgDBWrapper, org.PublicId, []string{"ldaps://alice2.com"})
|
||||
|
||||
return "", []string{am1a.ScopeId}, []*AuthMethod{am1a}
|
||||
},
|
||||
opt: []Option{WithLimit(testCtx, 1), WithOrderByCreateTime(testCtx, true)},
|
||||
},
|
||||
{
|
||||
name: "unauthenticated-user",
|
||||
setupFn: func() (string, []string, []*AuthMethod) {
|
||||
org, _ := iam.TestScopes(t, iam.TestRepo(t, testConn, testWrapper))
|
||||
databaseWrapper, err := testKms.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase)
|
||||
require.NoError(t, err)
|
||||
_ = TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap1.alice.com"}, WithOperationalState(testCtx, InactiveState))
|
||||
_ = TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap2.alice.com"}, WithOperationalState(testCtx, ActivePrivateState))
|
||||
amActivePub := TestAuthMethod(t, testConn, databaseWrapper, org.PublicId, []string{"ldaps://ldap3.alice.com"}, WithOperationalState(testCtx, ActivePublicState))
|
||||
|
||||
return "", []string{amActivePub.ScopeId}, []*AuthMethod{amActivePub}
|
||||
},
|
||||
opt: []Option{WithUnauthenticatedUser(testCtx, true)},
|
||||
},
|
||||
{
|
||||
name: "not-found-auth-method-id",
|
||||
setupFn: func() (string, []string, []*AuthMethod) {
|
||||
return "not-a-valid-id", nil, nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "not-found-scope-ids",
|
||||
setupFn: func() (string, []string, []*AuthMethod) {
|
||||
return "", []string{"not-valid-scope-1", "not-valid-scope-2"}, nil
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no-search-criteria",
|
||||
setupFn: func() (string, []string, []*AuthMethod) {
|
||||
return "", nil, nil
|
||||
},
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing search criteria: both auth method id and scope ids are empty",
|
||||
},
|
||||
{
|
||||
name: "search-too-many",
|
||||
setupFn: func() (string, []string, []*AuthMethod) {
|
||||
return "auth-method-id", []string{"scope-id"}, nil
|
||||
},
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "searching for both an auth method id and scope ids is not supported",
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
r, err := NewRepository(testCtx, testRw, testRw, testKms)
|
||||
require.NoError(err)
|
||||
|
||||
authMethodId, scopeIds, want := tc.setupFn()
|
||||
|
||||
got, err := r.getAuthMethods(testCtx, authMethodId, scopeIds, tc.opt...)
|
||||
|
||||
if tc.wantErrMatch != nil {
|
||||
require.Error(err)
|
||||
assert.Truef(errors.Match(tc.wantErrMatch, err), "want err code: %q got: %q", tc.wantErrMatch, err)
|
||||
assert.Nil(got)
|
||||
if tc.wantErrContains != "" {
|
||||
assert.Contains(err.Error(), tc.wantErrContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
TestSortAuthMethods(t, want)
|
||||
TestSortAuthMethods(t, got)
|
||||
assert.Equal(want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestRepository_ListAuthMethods only covers error conditions, since all the
|
||||
// search criteria testing is handled in the TestRepository_getAuthMethods unit
|
||||
// tests.
|
||||
func TestRepository_ListAuthMethods(t *testing.T) {
|
||||
testConn, _ := db.TestSetup(t, "postgres")
|
||||
testRw := db.New(testConn)
|
||||
testWrapper := db.TestWrapper(t)
|
||||
testKms := kms.TestKms(t, testConn, testWrapper)
|
||||
testCtx := context.Background()
|
||||
iamRepo := iam.TestRepo(t, testConn, testWrapper)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
setupFn func() (scopeIds []string, want []*AuthMethod, wantPrimaryAuthMethodId string)
|
||||
opt []Option
|
||||
wantErrMatch *errors.Template
|
||||
}{
|
||||
{
|
||||
name: "with-limits",
|
||||
setupFn: func() ([]string, []*AuthMethod, string) {
|
||||
org, _ := iam.TestScopes(t, iam.TestRepo(t, testConn, testWrapper))
|
||||
orgDbWrapper, err := testKms.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase)
|
||||
require.NoError(t, err)
|
||||
am1a := TestAuthMethod(t, testConn, orgDbWrapper, org.PublicId, []string{"ldaps://alice.com"})
|
||||
iam.TestSetPrimaryAuthMethod(t, iamRepo, org, am1a.PublicId)
|
||||
am1a.IsPrimaryAuthMethod = true
|
||||
|
||||
_ = TestAuthMethod(t, testConn, orgDbWrapper, org.PublicId, []string{"ldaps://alice2.com"})
|
||||
|
||||
return []string{am1a.ScopeId}, []*AuthMethod{am1a}, am1a.PublicId
|
||||
},
|
||||
opt: []Option{WithLimit(testCtx, 1), WithOrderByCreateTime(testCtx, true)},
|
||||
},
|
||||
{
|
||||
name: "no-search-criteria",
|
||||
setupFn: func() ([]string, []*AuthMethod, string) {
|
||||
return nil, nil, ""
|
||||
},
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
repo, err := NewRepository(testCtx, testRw, testRw, testKms)
|
||||
assert.NoError(err)
|
||||
scopeIds, want, wantPrimaryAuthMethodId := tt.setupFn()
|
||||
|
||||
got, err := repo.ListAuthMethods(testCtx, scopeIds, tt.opt...)
|
||||
if tt.wantErrMatch != nil {
|
||||
require.Error(err)
|
||||
assert.Truef(errors.Match(tt.wantErrMatch, err), "want err code: %q got: %q", tt.wantErrMatch, err)
|
||||
assert.Nil(got)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
sort.Slice(want, func(a, b int) bool {
|
||||
return want[a].PublicId < want[b].PublicId
|
||||
})
|
||||
sort.Slice(got, func(a, b int) bool {
|
||||
return got[a].PublicId < got[b].PublicId
|
||||
})
|
||||
assert.Equal(want, got)
|
||||
if wantPrimaryAuthMethodId != "" {
|
||||
found := false
|
||||
for _, am := range got {
|
||||
if am.PublicId == wantPrimaryAuthMethodId {
|
||||
assert.Truef(am.IsPrimaryAuthMethod, "expected IsPrimaryAuthMethod to be true for: %s", am.PublicId)
|
||||
if am.IsPrimaryAuthMethod {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
}
|
||||
assert.Truef(found, "expected to find primary id %s in: %+v", wantPrimaryAuthMethodId, got)
|
||||
} else {
|
||||
for _, am := range got {
|
||||
assert.Falsef(am.IsPrimaryAuthMethod, "did not expect %s to be IsPrimaryAuthMethod", am.PublicId)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,738 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/kms"
|
||||
"github.com/hashicorp/boundary/internal/oplog"
|
||||
"github.com/hashicorp/go-dbw"
|
||||
"github.com/hashicorp/go-secure-stdlib/strutil"
|
||||
)
|
||||
|
||||
const (
|
||||
OperationalStateField = "OperationalState"
|
||||
VersionField = "Version"
|
||||
IsPrimaryAuthMethodField = "IsPrimaryAuthMethod"
|
||||
NameField = "Name"
|
||||
DescriptionField = "Description"
|
||||
StartTlsField = "StartTls"
|
||||
InsecureTlsField = "InsecureTls"
|
||||
DiscoverDnField = "DiscoverDn"
|
||||
AnonGroupSearchField = "AnonGroupSearch"
|
||||
UpnDomainField = "UpnDomain"
|
||||
UrlsField = "Urls"
|
||||
UserDnField = "UserDn"
|
||||
UserAttrField = "UserAttr"
|
||||
UserFilterField = "UserFilter"
|
||||
EnableGroupsField = "EnableGroups"
|
||||
UseTokenGroupsField = "UseTokenGroups"
|
||||
GroupDnField = "GroupDn"
|
||||
GroupAttrField = "GroupAttr"
|
||||
GroupFilterField = "GroupFilter"
|
||||
CertificatesField = "Certificates"
|
||||
ClientCertificateField = "ClientCertificate"
|
||||
ClientCertificateKeyField = "ClientCertificateKey"
|
||||
BindDnField = "BindDn"
|
||||
BindPasswordField = "BindPassword"
|
||||
AccountAttributeMapsField = "AccountAttributeMaps"
|
||||
GroupNamesField = "GroupNames"
|
||||
)
|
||||
|
||||
// isEmpty returns true if all the args are empty. Only supports checking
|
||||
// strings and pointers, all other types are assumed to be empty.
|
||||
func isEmpty(args ...any) bool {
|
||||
for _, i := range args {
|
||||
switch v := reflect.ValueOf(i); v.Kind() {
|
||||
case reflect.Pointer:
|
||||
if !v.IsNil() {
|
||||
return false
|
||||
}
|
||||
case reflect.String:
|
||||
if v.String() != "" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// UpdateAuthMethod will retrieve the auth method from the repository,
|
||||
// and update it based on the field masks provided.
|
||||
//
|
||||
// fieldMaskPaths provides field_mask.proto paths for fields that should
|
||||
// be updated. Fields will be set to NULL if the field is a
|
||||
// zero value and included in fieldMask. Name, Description, StartTLs,
|
||||
// DiscoverDn, AnonGroupSearch, UpnDomain, UserDn, UserAttr, UserFilter,
|
||||
// GroupDn, GroupAttr, GroupFilter, ClientCertificateKey, ClientCertificate,
|
||||
// BindDn and BindPassword are all updatable fields. The AuthMethod's Value
|
||||
// Objects of Urls and Certificates are also updatable. If no updatable fields
|
||||
// are included in the fieldMaskPaths, then an error is returned.
|
||||
//
|
||||
// No Options are currently supported.
|
||||
func (r *Repository) UpdateAuthMethod(ctx context.Context, am *AuthMethod, version uint32, fieldMaskPaths []string, _ ...Option) (*AuthMethod, int, error) {
|
||||
const op = "ldap.(AuthMethod).Update"
|
||||
switch {
|
||||
case am == nil:
|
||||
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing auth method")
|
||||
case am.AuthMethod == nil:
|
||||
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing auth method store")
|
||||
case am.PublicId == "":
|
||||
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing public id")
|
||||
}
|
||||
if err := validateFieldMask(ctx, fieldMaskPaths); err != nil {
|
||||
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
dbMask, nullFields := dbw.BuildUpdatePaths(
|
||||
map[string]any{
|
||||
OperationalStateField: am.OperationalState,
|
||||
NameField: am.Name,
|
||||
DescriptionField: am.Description,
|
||||
StartTlsField: am.StartTls,
|
||||
InsecureTlsField: am.InsecureTls,
|
||||
DiscoverDnField: am.DiscoverDn,
|
||||
AnonGroupSearchField: am.AnonGroupSearch,
|
||||
UpnDomainField: am.UpnDomain,
|
||||
UserDnField: am.UserDn,
|
||||
UserAttrField: am.UserAttr,
|
||||
UserFilterField: am.UserFilter,
|
||||
EnableGroupsField: am.EnableGroups,
|
||||
UseTokenGroupsField: am.UseTokenGroups,
|
||||
GroupDnField: am.GroupDn,
|
||||
GroupAttrField: am.GroupAttr,
|
||||
GroupFilterField: am.GroupFilter,
|
||||
CertificatesField: am.Certificates,
|
||||
ClientCertificateField: am.ClientCertificate,
|
||||
ClientCertificateKeyField: am.ClientCertificateKey,
|
||||
BindDnField: am.BindDn,
|
||||
BindPasswordField: am.BindPassword,
|
||||
UrlsField: am.Urls,
|
||||
AccountAttributeMapsField: am.AccountAttributeMaps,
|
||||
},
|
||||
fieldMaskPaths,
|
||||
[]string{
|
||||
StartTlsField,
|
||||
InsecureTlsField,
|
||||
DiscoverDnField,
|
||||
AnonGroupSearchField,
|
||||
EnableGroupsField,
|
||||
UseTokenGroupsField,
|
||||
},
|
||||
)
|
||||
if len(dbMask) == 0 && len(nullFields) == 0 {
|
||||
return nil, db.NoRowsAffected, errors.New(ctx, errors.EmptyFieldMask, op, "empty field mask")
|
||||
}
|
||||
if strutil.StrListContains(nullFields, UrlsField) {
|
||||
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing urls (you cannot delete all of them; there must be at least one)")
|
||||
}
|
||||
|
||||
origAm, err := r.LookupAuthMethod(ctx, am.PublicId)
|
||||
if err != nil {
|
||||
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("%q auth method not found", am.PublicId))
|
||||
}
|
||||
if origAm == nil {
|
||||
return nil, db.NoRowsAffected, errors.New(ctx, errors.RecordNotFound, op, fmt.Sprintf("auth method %q", am.PublicId))
|
||||
}
|
||||
// there's no reason to continue if another controller has already updated this auth method.
|
||||
if origAm.Version != version {
|
||||
return nil, db.NoRowsAffected, errors.New(ctx, errors.VersionMismatch, op, fmt.Sprintf("update version %d doesn't match db version %d", version, origAm.Version))
|
||||
}
|
||||
|
||||
dbWrapper, err := r.kms.GetWrapper(ctx, origAm.ScopeId, kms.KeyPurposeDatabase)
|
||||
if err != nil {
|
||||
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get database wrapper"))
|
||||
}
|
||||
addUrls, deleteUrls, err := valueObjectChanges(ctx, origAm.PublicId, UrlVO, am.Urls, origAm.Urls, dbMask, nullFields)
|
||||
if err != nil {
|
||||
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to update urls"))
|
||||
}
|
||||
addCerts, deleteCerts, err := valueObjectChanges(ctx, origAm.PublicId, CertificateVO, am.Certificates, origAm.Certificates, dbMask, nullFields)
|
||||
if err != nil {
|
||||
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to update certificates"))
|
||||
}
|
||||
addMaps, deleteMaps, err := valueObjectChanges(ctx, origAm.PublicId, AccountAttributeMapsVO, am.AccountAttributeMaps, origAm.AccountAttributeMaps, dbMask, nullFields)
|
||||
if err != nil {
|
||||
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to update account attribute maps"))
|
||||
}
|
||||
|
||||
combinedMasks := append(dbMask, nullFields...)
|
||||
|
||||
var addUserSearchConf, deleteUserSearchConf any
|
||||
if strListContainsOneOf(combinedMasks, UserDnField, UserAttrField, UserFilterField) {
|
||||
if !isEmpty(origAm.UserDn, origAm.UserAttr, origAm.UserFilter) {
|
||||
usc := allocUserEntrySearchConf()
|
||||
usc.LdapMethodId = am.PublicId
|
||||
deleteUserSearchConf = usc
|
||||
}
|
||||
userDn := origAm.UserDn
|
||||
switch {
|
||||
case strutil.StrListContains(dbMask, UserDnField):
|
||||
userDn = am.UserDn
|
||||
case strutil.StrListContains(nullFields, UserDnField):
|
||||
userDn = ""
|
||||
}
|
||||
userAttr := origAm.UserAttr
|
||||
switch {
|
||||
case strutil.StrListContains(dbMask, UserAttrField):
|
||||
userAttr = am.UserAttr
|
||||
case strutil.StrListContains(nullFields, UserAttrField):
|
||||
userAttr = ""
|
||||
}
|
||||
userFilter := origAm.UserFilter
|
||||
switch {
|
||||
case strutil.StrListContains(dbMask, UserFilterField):
|
||||
userFilter = am.UserFilter
|
||||
case strutil.StrListContains(nullFields, UserFilterField):
|
||||
userFilter = ""
|
||||
}
|
||||
if !isEmpty(userDn, userAttr, userFilter) {
|
||||
addUserSearchConf, err = NewUserEntrySearchConf(ctx, am.PublicId, WithUserDn(ctx, userDn), WithUserAttr(ctx, userAttr), WithUserFilter(ctx, userFilter))
|
||||
if err != nil {
|
||||
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to update user search configuration"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var addGroupSearchConf, deleteGroupSearchConf any
|
||||
if strListContainsOneOf(combinedMasks, GroupDnField, GroupAttrField, GroupFilterField) {
|
||||
if !isEmpty(origAm.GroupDn, origAm.GroupAttr, origAm.GroupFilter) {
|
||||
gsc := allocGroupEntrySearchConf()
|
||||
gsc.LdapMethodId = am.PublicId
|
||||
deleteGroupSearchConf = gsc
|
||||
}
|
||||
groupDn := origAm.GroupDn
|
||||
switch {
|
||||
case strutil.StrListContains(dbMask, GroupDnField):
|
||||
groupDn = am.GroupDn
|
||||
case strutil.StrListContains(nullFields, GroupDnField):
|
||||
groupDn = ""
|
||||
}
|
||||
groupAttr := origAm.GroupAttr
|
||||
switch {
|
||||
case strutil.StrListContains(dbMask, GroupAttrField):
|
||||
groupAttr = am.GroupAttr
|
||||
case strutil.StrListContains(nullFields, GroupAttrField):
|
||||
groupAttr = ""
|
||||
}
|
||||
groupFilter := origAm.GroupFilter
|
||||
switch {
|
||||
case strutil.StrListContains(dbMask, GroupFilterField):
|
||||
groupFilter = am.GroupFilter
|
||||
case strutil.StrListContains(nullFields, GroupFilterField):
|
||||
groupFilter = ""
|
||||
}
|
||||
if !isEmpty(groupDn, groupAttr, groupFilter) {
|
||||
addGroupSearchConf, err = NewGroupEntrySearchConf(ctx, am.PublicId, WithGroupDn(ctx, groupDn), WithGroupAttr(ctx, groupAttr), WithGroupFilter(ctx, groupFilter))
|
||||
if err != nil {
|
||||
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to update group search configuration"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var addClientCert, deleteClientCert any
|
||||
if strListContainsOneOf(combinedMasks, ClientCertificateField, ClientCertificateKeyField) {
|
||||
if !isEmpty(origAm.ClientCertificate, origAm.ClientCertificateKey) {
|
||||
cc := allocClientCertificate()
|
||||
cc.LdapMethodId = am.PublicId
|
||||
deleteClientCert = cc
|
||||
}
|
||||
clientCertificate := origAm.ClientCertificate
|
||||
switch {
|
||||
case strutil.StrListContains(dbMask, ClientCertificateField):
|
||||
clientCertificate = am.ClientCertificate
|
||||
case strutil.StrListContains(nullFields, ClientCertificateField):
|
||||
clientCertificate = ""
|
||||
}
|
||||
clientCertificateKey := origAm.ClientCertificateKey
|
||||
switch {
|
||||
case strutil.StrListContains(dbMask, ClientCertificateKeyField):
|
||||
clientCertificateKey = am.ClientCertificateKey
|
||||
case strutil.StrListContains(nullFields, ClientCertificateKeyField):
|
||||
clientCertificateKey = nil
|
||||
}
|
||||
if !isEmpty(clientCertificate, clientCertificateKey) {
|
||||
cc, err := NewClientCertificate(ctx, am.PublicId, clientCertificateKey, clientCertificate)
|
||||
if err != nil {
|
||||
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
if err := cc.encrypt(ctx, dbWrapper); err != nil {
|
||||
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
addClientCert = cc
|
||||
}
|
||||
}
|
||||
|
||||
var addBindCred, deleteBindCred any
|
||||
if strListContainsOneOf(combinedMasks, BindDnField, BindPasswordField) {
|
||||
if !isEmpty(origAm.BindDn, origAm.BindPassword) {
|
||||
bc := allocBindCredential()
|
||||
bc.LdapMethodId = am.PublicId
|
||||
deleteBindCred = bc
|
||||
}
|
||||
bindDn := origAm.BindDn
|
||||
switch {
|
||||
case strutil.StrListContains(dbMask, BindDnField):
|
||||
bindDn = am.BindDn
|
||||
case strutil.StrListContains(nullFields, BindDnField):
|
||||
bindDn = ""
|
||||
}
|
||||
bindPassword := origAm.BindPassword
|
||||
switch {
|
||||
case strutil.StrListContains(dbMask, BindPasswordField):
|
||||
bindPassword = am.BindPassword
|
||||
case strutil.StrListContains(nullFields, BindPasswordField):
|
||||
bindPassword = ""
|
||||
}
|
||||
if !isEmpty(bindDn, bindPassword) {
|
||||
bc, err := NewBindCredential(ctx, am.PublicId, bindDn, []byte(bindPassword))
|
||||
if err != nil {
|
||||
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
if err := bc.encrypt(ctx, dbWrapper); err != nil {
|
||||
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
addBindCred = bc
|
||||
}
|
||||
}
|
||||
|
||||
var filteredDbMask, filteredNullFields []string
|
||||
for _, f := range dbMask {
|
||||
switch f {
|
||||
case
|
||||
UrlsField,
|
||||
CertificatesField,
|
||||
AccountAttributeMapsField,
|
||||
UserDnField, UserAttrField, UserFilterField,
|
||||
GroupDnField, GroupAttrField, GroupFilterField,
|
||||
ClientCertificateField, ClientCertificateKeyField,
|
||||
BindDnField, BindPasswordField:
|
||||
continue
|
||||
default:
|
||||
filteredDbMask = append(filteredDbMask, f)
|
||||
}
|
||||
}
|
||||
for _, f := range nullFields {
|
||||
switch f {
|
||||
case
|
||||
StartTlsField, InsecureTlsField, DiscoverDnField, AnonGroupSearchField, EnableGroupsField, UseTokenGroupsField,
|
||||
UrlsField,
|
||||
CertificatesField,
|
||||
AccountAttributeMapsField,
|
||||
UserDnField, UserAttrField, UserFilterField,
|
||||
GroupDnField, GroupAttrField, GroupFilterField,
|
||||
ClientCertificateField, ClientCertificateKeyField,
|
||||
BindDnField, BindPasswordField:
|
||||
continue
|
||||
default:
|
||||
filteredNullFields = append(filteredNullFields, f)
|
||||
}
|
||||
}
|
||||
|
||||
// handle no changes...
|
||||
if len(filteredDbMask) == 0 &&
|
||||
len(filteredNullFields) == 0 &&
|
||||
len(addUrls) == 0 &&
|
||||
len(deleteUrls) == 0 &&
|
||||
len(addCerts) == 0 &&
|
||||
len(deleteCerts) == 0 &&
|
||||
addUserSearchConf == nil &&
|
||||
deleteUserSearchConf == nil &&
|
||||
addGroupSearchConf == nil &&
|
||||
deleteGroupSearchConf == nil &&
|
||||
addClientCert == nil &&
|
||||
deleteClientCert == nil &&
|
||||
addBindCred == nil &&
|
||||
deleteBindCred == nil &&
|
||||
len(addMaps) == 0 &&
|
||||
len(deleteMaps) == 0 {
|
||||
return origAm, db.NoRowsAffected, nil
|
||||
}
|
||||
|
||||
oplogWrapper, err := r.kms.GetWrapper(ctx, origAm.ScopeId, kms.KeyPurposeOplog)
|
||||
if err != nil {
|
||||
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper"))
|
||||
}
|
||||
var updatedAm *AuthMethod
|
||||
var rowsUpdated int
|
||||
_, err = r.writer.DoTx(
|
||||
ctx,
|
||||
db.StdRetryCnt,
|
||||
db.ExpBackoff{},
|
||||
func(reader db.Reader, w db.Writer) error {
|
||||
msgs := make([]*oplog.Message, 0, 7) // AuthMethod, Algs*2, Certs*2, Audiences*2
|
||||
ticket, err := w.GetTicket(ctx, am)
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to get ticket"))
|
||||
}
|
||||
var authMethodOplogMsg oplog.Message
|
||||
switch {
|
||||
case len(filteredDbMask) == 0 && len(filteredNullFields) == 0:
|
||||
// the auth method's fields are not being updated, just it's value objects, so we need to just update the auth
|
||||
// method's version.
|
||||
updatedAm = am.clone()
|
||||
updatedAm.Version = uint32(version) + 1
|
||||
rowsUpdated, err = w.Update(ctx, updatedAm, []string{VersionField}, nil, db.NewOplogMsg(&authMethodOplogMsg), db.WithVersion(&version))
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to update auth method version"))
|
||||
}
|
||||
if rowsUpdated != 1 {
|
||||
return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("updated auth method version and %d rows updated", rowsUpdated))
|
||||
}
|
||||
default:
|
||||
updatedAm = am.clone()
|
||||
rowsUpdated, err = w.Update(ctx, updatedAm, filteredDbMask, filteredNullFields, db.NewOplogMsg(&authMethodOplogMsg), db.WithVersion(&version))
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to update auth method"))
|
||||
}
|
||||
if rowsUpdated != 1 {
|
||||
return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("updated auth method and %d rows updated", rowsUpdated))
|
||||
}
|
||||
}
|
||||
msgs = append(msgs, &authMethodOplogMsg)
|
||||
|
||||
if len(deleteCerts) > 0 {
|
||||
deleteCertOplogMsgs := make([]*oplog.Message, 0, len(deleteCerts))
|
||||
rowsDeleted, err := w.DeleteItems(ctx, deleteCerts, db.NewOplogMsgs(&deleteCertOplogMsgs))
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to delete certificates"))
|
||||
}
|
||||
if rowsDeleted != len(deleteCerts) {
|
||||
return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("certificates deleted %d did not match request for %d", rowsDeleted, len(deleteCerts)))
|
||||
}
|
||||
msgs = append(msgs, deleteCertOplogMsgs...)
|
||||
}
|
||||
if len(addCerts) > 0 {
|
||||
addCertsOplogMsgs := make([]*oplog.Message, 0, len(addCerts))
|
||||
if err := w.CreateItems(ctx, addCerts, db.NewOplogMsgs(&addCertsOplogMsgs)); err != nil {
|
||||
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to add certificates"))
|
||||
}
|
||||
msgs = append(msgs, addCertsOplogMsgs...)
|
||||
}
|
||||
|
||||
if len(deleteUrls) > 0 {
|
||||
deleteAudsOplogMsgs := make([]*oplog.Message, 0, len(deleteUrls))
|
||||
rowsDeleted, err := w.DeleteItems(ctx, deleteUrls, db.NewOplogMsgs(&deleteAudsOplogMsgs))
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to delete URLs"))
|
||||
}
|
||||
if rowsDeleted != len(deleteUrls) {
|
||||
return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("urls deleted %d did not match request for %d", rowsDeleted, len(deleteUrls)))
|
||||
}
|
||||
msgs = append(msgs, deleteAudsOplogMsgs...)
|
||||
}
|
||||
if len(addUrls) > 0 {
|
||||
addUrlsOplogMsgs := make([]*oplog.Message, 0, len(addUrls))
|
||||
if err := w.CreateItems(ctx, addUrls, db.NewOplogMsgs(&addUrlsOplogMsgs)); err != nil {
|
||||
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to add urls"))
|
||||
}
|
||||
msgs = append(msgs, addUrlsOplogMsgs...)
|
||||
}
|
||||
|
||||
if deleteUserSearchConf != nil {
|
||||
var deleteUserSearchConfMsg oplog.Message
|
||||
rowsDeleted, err := w.Delete(ctx, deleteUserSearchConf, db.NewOplogMsg(&deleteUserSearchConfMsg))
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to delete user search conf"))
|
||||
}
|
||||
if rowsDeleted != 1 {
|
||||
return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("user search conf deleted %d did not match request for 1", rowsDeleted))
|
||||
}
|
||||
msgs = append(msgs, &deleteUserSearchConfMsg)
|
||||
}
|
||||
if addUserSearchConf != nil {
|
||||
var addUserSearchConfOplogMsg oplog.Message
|
||||
if err := w.Create(ctx, addUserSearchConf, db.NewOplogMsg(&addUserSearchConfOplogMsg)); err != nil {
|
||||
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to add user search conf"))
|
||||
}
|
||||
msgs = append(msgs, &addUserSearchConfOplogMsg)
|
||||
}
|
||||
|
||||
if deleteGroupSearchConf != nil {
|
||||
var deleteGroupSearchConfMsg oplog.Message
|
||||
rowsDeleted, err := w.Delete(ctx, deleteGroupSearchConf, db.NewOplogMsg(&deleteGroupSearchConfMsg))
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to delete group search conf"))
|
||||
}
|
||||
if rowsDeleted != 1 {
|
||||
return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("group search conf deleted %d did not match request for 1", rowsDeleted))
|
||||
}
|
||||
msgs = append(msgs, &deleteGroupSearchConfMsg)
|
||||
}
|
||||
if addGroupSearchConf != nil {
|
||||
var addGroupSearchConfOplogMsg oplog.Message
|
||||
if err := w.Create(ctx, addGroupSearchConf, db.NewOplogMsg(&addGroupSearchConfOplogMsg)); err != nil {
|
||||
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to add group search conf"))
|
||||
}
|
||||
msgs = append(msgs, &addGroupSearchConfOplogMsg)
|
||||
}
|
||||
|
||||
if deleteClientCert != nil {
|
||||
var deleteClientCertMsg oplog.Message
|
||||
rowsDeleted, err := w.Delete(ctx, deleteClientCert, db.NewOplogMsg(&deleteClientCertMsg))
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to delete client cert"))
|
||||
}
|
||||
if rowsDeleted != 1 {
|
||||
return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("client cert deleted %d did not match request for 1", rowsDeleted))
|
||||
}
|
||||
msgs = append(msgs, &deleteClientCertMsg)
|
||||
}
|
||||
if addClientCert != nil {
|
||||
var addClientCertOplogMsg oplog.Message
|
||||
if err := w.Create(ctx, addClientCert, db.NewOplogMsg(&addClientCertOplogMsg)); err != nil {
|
||||
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to add client cert"))
|
||||
}
|
||||
msgs = append(msgs, &addClientCertOplogMsg)
|
||||
}
|
||||
|
||||
if deleteBindCred != nil {
|
||||
var deleteBindCredMsg oplog.Message
|
||||
rowsDeleted, err := w.Delete(ctx, deleteBindCred, db.NewOplogMsg(&deleteBindCredMsg))
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to delete bind credential conf"))
|
||||
}
|
||||
if rowsDeleted != 1 {
|
||||
return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("bind credential deleted %d did not match request for 1", rowsDeleted))
|
||||
}
|
||||
msgs = append(msgs, &deleteBindCredMsg)
|
||||
}
|
||||
if addBindCred != nil {
|
||||
var addBindCredOplogMsg oplog.Message
|
||||
if err := w.Create(ctx, addBindCred, db.NewOplogMsg(&addBindCredOplogMsg)); err != nil {
|
||||
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to add bind credential"))
|
||||
}
|
||||
msgs = append(msgs, &addBindCredOplogMsg)
|
||||
}
|
||||
if len(deleteMaps) > 0 {
|
||||
deleteMapsOplogMsgs := make([]*oplog.Message, 0, len(deleteMaps))
|
||||
rowsDeleted, err := w.DeleteItems(ctx, deleteMaps, db.NewOplogMsgs(&deleteMapsOplogMsgs))
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to delete account attribute maps"))
|
||||
}
|
||||
if rowsDeleted != len(deleteMaps) {
|
||||
return errors.New(ctx, errors.MultipleRecords, op, fmt.Sprintf("account attribute maps deleted %d did not match request for %d", rowsDeleted, len(deleteMaps)))
|
||||
}
|
||||
msgs = append(msgs, deleteMapsOplogMsgs...)
|
||||
}
|
||||
if len(addMaps) > 0 {
|
||||
addMapsOplogMsgs := make([]*oplog.Message, 0, len(addMaps))
|
||||
if err := w.CreateItems(ctx, addMaps, db.NewOplogMsgs(&addMapsOplogMsgs)); err != nil {
|
||||
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to add account attribute maps"))
|
||||
}
|
||||
msgs = append(msgs, addMapsOplogMsgs...)
|
||||
}
|
||||
|
||||
metadata, err := updatedAm.oplog(ctx, oplog.OpType_OP_TYPE_UPDATE)
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to generate oplog metadata"))
|
||||
}
|
||||
if err := w.WriteOplogEntryWith(ctx, oplogWrapper, ticket, metadata, msgs); err != nil {
|
||||
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to write oplog"))
|
||||
}
|
||||
// we need a new repo, that's using the same reader/writer as this TxHandler
|
||||
txRepo := &Repository{
|
||||
reader: reader,
|
||||
writer: w,
|
||||
kms: r.kms,
|
||||
// intentionally not setting the defaultLimit, so we'll get all
|
||||
// the account ids without a limit
|
||||
}
|
||||
updatedAm, err = txRepo.lookupAuthMethod(ctx, updatedAm.PublicId)
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op, errors.WithMsg("unable to lookup auth method after update"))
|
||||
}
|
||||
if updatedAm == nil {
|
||||
return errors.New(ctx, errors.RecordNotFound, op, "unable to lookup auth method after update")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
return updatedAm, rowsUpdated, nil
|
||||
}
|
||||
|
||||
// validateFieldMasks ensures that all the fields in the mask are updatable
|
||||
func validateFieldMask(ctx context.Context, fieldMaskPaths []string) error {
|
||||
const op = "ldap.validateFieldMasks"
|
||||
for _, f := range fieldMaskPaths {
|
||||
switch {
|
||||
case strings.EqualFold(OperationalStateField, f):
|
||||
case strings.EqualFold(NameField, f):
|
||||
case strings.EqualFold(DescriptionField, f):
|
||||
case strings.EqualFold(StartTlsField, f):
|
||||
case strings.EqualFold(InsecureTlsField, f):
|
||||
case strings.EqualFold(DiscoverDnField, f):
|
||||
case strings.EqualFold(AnonGroupSearchField, f):
|
||||
case strings.EqualFold(UpnDomainField, f):
|
||||
case strings.EqualFold(UserDnField, f):
|
||||
case strings.EqualFold(UserAttrField, f):
|
||||
case strings.EqualFold(UserFilterField, f):
|
||||
case strings.EqualFold(EnableGroupsField, f):
|
||||
case strings.EqualFold(UseTokenGroupsField, f):
|
||||
case strings.EqualFold(GroupDnField, f):
|
||||
case strings.EqualFold(GroupAttrField, f):
|
||||
case strings.EqualFold(GroupFilterField, f):
|
||||
case strings.EqualFold(CertificatesField, f):
|
||||
case strings.EqualFold(ClientCertificateField, f):
|
||||
case strings.EqualFold(ClientCertificateKeyField, f):
|
||||
case strings.EqualFold(BindDnField, f):
|
||||
case strings.EqualFold(BindPasswordField, f):
|
||||
case strings.EqualFold(UrlsField, f):
|
||||
case strings.EqualFold(AccountAttributeMapsField, f):
|
||||
default:
|
||||
return errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("invalid field mask: %q", f))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// voName represents the names of auth method value objects
|
||||
type voName string
|
||||
|
||||
const (
|
||||
CertificateVO voName = "Certificates"
|
||||
UrlVO voName = "Urls"
|
||||
AccountAttributeMapsVO voName = "AccountAttributeMaps"
|
||||
)
|
||||
|
||||
// validVoName decides if the name is valid
|
||||
func validVoName(name voName) bool {
|
||||
switch name {
|
||||
case CertificateVO, UrlVO, AccountAttributeMapsVO:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// factoryFunc defines a func type for value object factories
|
||||
type factoryFunc func(ctx context.Context, publicId string, idx int, s string) (any, error)
|
||||
|
||||
// supportedFactories are the currently supported factoryFunc for value objects
|
||||
var supportedFactories = map[voName]factoryFunc{
|
||||
CertificateVO: func(ctx context.Context, publicId string, idx int, s string) (any, error) {
|
||||
return NewCertificate(ctx, publicId, s)
|
||||
},
|
||||
UrlVO: func(ctx context.Context, publicId string, idx int, s string) (any, error) {
|
||||
u, err := url.Parse(s)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, "ldap.urlFactory")
|
||||
}
|
||||
return NewUrl(ctx, publicId, idx+1, u)
|
||||
},
|
||||
AccountAttributeMapsVO: func(ctx context.Context, publicId string, idx int, s string) (any, error) {
|
||||
const op = "ldap.AccountAttributeMapsFactory"
|
||||
acm, err := ParseAccountAttributeMaps(ctx, s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(acm) > 1 {
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unable to parse account attribute map %s", s))
|
||||
}
|
||||
var m AttributeMap
|
||||
for _, m = range acm {
|
||||
}
|
||||
to, err := ConvertToAccountToAttribute(ctx, m.To)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
return NewAccountAttributeMap(ctx, publicId, m.From, to)
|
||||
},
|
||||
}
|
||||
|
||||
// valueObjectChanges takes the new and old list of VOs (value objects) and
|
||||
// using the dbMasks/nullFields it will return lists of VOs which need to be
|
||||
// added and deleted in order to reconcile auth method's value objects.
|
||||
func valueObjectChanges(
|
||||
ctx context.Context,
|
||||
publicId string,
|
||||
valueObjectName voName,
|
||||
newVOs,
|
||||
oldVOs,
|
||||
dbMask,
|
||||
nullFields []string,
|
||||
) (add []any, del []any, e error) {
|
||||
const op = "ldap.valueObjectChanges"
|
||||
switch {
|
||||
case publicId == "":
|
||||
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, "missing public id")
|
||||
case !validVoName(valueObjectName):
|
||||
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("invalid value object name: %s", valueObjectName))
|
||||
case !strutil.StrListContains(dbMask, string(valueObjectName)) && !strutil.StrListContains(nullFields, string(valueObjectName)):
|
||||
return nil, nil, nil
|
||||
case len(strutil.RemoveDuplicates(newVOs, false)) != len(newVOs):
|
||||
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("duplicate new %s", valueObjectName))
|
||||
case len(strutil.RemoveDuplicates(oldVOs, false)) != len(oldVOs):
|
||||
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("duplicate old %s", valueObjectName))
|
||||
}
|
||||
|
||||
factory, ok := supportedFactories[valueObjectName]
|
||||
if !ok {
|
||||
return nil, nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unsupported factory for value object: %s", valueObjectName))
|
||||
}
|
||||
|
||||
foundVOs := map[string]int{}
|
||||
for i, a := range oldVOs {
|
||||
foundVOs[a] = i
|
||||
}
|
||||
var adds []any
|
||||
var deletes []any
|
||||
if strutil.StrListContains(nullFields, string(valueObjectName)) {
|
||||
deletes = make([]any, 0, len(oldVOs))
|
||||
for i, v := range oldVOs {
|
||||
deleteObj, err := factory(ctx, publicId, i, v)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
deletes = append(deletes, deleteObj)
|
||||
delete(foundVOs, v)
|
||||
}
|
||||
}
|
||||
if strutil.StrListContains(dbMask, string(valueObjectName)) {
|
||||
adds = make([]any, 0, len(newVOs))
|
||||
for i, v := range newVOs {
|
||||
if _, ok := foundVOs[v]; ok {
|
||||
delete(foundVOs, v)
|
||||
continue
|
||||
}
|
||||
obj, err := factory(ctx, publicId, i, v)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
adds = append(adds, obj)
|
||||
delete(foundVOs, v)
|
||||
}
|
||||
}
|
||||
if len(foundVOs) > 0 {
|
||||
for v := range foundVOs {
|
||||
obj, err := factory(ctx, publicId, foundVOs[v], v)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
deletes = append(deletes, obj)
|
||||
delete(foundVOs, v)
|
||||
}
|
||||
}
|
||||
return adds, deletes, nil
|
||||
}
|
||||
|
||||
func strListContainsOneOf(haystack []string, needles ...string) bool {
|
||||
for _, item := range haystack {
|
||||
for _, n := range needles {
|
||||
if item == n {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,153 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/kms"
|
||||
"github.com/hashicorp/boundary/internal/oplog"
|
||||
"github.com/hashicorp/cap/ldap"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultEmailAttribute = "email"
|
||||
DefaultFullNameAttribute = "fullName"
|
||||
DefaultRequestTimeout = 5 // seconds
|
||||
)
|
||||
|
||||
// Authenticate authenticates loginName and password via the auth method's
|
||||
// configured LDAP service. The account for the loginName is returned if
|
||||
// authentication is successful. Returns nil if authentication fails.
|
||||
//
|
||||
// If the AuthMethod.EnableGroups is true, then the authenticated user's groups
|
||||
// will be returned in account.
|
||||
//
|
||||
// Authenticate will update the stored values for the authenticated user's
|
||||
// Account: FullName, Email, Dn, EntryAttributes, and MemberOfGroups.
|
||||
//
|
||||
// Note: the auth_method table uses public id as its PK, so there's no need a
|
||||
// scope id parameter.
|
||||
func (r *Repository) Authenticate(ctx context.Context, authMethodId, loginName, password string) (*Account, error) {
|
||||
const op = "ldap.(Repository).Authenticate"
|
||||
switch {
|
||||
case authMethodId == "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth method id", errors.WithoutEvent())
|
||||
case loginName == "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing login name", errors.WithoutEvent())
|
||||
case password == "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing password", errors.WithoutEvent())
|
||||
}
|
||||
|
||||
// lookup auth method
|
||||
am, err := r.lookupAuthMethod(ctx, authMethodId)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to lookup auth method id: %q", authMethodId))
|
||||
}
|
||||
if am == nil {
|
||||
return nil, errors.New(ctx, errors.RecordNotFound, op, fmt.Sprintf("auth method id %q not found", authMethodId))
|
||||
}
|
||||
|
||||
// config cap ldap provider
|
||||
client, err := ldap.NewClient(ctx, &ldap.ClientConfig{
|
||||
IncludeUserAttributes: true,
|
||||
StartTLS: am.StartTls,
|
||||
InsecureTLS: am.InsecureTls,
|
||||
DiscoverDN: am.DiscoverDn,
|
||||
AnonymousGroupSearch: am.AnonGroupSearch,
|
||||
UPNDomain: am.UpnDomain,
|
||||
URLs: am.Urls,
|
||||
UserDN: am.UserDn,
|
||||
UserFilter: am.UserFilter,
|
||||
UserAttr: am.UserAttr,
|
||||
IncludeUserGroups: am.EnableGroups,
|
||||
UseTokenGroups: am.UseTokenGroups,
|
||||
GroupDN: am.GroupDn,
|
||||
GroupAttr: am.GroupAttr,
|
||||
GroupFilter: am.GroupFilter,
|
||||
Certificates: am.Certificates,
|
||||
ClientTLSKey: string(am.ClientCertificateKey),
|
||||
ClientTLSCert: am.ClientCertificate,
|
||||
BindDN: am.BindDn,
|
||||
BindPassword: am.BindPassword,
|
||||
RequestTimeout: DefaultRequestTimeout,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to initialize ldap client with auth method retrieved from database"))
|
||||
}
|
||||
defer client.Close(ctx)
|
||||
|
||||
// authen user
|
||||
authResult, err := client.Authenticate(ctx, loginName, password)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("authenticate failed"))
|
||||
}
|
||||
acct, err := NewAccount(ctx, am.ScopeId, am.PublicId, loginName)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
acctId, err := newAccountId(ctx, authMethodId, loginName)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
acct.PublicId = acctId
|
||||
acct.Dn = authResult.UserDN
|
||||
|
||||
if authResult.UserAttributes != nil {
|
||||
found, email := caseInsensitiveAttributeSearch(DefaultEmailAttribute, authResult.UserAttributes)
|
||||
if found {
|
||||
acct.Email = email[0]
|
||||
}
|
||||
found, fullName := caseInsensitiveAttributeSearch(DefaultFullNameAttribute, authResult.UserAttributes)
|
||||
if found {
|
||||
acct.FullName = fullName[0]
|
||||
}
|
||||
}
|
||||
if len(authResult.Groups) > 0 {
|
||||
encodedGroups, err := json.Marshal(authResult.Groups)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to encode user groups"))
|
||||
}
|
||||
acct.MemberOfGroups = string(encodedGroups)
|
||||
}
|
||||
|
||||
databaseWrapper, err := r.kms.GetWrapper(ctx, am.ScopeId, kms.KeyPurposeDatabase)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
|
||||
md, err := acct.oplog(ctx, oplog.OpType_OP_TYPE_CREATE)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
// upsert account
|
||||
if err := r.writer.Create(
|
||||
ctx,
|
||||
acct,
|
||||
db.WithOnConflict(&db.OnConflict{
|
||||
Target: db.Columns{"public_id"}, // id is predictable and uses both auth method id and login name for inputs
|
||||
Action: db.SetColumns([]string{"full_name", "email", "dn", "member_of_groups"}),
|
||||
}),
|
||||
db.WithOplog(databaseWrapper, md),
|
||||
); err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to create/update ldap account"))
|
||||
}
|
||||
|
||||
// return account
|
||||
return acct, nil
|
||||
}
|
||||
|
||||
func caseInsensitiveAttributeSearch(attrName string, attributes map[string][]string) (bool, []string) {
|
||||
for k, v := range attributes {
|
||||
if strings.EqualFold(k, attrName) {
|
||||
return true, v
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/hashicorp/boundary/internal/auth/ldap/store"
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/iam"
|
||||
"github.com/hashicorp/boundary/internal/kms"
|
||||
"github.com/hashicorp/cap/ldap"
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/jimlambrt/gldap"
|
||||
"github.com/jimlambrt/gldap/testdirectory"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/testing/protocmp"
|
||||
)
|
||||
|
||||
func TestRepository_authenticate(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCtx := context.Background()
|
||||
|
||||
rootWrapper := db.TestWrapper(t)
|
||||
|
||||
testConn, _ := db.TestSetup(t, "postgres")
|
||||
testRw := db.New(testConn)
|
||||
|
||||
testKms := kms.TestKms(t, testConn, rootWrapper)
|
||||
|
||||
testRepo, err := NewRepository(testCtx, testRw, testRw, testKms)
|
||||
require.NoError(t, err)
|
||||
|
||||
iamRepo := iam.TestRepo(t, testConn, rootWrapper)
|
||||
org, _ := iam.TestScopes(t, iamRepo)
|
||||
orgDbWrapper, err := testKms.GetWrapper(testCtx, org.PublicId, kms.KeyPurposeDatabase)
|
||||
require.NoError(t, err)
|
||||
|
||||
logger := hclog.New(&hclog.LoggerOptions{
|
||||
Name: "test-logger",
|
||||
Level: hclog.Error,
|
||||
})
|
||||
td := testdirectory.Start(t,
|
||||
testdirectory.WithDefaults(t, &testdirectory.Defaults{AllowAnonymousBind: true}),
|
||||
testdirectory.WithLogger(t, logger),
|
||||
)
|
||||
tdCerts, err := ParseCertificates(testCtx, td.Cert())
|
||||
require.NoError(t, err)
|
||||
|
||||
testAm := TestAuthMethod(t, testConn, orgDbWrapper, org.PublicId,
|
||||
[]string{fmt.Sprintf("ldaps://127.0.0.1:%d", td.Port())},
|
||||
WithCertificates(testCtx, tdCerts...),
|
||||
WithDiscoverDn(testCtx),
|
||||
WithEnableGroups(testCtx),
|
||||
WithUserDn(testCtx, testdirectory.DefaultUserDN),
|
||||
WithGroupDn(testCtx, testdirectory.DefaultGroupDN),
|
||||
)
|
||||
|
||||
const (
|
||||
testLoginName = "alice"
|
||||
testPassword = "password"
|
||||
)
|
||||
|
||||
testAccount := TestAccount(t, testConn, testAm, testLoginName)
|
||||
|
||||
groups := []*gldap.Entry{
|
||||
testdirectory.NewGroup(t, "admin", []string{"alice"}),
|
||||
testdirectory.NewGroup(t, "admin", []string{"eve"}, testdirectory.WithDefaults(t, &testdirectory.Defaults{UPNDomain: "example.com"})),
|
||||
}
|
||||
tokenGroups := map[string][]*gldap.Entry{
|
||||
"S-1-1": {
|
||||
testdirectory.NewGroup(t, "admin-token-group", []string{"alice"}),
|
||||
},
|
||||
}
|
||||
sidBytes, err := ldap.SIDBytes(1, 1)
|
||||
require.NoError(t, err)
|
||||
users := testdirectory.NewUsers(t, []string{"alice", "bob"}, testdirectory.WithMembersOf(t, "admin"), testdirectory.WithTokenGroups(t, sidBytes))
|
||||
users = append(
|
||||
users,
|
||||
testdirectory.NewUsers(
|
||||
t,
|
||||
[]string{"eve"},
|
||||
testdirectory.WithDefaults(t, &testdirectory.Defaults{UPNDomain: "example.com"}),
|
||||
testdirectory.WithMembersOf(t, "admin"))...,
|
||||
)
|
||||
// add some attributes that we always want to filter out of an AuthResult,
|
||||
// so if we ever start seeing tests fail because of them; we know that we've
|
||||
// messed up the default filtering
|
||||
for _, u := range users {
|
||||
u.Attributes = append(u.Attributes,
|
||||
gldap.NewEntryAttribute(ldap.DefaultADUserPasswordAttribute, []string{"password"}),
|
||||
gldap.NewEntryAttribute(ldap.DefaultOpenLDAPUserPasswordAttribute, []string{"password"}),
|
||||
gldap.NewEntryAttribute("fullName", []string{"test-full-name"}),
|
||||
)
|
||||
}
|
||||
td.SetUsers(users...)
|
||||
td.SetGroups(groups...)
|
||||
td.SetTokenGroups(tokenGroups)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
repo *Repository
|
||||
authMethodId string
|
||||
loginName string
|
||||
password string
|
||||
want func(got *Account) *Account
|
||||
wantErrMatch *errors.Template
|
||||
wantErrContains string
|
||||
}{
|
||||
{
|
||||
name: "success-existing-account",
|
||||
ctx: testCtx,
|
||||
repo: testRepo,
|
||||
authMethodId: testAm.PublicId,
|
||||
loginName: testAccount.LoginName,
|
||||
password: testPassword,
|
||||
want: func(got *Account) *Account {
|
||||
a := &Account{Account: &store.Account{
|
||||
AuthMethodId: testAm.PublicId,
|
||||
ScopeId: testAccount.ScopeId,
|
||||
PublicId: testAccount.PublicId,
|
||||
Version: testAccount.Version,
|
||||
Dn: "cn=alice,ou=people,dc=example,dc=org",
|
||||
Email: "alice@example.com",
|
||||
FullName: "test-full-name",
|
||||
LoginName: "alice",
|
||||
MemberOfGroups: "[\"cn=admin,ou=groups,dc=example,dc=org\"]",
|
||||
}}
|
||||
return a
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "success-new-account-with-no-groups",
|
||||
ctx: testCtx,
|
||||
repo: testRepo,
|
||||
authMethodId: testAm.PublicId,
|
||||
loginName: "bob",
|
||||
password: testPassword,
|
||||
want: func(got *Account) *Account {
|
||||
a := &Account{Account: &store.Account{
|
||||
AuthMethodId: testAm.PublicId,
|
||||
ScopeId: testAm.ScopeId,
|
||||
PublicId: got.PublicId,
|
||||
Version: got.Version,
|
||||
Dn: "cn=bob,ou=people,dc=example,dc=org",
|
||||
Email: "bob@example.com",
|
||||
FullName: "test-full-name",
|
||||
LoginName: "bob",
|
||||
}}
|
||||
return a
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing-auth-method-id",
|
||||
ctx: testCtx,
|
||||
repo: testRepo,
|
||||
loginName: "alice",
|
||||
password: testPassword,
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing auth method id",
|
||||
},
|
||||
{
|
||||
name: "missing-login-name",
|
||||
ctx: testCtx,
|
||||
repo: testRepo,
|
||||
authMethodId: testAm.PublicId,
|
||||
password: testPassword,
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing login name",
|
||||
},
|
||||
{
|
||||
name: "missing-password",
|
||||
ctx: testCtx,
|
||||
repo: testRepo,
|
||||
authMethodId: testAm.PublicId,
|
||||
loginName: "alice",
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing password",
|
||||
},
|
||||
{
|
||||
name: "auth-method-id-not-found",
|
||||
ctx: testCtx,
|
||||
repo: testRepo,
|
||||
authMethodId: "auth-method-id-not-found",
|
||||
loginName: "alice",
|
||||
password: testPassword,
|
||||
wantErrMatch: errors.T(errors.RecordNotFound),
|
||||
wantErrContains: "auth method id \"auth-method-id-not-found\" not found",
|
||||
},
|
||||
{
|
||||
name: "auth-method-id-lookup-err",
|
||||
ctx: testCtx,
|
||||
repo: func() *Repository {
|
||||
conn, mock := db.TestSetupWithMock(t)
|
||||
mock.ExpectQuery(`SELECT`).WillReturnError(fmt.Errorf("auth-method-id-lookup-err"))
|
||||
rw := db.New(conn)
|
||||
r, err := NewRepository(testCtx, rw, rw, testKms)
|
||||
require.NoError(t, err)
|
||||
return r
|
||||
}(),
|
||||
authMethodId: testAm.PublicId,
|
||||
loginName: "alice",
|
||||
password: testPassword,
|
||||
wantErrMatch: errors.T(errors.Unknown),
|
||||
wantErrContains: "auth-method-id-lookup-err",
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
got, err := tc.repo.Authenticate(tc.ctx, tc.authMethodId, tc.loginName, tc.password)
|
||||
if tc.wantErrMatch != nil {
|
||||
require.Error(err)
|
||||
assert.Nil(got)
|
||||
assert.Truef(errors.Match(tc.wantErrMatch, err), "unexpected error: %s", err.Error())
|
||||
if tc.wantErrContains != "" {
|
||||
assert.Contains(err.Error(), tc.wantErrContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
require.NotNil(tc.want)
|
||||
assert.NotEmpty(got.UpdateTime)
|
||||
assert.NotEmpty(got.CreateTime)
|
||||
w := tc.want(got)
|
||||
w.UpdateTime = got.UpdateTime
|
||||
w.CreateTime = got.CreateTime
|
||||
assert.Empty(cmp.Diff(w, got, protocmp.Transform()))
|
||||
})
|
||||
}
|
||||
t.Run("use-token-groups", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
amWithTokenGroups := TestAuthMethod(t, testConn, orgDbWrapper, org.PublicId,
|
||||
[]string{fmt.Sprintf("ldaps://127.0.0.1:%d", td.Port())},
|
||||
WithCertificates(testCtx, tdCerts...),
|
||||
WithDiscoverDn(testCtx),
|
||||
WithEnableGroups(testCtx),
|
||||
WithUserDn(testCtx, testdirectory.DefaultUserDN),
|
||||
WithUseTokenGroups(testCtx),
|
||||
)
|
||||
|
||||
got, err := testRepo.Authenticate(testCtx, amWithTokenGroups.PublicId, testLoginName, testPassword)
|
||||
require.NoError(err)
|
||||
assert.NotNil(got)
|
||||
assert.NotEmpty(got.UpdateTime)
|
||||
assert.NotEmpty(got.CreateTime)
|
||||
w := &Account{Account: &store.Account{
|
||||
AuthMethodId: amWithTokenGroups.PublicId,
|
||||
ScopeId: amWithTokenGroups.ScopeId,
|
||||
PublicId: got.PublicId,
|
||||
Version: got.Version,
|
||||
Dn: "cn=alice,ou=people,dc=example,dc=org",
|
||||
Email: "alice@example.com",
|
||||
FullName: "test-full-name",
|
||||
LoginName: "alice",
|
||||
MemberOfGroups: "[\"cn=admin-token-group,ou=groups,dc=example,dc=org\"]",
|
||||
}}
|
||||
w.UpdateTime = got.UpdateTime
|
||||
w.CreateTime = got.CreateTime
|
||||
assert.Empty(cmp.Diff(w, got, protocmp.Transform()))
|
||||
})
|
||||
t.Run("authenticate-err", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
|
||||
amWithNoCerts := TestAuthMethod(t, testConn, orgDbWrapper, org.PublicId,
|
||||
[]string{fmt.Sprintf("ldaps://127.0.0.1:%d", td.Port())},
|
||||
WithDiscoverDn(testCtx),
|
||||
WithEnableGroups(testCtx),
|
||||
WithUserDn(testCtx, testdirectory.DefaultUserDN),
|
||||
WithGroupDn(testCtx, testdirectory.DefaultGroupDN),
|
||||
)
|
||||
got, err := testRepo.Authenticate(testCtx, amWithNoCerts.PublicId, testLoginName, testPassword)
|
||||
require.Error(err)
|
||||
assert.Contains(err.Error(), "authenticate failed")
|
||||
assert.Contains(err.Error(), "failed to connect")
|
||||
assert.Nil(got)
|
||||
})
|
||||
}
|
||||
|
||||
func Test_caseInsensitiveAttributeSearch(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert := assert.New(t)
|
||||
|
||||
found, values := caseInsensitiveAttributeSearch("fullName", map[string][]string{"fullName": {"eve"}})
|
||||
assert.True(found)
|
||||
assert.Equal([]string{"eve"}, values)
|
||||
|
||||
found, values = caseInsensitiveAttributeSearch("preferredName", map[string][]string{"fullName": {"eve"}})
|
||||
assert.False(found)
|
||||
assert.Empty(values)
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/kms"
|
||||
"github.com/hashicorp/boundary/internal/oplog"
|
||||
"github.com/hashicorp/go-dbw"
|
||||
)
|
||||
|
||||
// CreateManagedGroup inserts an ManagedGroup, mg, into the repository and
|
||||
// returns a new ManagedGroup containing its PublicId. mg is not changed. mg
|
||||
// must contain a valid AuthMethodId. mg must not contain a PublicId. The
|
||||
// PublicId is generated and assigned by this method. All options are ignored.
|
||||
//
|
||||
// Both mg.Name and mg.Description are optional. If mg.Name is set, it must be
|
||||
// unique within mg.AuthMethodId.
|
||||
func (r *Repository) CreateManagedGroup(ctx context.Context, scopeId string, mg *ManagedGroup, _ ...Option) (*ManagedGroup, error) {
|
||||
const op = "ldap.(Repository).CreateManagedGroup"
|
||||
switch {
|
||||
case mg == nil:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing managed group")
|
||||
case mg.ManagedGroup == nil:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing embedded managed group")
|
||||
case mg.AuthMethodId == "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth method id")
|
||||
case len(mg.GroupNames) == 0:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing group names")
|
||||
case mg.PublicId != "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "public id must be empty")
|
||||
case scopeId == "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing scope id")
|
||||
}
|
||||
|
||||
mg = mg.clone()
|
||||
|
||||
id, err := newManagedGroupId(ctx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
mg.PublicId = id
|
||||
|
||||
oplogWrapper, err := r.kms.GetWrapper(ctx, scopeId, kms.KeyPurposeOplog)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper"), errors.WithCode(errors.Encrypt))
|
||||
}
|
||||
|
||||
oplogMetadata, err := mg.oplog(ctx, oplog.OpType_OP_TYPE_CREATE, scopeId)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to generate managed group oplog metadata"))
|
||||
}
|
||||
|
||||
var newManagedGroup *ManagedGroup
|
||||
_, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{},
|
||||
func(_ db.Reader, w db.Writer) error {
|
||||
newManagedGroup = mg.clone()
|
||||
if err := w.Create(ctx, newManagedGroup, db.WithOplog(oplogWrapper, oplogMetadata)); err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
if errors.IsUniqueError(err) {
|
||||
return nil, errors.New(ctx, errors.NotUnique, op, fmt.Sprintf(
|
||||
"in auth method %s: name %q already exists",
|
||||
mg.AuthMethodId, mg.Name))
|
||||
}
|
||||
return nil, errors.Wrap(ctx, err, op, errors.WithMsg(mg.AuthMethodId))
|
||||
}
|
||||
return newManagedGroup, nil
|
||||
}
|
||||
|
||||
// LookupManagedGroup will look up a managed group in the repository. If the managed group is not
|
||||
// found, it will return nil, nil. All options are ignored.
|
||||
func (r *Repository) LookupManagedGroup(ctx context.Context, withPublicId string, _ ...Option) (*ManagedGroup, error) {
|
||||
const op = "ldap.(Repository).LookupManagedGroup"
|
||||
if withPublicId == "" {
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing public id")
|
||||
}
|
||||
a := AllocManagedGroup()
|
||||
a.PublicId = withPublicId
|
||||
if err := r.reader.LookupByPublicId(ctx, a); err != nil {
|
||||
if errors.IsNotFoundError(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed for %s", withPublicId)))
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// ListManagedGroups in an auth method and supports WithLimit option.
|
||||
func (r *Repository) ListManagedGroups(ctx context.Context, withAuthMethodId string, opt ...Option) ([]*ManagedGroup, error) {
|
||||
const op = "ldap.(Repository).ListManagedGroups"
|
||||
if withAuthMethodId == "" {
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth method id")
|
||||
}
|
||||
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
|
||||
}
|
||||
var mgs []*ManagedGroup
|
||||
err = r.reader.SearchWhere(ctx, &mgs, "auth_method_id = ?", []any{withAuthMethodId}, db.WithLimit(limit))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
return mgs, nil
|
||||
}
|
||||
|
||||
// DeleteManagedGroup deletes the managed group for the provided id from the
|
||||
// repository returning a count of the number of records deleted. All options
|
||||
// are ignored.
|
||||
func (r *Repository) DeleteManagedGroup(ctx context.Context, scopeId, withPublicId string, opt ...Option) (int, error) {
|
||||
const op = "ldap.(Repository).DeleteManagedGroup"
|
||||
switch {
|
||||
case withPublicId == "":
|
||||
return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing public id")
|
||||
case scopeId == "":
|
||||
return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing scope id")
|
||||
}
|
||||
mg := AllocManagedGroup()
|
||||
mg.PublicId = withPublicId
|
||||
if err := r.reader.LookupById(ctx, mg); err != nil {
|
||||
return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("managed group not found"))
|
||||
}
|
||||
oplogWrapper, err := r.kms.GetWrapper(ctx, scopeId, kms.KeyPurposeOplog)
|
||||
if err != nil {
|
||||
return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt), errors.WithMsg("unable to get oplog wrapper"))
|
||||
}
|
||||
|
||||
metadata, err := mg.oplog(ctx, oplog.OpType_OP_TYPE_DELETE, scopeId)
|
||||
if err != nil {
|
||||
return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to generate oplog metadata"))
|
||||
}
|
||||
|
||||
var rowsDeleted int
|
||||
_, err = r.writer.DoTx(
|
||||
ctx,
|
||||
db.StdRetryCnt,
|
||||
db.ExpBackoff{},
|
||||
func(_ db.Reader, w db.Writer) (err error) {
|
||||
dMg := mg.clone()
|
||||
rowsDeleted, err = w.Delete(ctx, dMg, db.WithOplog(oplogWrapper, metadata))
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
if rowsDeleted > 1 {
|
||||
return errors.New(ctx, errors.MultipleRecords, op, "more than 1 resource would have been deleted")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(withPublicId))
|
||||
}
|
||||
|
||||
return rowsDeleted, nil
|
||||
}
|
||||
|
||||
// UpdateManagedGroup updates the repository entry for mg.PublicId with the
|
||||
// values in mg for the fields listed in fieldMaskPaths. It returns a new
|
||||
// ManagedGroup containing the updated values and a count of the number of
|
||||
// records updated. mg is not changed.
|
||||
//
|
||||
// mg must contain a valid PublicId. Only mg.Name, mg.Description, and mg.GroupNames
|
||||
// can be updated. If mg.Name is set to a non-empty string, it must be unique
|
||||
// within mg.AuthMethodId.
|
||||
//
|
||||
// An attribute of a will be set to NULL in the database if the attribute in a
|
||||
// is the zero value and it is included in fieldMaskPaths.
|
||||
func (r *Repository) UpdateManagedGroup(ctx context.Context, scopeId string, mg *ManagedGroup, version uint32, fieldMaskPaths []string, opt ...Option) (*ManagedGroup, int, error) {
|
||||
const op = "ldap.(Repository).UpdateManagedGroup"
|
||||
switch {
|
||||
case mg == nil:
|
||||
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing ManagedGroup")
|
||||
case mg.ManagedGroup == nil:
|
||||
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing embedded ManagedGroup")
|
||||
case mg.PublicId == "":
|
||||
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing public id")
|
||||
case version == 0:
|
||||
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing version")
|
||||
case scopeId == "":
|
||||
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing scope id")
|
||||
}
|
||||
|
||||
for _, f := range fieldMaskPaths {
|
||||
switch {
|
||||
case strings.EqualFold(NameField, f):
|
||||
case strings.EqualFold(DescriptionField, f):
|
||||
case strings.EqualFold(GroupNamesField, f):
|
||||
default:
|
||||
return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidFieldMask, op, f)
|
||||
}
|
||||
}
|
||||
var dbMask, nullFields []string
|
||||
dbMask, nullFields = dbw.BuildUpdatePaths(
|
||||
map[string]any{
|
||||
NameField: mg.Name,
|
||||
DescriptionField: mg.Description,
|
||||
GroupNamesField: mg.GroupNames,
|
||||
},
|
||||
fieldMaskPaths,
|
||||
nil,
|
||||
)
|
||||
if len(dbMask) == 0 && len(nullFields) == 0 {
|
||||
return nil, db.NoRowsAffected, errors.New(ctx, errors.EmptyFieldMask, op, "missing field mask")
|
||||
}
|
||||
|
||||
oplogWrapper, err := r.kms.GetWrapper(ctx, scopeId, kms.KeyPurposeOplog)
|
||||
if err != nil {
|
||||
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt),
|
||||
errors.WithMsg(("unable to get oplog wrapper")))
|
||||
}
|
||||
|
||||
foundMg := AllocManagedGroup()
|
||||
foundMg.PublicId = mg.PublicId
|
||||
if err := r.reader.LookupById(ctx, foundMg); err != nil {
|
||||
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("managed group not found"))
|
||||
}
|
||||
metadata, err := foundMg.oplog(ctx, oplog.OpType_OP_TYPE_UPDATE, scopeId)
|
||||
if err != nil {
|
||||
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to generate oplog metadata"))
|
||||
}
|
||||
var rowsUpdated int
|
||||
var returnedManagedGroup *ManagedGroup
|
||||
_, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{},
|
||||
func(_ db.Reader, w db.Writer) error {
|
||||
returnedManagedGroup = mg.clone()
|
||||
var err error
|
||||
rowsUpdated, err = w.Update(ctx, returnedManagedGroup, dbMask, nullFields, db.WithOplog(oplogWrapper, metadata), db.WithVersion(&version))
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op)
|
||||
}
|
||||
if rowsUpdated > 1 {
|
||||
return errors.New(ctx, errors.MultipleRecords, op, "more than 1 resource would have been updated")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
if errors.IsUniqueError(err) {
|
||||
return nil, db.NoRowsAffected, errors.New(ctx, errors.NotUnique, op,
|
||||
fmt.Sprintf("name %s already exists: %s", mg.Name, mg.PublicId))
|
||||
}
|
||||
return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg(mg.PublicId))
|
||||
}
|
||||
|
||||
return returnedManagedGroup, rowsUpdated, nil
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/auth/oidc/store"
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
)
|
||||
|
||||
// managedGroupMemberAccountTableName defines the default table name for a Managed Group
|
||||
const managedGroupMemberAccountTableName = "auth_ldap_managed_group_member_account"
|
||||
|
||||
// ManagedGroupMemberAccount contains a mapping between a managed group and a
|
||||
// member account.
|
||||
type ManagedGroupMemberAccount struct {
|
||||
*store.ManagedGroupMemberAccount
|
||||
tableName string
|
||||
}
|
||||
|
||||
// TableName returns the table name.
|
||||
func (mg *ManagedGroupMemberAccount) TableName() string {
|
||||
if mg.tableName != "" {
|
||||
return mg.tableName
|
||||
}
|
||||
return managedGroupMemberAccountTableName
|
||||
}
|
||||
|
||||
// SetTableName sets the table name.
|
||||
func (mg *ManagedGroupMemberAccount) SetTableName(n string) {
|
||||
mg.tableName = n
|
||||
}
|
||||
|
||||
// ListManagedGroupMembershipsByMember lists managed group memberships via the
|
||||
// member (account) ID and supports WithLimit option.
|
||||
func (r *Repository) ListManagedGroupMembershipsByMember(ctx context.Context, withAcctId string, opt ...Option) ([]*ManagedGroupMemberAccount, error) {
|
||||
const op = "ldap.(Repository).ListManagedGroupMembershipsByMember"
|
||||
if withAcctId == "" {
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing account id")
|
||||
}
|
||||
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
|
||||
}
|
||||
var mgs []*ManagedGroupMemberAccount
|
||||
err = r.reader.SearchWhere(ctx, &mgs, "member_id = ?", []any{withAcctId}, db.WithLimit(limit))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
return mgs, nil
|
||||
}
|
||||
|
||||
// ListManagedGroupMembershipsByGroup lists managed group memberships via the
|
||||
// group ID and supports WithLimit option.
|
||||
func (r *Repository) ListManagedGroupMembershipsByGroup(ctx context.Context, withGroupId string, opt ...Option) ([]*ManagedGroupMemberAccount, error) {
|
||||
const op = "ldap.(Repository).ListManagedGroupMembershipsByGroup"
|
||||
if withGroupId == "" {
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing managed group id")
|
||||
}
|
||||
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
|
||||
}
|
||||
var mgs []*ManagedGroupMemberAccount
|
||||
err = r.reader.SearchWhere(ctx, &mgs, "managed_group_id = ?", []any{withGroupId}, db.WithLimit(limit))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
return mgs, nil
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ldap_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"math/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/auth/ldap"
|
||||
"github.com/hashicorp/boundary/internal/auth/oidc/store"
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/iam"
|
||||
"github.com/hashicorp/boundary/internal/kms"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_ManagedGroupMemberships(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testConn, _ := db.TestSetup(t, "postgres")
|
||||
testRootWrapper := db.TestWrapper(t)
|
||||
testRw := db.New(testConn)
|
||||
|
||||
testKms := kms.TestKms(t, testConn, testRootWrapper)
|
||||
iamRepo := iam.TestRepo(t, testConn, testRootWrapper)
|
||||
|
||||
testCtx := context.Background()
|
||||
testGlobalDbWrapper, err := testKms.GetWrapper(testCtx, "global", kms.KeyPurposeDatabase)
|
||||
require.NoError(t, err)
|
||||
|
||||
testAuthMethod := ldap.TestAuthMethod(t, testConn, testGlobalDbWrapper, "global", []string{"ldaps://ldap1"})
|
||||
testAuthMethodStatic := ldap.TestAuthMethod(t, testConn, testGlobalDbWrapper, "global", []string{"ldaps://ldap1"})
|
||||
|
||||
repo, err := ldap.NewRepository(testCtx, testRw, testRw, testKms)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, repo)
|
||||
|
||||
// make some groups with GroupNames which will initially match no accounts
|
||||
mgs := make([]*ldap.ManagedGroup, 0, 100)
|
||||
for i := 0; i < 100; i++ {
|
||||
got := ldap.TestManagedGroup(t, testConn, testAuthMethod, []string{"foo", "bar"})
|
||||
mgs = append(mgs, got)
|
||||
}
|
||||
|
||||
testUser := iam.TestUser(t, iamRepo, "global")
|
||||
staticAccount := ldap.TestAccount(t, testConn, testAuthMethodStatic, "test-static-login-name", ldap.WithMemberOfGroups(testCtx, "static"))
|
||||
staticGroup := ldap.TestManagedGroup(t, testConn, testAuthMethod, []string{"static"})
|
||||
const staticMembershipCount = 1
|
||||
|
||||
testGroupNames := []string{"admin", "users"}
|
||||
testGroupNamesEncodes, err := json.Marshal(testGroupNames)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Fetch existing default admin user u_1234567890 which will have 20 static
|
||||
// groups associated with it. Then associate a new ldap account to that
|
||||
// user
|
||||
account := ldap.TestAccount(t, testConn, testAuthMethod, "test-login-name", ldap.WithMemberOfGroups(testCtx, testGroupNames...))
|
||||
|
||||
adminUser, _, err := iamRepo.LookupUser(testCtx, testUser.PublicId)
|
||||
require.NoError(t, err)
|
||||
accts, err := iamRepo.AddUserAccounts(testCtx, testUser.PublicId, adminUser.Version, []string{account.PublicId, staticAccount.PublicId})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, accts, 2)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
account *ldap.Account
|
||||
wantMgsCount int
|
||||
specificMgs []*ldap.ManagedGroup
|
||||
wantErr errors.Code
|
||||
wantErrContains string
|
||||
}{
|
||||
{
|
||||
name: "valid fixed",
|
||||
account: account,
|
||||
specificMgs: mgs[0:20],
|
||||
},
|
||||
{
|
||||
name: "valid fixed, same values",
|
||||
account: account,
|
||||
specificMgs: mgs[0:20],
|
||||
},
|
||||
{
|
||||
name: "valid fixed, new values",
|
||||
account: account,
|
||||
specificMgs: mgs[20:40],
|
||||
},
|
||||
{
|
||||
name: "valid none",
|
||||
account: account,
|
||||
wantMgsCount: 0,
|
||||
},
|
||||
{
|
||||
name: "valid none, second test, testing gracefully aborting",
|
||||
account: account,
|
||||
wantMgsCount: 0,
|
||||
},
|
||||
{
|
||||
name: "valid fixed, prep for random",
|
||||
account: account,
|
||||
specificMgs: mgs[20:50],
|
||||
},
|
||||
{
|
||||
name: "valid random",
|
||||
account: account,
|
||||
wantMgsCount: 30,
|
||||
},
|
||||
{
|
||||
name: "valid random, second test",
|
||||
account: account,
|
||||
wantMgsCount: 20,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
|
||||
// We are intentionally carrying things over between tests to be
|
||||
// more realistic but that means we need correct versions, so update
|
||||
// them first.
|
||||
currMgs, err := repo.ListManagedGroups(testCtx, testAuthMethod.PublicId)
|
||||
require.NoError(err)
|
||||
require.Len(currMgs, 101)
|
||||
currVersionMap := make(map[string]uint32, len(currMgs))
|
||||
for _, currMg := range currMgs {
|
||||
currVersionMap[currMg.PublicId] = currMg.Version
|
||||
}
|
||||
for _, mg := range mgs {
|
||||
mg.Version = currVersionMap[mg.PublicId]
|
||||
}
|
||||
|
||||
var mgsToTest []*ldap.ManagedGroup
|
||||
var finalMgs map[string]*ldap.ManagedGroup
|
||||
|
||||
mgsToTest = tc.specificMgs
|
||||
if mgsToTest == nil {
|
||||
// Select at random
|
||||
mgsToTest = make([]*ldap.ManagedGroup, tc.wantMgsCount)
|
||||
for i := 0; i < tc.wantMgsCount; i++ {
|
||||
mg := mgs[rand.Int()%len(mgs)]
|
||||
mgsToTest[i] = mg
|
||||
}
|
||||
}
|
||||
finalMgs = make(map[string]*ldap.ManagedGroup)
|
||||
for _, v := range mgsToTest {
|
||||
v.GroupNames = string(testGroupNamesEncodes)
|
||||
v, _, err = repo.UpdateManagedGroup(testCtx, "global", v, v.Version, []string{"GroupNames"})
|
||||
require.NoError(err)
|
||||
finalMgs[v.PublicId] = v
|
||||
}
|
||||
|
||||
// Ensure the same set was found; all memberships found should have
|
||||
// been in the finalMgs map, and when they are all removed there
|
||||
// should be nothing left.
|
||||
for _, mship := range mgsToTest {
|
||||
// Randomly check a few to ensure the MembershipsByGroup function works
|
||||
members, err := repo.ListManagedGroupMembershipsByGroup(testCtx, mship.PublicId)
|
||||
require.NoError(err)
|
||||
require.NotEmpty(members)
|
||||
var found bool
|
||||
for _, v := range members {
|
||||
if v.MemberId == tc.account.GetPublicId() {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.True(found)
|
||||
delete(finalMgs, mship.PublicId)
|
||||
}
|
||||
assert.Len(finalMgs, 0)
|
||||
|
||||
// Now check that the static account still has the same memberships
|
||||
memberships, err := repo.ListManagedGroupMembershipsByMember(testCtx, staticAccount.PublicId)
|
||||
require.NoError(err)
|
||||
assert.Len(memberships, staticMembershipCount)
|
||||
assert.Equal(memberships[0].ManagedGroupId, staticGroup.PublicId)
|
||||
})
|
||||
}
|
||||
t.Run("ListManagedGroupMembershipsByGroup-invalid-parameters", func(t *testing.T) {
|
||||
got, err := repo.ListManagedGroupMembershipsByGroup(testCtx, "")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "missing managed group id")
|
||||
assert.Nil(t, got)
|
||||
})
|
||||
t.Run("ListManagedGroupMembershipsByMember-invalid-parameter", func(t *testing.T) {
|
||||
got, err := repo.ListManagedGroupMembershipsByMember(testCtx, "")
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "missing account id")
|
||||
assert.Nil(t, got)
|
||||
})
|
||||
}
|
||||
|
||||
func TestManagedGroupMemberAccount_SetTableName(t *testing.T) {
|
||||
t.Parallel()
|
||||
allocFn := func() *ldap.ManagedGroupMemberAccount {
|
||||
return &ldap.ManagedGroupMemberAccount{
|
||||
ManagedGroupMemberAccount: &store.ManagedGroupMemberAccount{},
|
||||
}
|
||||
}
|
||||
defaultTableName := "auth_ldap_managed_group_member_account"
|
||||
tests := []struct {
|
||||
name string
|
||||
setNameTo string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "new-name",
|
||||
setNameTo: "new-name",
|
||||
want: "new-name",
|
||||
},
|
||||
{
|
||||
name: "reset to default",
|
||||
setNameTo: "",
|
||||
want: defaultTableName,
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
def := allocFn()
|
||||
require.Equal(defaultTableName, def.TableName())
|
||||
m := allocFn()
|
||||
m.SetTableName(tc.setNameTo)
|
||||
assert.Equal(tc.want, m.TableName())
|
||||
})
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,121 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/kms"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewRepository(t *testing.T) {
|
||||
ctx := context.TODO()
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
rw := db.New(conn)
|
||||
wrapper := db.TestWrapper(t)
|
||||
kmsCache := kms.TestKms(t, conn, wrapper)
|
||||
|
||||
type args struct {
|
||||
r db.Reader
|
||||
w db.Writer
|
||||
kms *kms.Kms
|
||||
opts []Option
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *Repository
|
||||
wantErrMatch *errors.Template
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
args: args{
|
||||
r: rw,
|
||||
w: rw,
|
||||
kms: kmsCache,
|
||||
},
|
||||
want: &Repository{
|
||||
reader: rw,
|
||||
writer: rw,
|
||||
kms: kmsCache,
|
||||
defaultLimit: db.DefaultLimit,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid with limit",
|
||||
args: args{
|
||||
r: rw,
|
||||
w: rw,
|
||||
kms: kmsCache,
|
||||
opts: []Option{WithLimit(context.Background(), 5)},
|
||||
},
|
||||
want: &Repository{
|
||||
reader: rw,
|
||||
writer: rw,
|
||||
kms: kmsCache,
|
||||
defaultLimit: 5,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nil-reader",
|
||||
args: args{
|
||||
r: nil,
|
||||
w: rw,
|
||||
kms: kmsCache,
|
||||
},
|
||||
want: nil,
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
},
|
||||
{
|
||||
name: "nil-writer",
|
||||
args: args{
|
||||
r: rw,
|
||||
w: nil,
|
||||
kms: kmsCache,
|
||||
},
|
||||
want: nil,
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
},
|
||||
{
|
||||
name: "nil-wrapper",
|
||||
args: args{
|
||||
r: rw,
|
||||
w: rw,
|
||||
kms: nil,
|
||||
},
|
||||
want: nil,
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
},
|
||||
{
|
||||
name: "all-nils",
|
||||
args: args{
|
||||
r: nil,
|
||||
w: nil,
|
||||
kms: nil,
|
||||
},
|
||||
want: nil,
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
got, err := NewRepository(ctx, tt.args.r, tt.args.w, tt.args.kms, tt.args.opts...)
|
||||
if tt.wantErrMatch != nil {
|
||||
require.Error(err)
|
||||
assert.Truef(errors.Match(tt.wantErrMatch, err), "want err code: %q got: %q", tt.wantErrMatch, err)
|
||||
assert.Nil(got)
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
require.NotNil(got)
|
||||
assert.Equal(tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/kms"
|
||||
"github.com/hashicorp/boundary/internal/libs/crypto"
|
||||
"github.com/hashicorp/boundary/internal/util"
|
||||
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
CtCertificateKeyField = "CtCertificateKey"
|
||||
CtPasswordField = "CtPassword"
|
||||
KeyIdField = "KeyId"
|
||||
)
|
||||
|
||||
func init() {
|
||||
kms.RegisterTableRewrapFn(clientCertificateTableName, clientCertificateRewrapFn)
|
||||
kms.RegisterTableRewrapFn(bindCredentialTableName, bindCredentialRewrapFn)
|
||||
}
|
||||
|
||||
// hmacField simply hmac's a field in a consistent manner for this pkg
|
||||
func hmacField(ctx context.Context, cipher wrapping.Wrapper, field []byte, publicId string) ([]byte, error) {
|
||||
const op = "ldap.hmacField"
|
||||
hm, err := crypto.HmacSha256(ctx, field, cipher, []byte(publicId), nil)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
return []byte(hm), nil
|
||||
}
|
||||
|
||||
// bindCredentialRewrapFn provides a kms.Rewrapfn for the BindCredential type
|
||||
func bindCredentialRewrapFn(ctx context.Context, dataKeyVersionId, scopeId string, reader db.Reader, writer db.Writer, kmsRepo kms.GetWrapperer) error {
|
||||
const op = "ldap.bindCredentialRewrapFn"
|
||||
if dataKeyVersionId == "" {
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "missing data key version id")
|
||||
}
|
||||
if scopeId == "" {
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "missing scope id")
|
||||
}
|
||||
if util.IsNil(reader) {
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "missing database reader")
|
||||
}
|
||||
if util.IsNil(writer) {
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "missing database writer")
|
||||
}
|
||||
if util.IsNil(kmsRepo) {
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "missing kms repository")
|
||||
}
|
||||
var creds []*BindCredential
|
||||
// This is the fastest query we can use without creating a new index on key_id.
|
||||
if err := reader.SearchWhere(ctx, &creds, "key_id=?", []any{dataKeyVersionId}, db.WithLimit(-1)); err != nil {
|
||||
return errors.Wrap(ctx, err, op, errors.WithMsg("failed to query sql for rows that need rewrapping"))
|
||||
}
|
||||
wrapper, err := kmsRepo.GetWrapper(ctx, scopeId, kms.KeyPurposeDatabase)
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op, errors.WithMsg("failed to fetch kms wrapper for rewrapping"))
|
||||
}
|
||||
for _, cc := range creds {
|
||||
if err := cc.decrypt(ctx, wrapper); err != nil {
|
||||
return errors.Wrap(ctx, err, op, errors.WithMsg("failed to decrypt bind credential"))
|
||||
}
|
||||
if err := cc.encrypt(ctx, wrapper); err != nil {
|
||||
return errors.Wrap(ctx, err, op, errors.WithMsg("failed to re-encrypt bind credential"))
|
||||
}
|
||||
if _, err := writer.Update(ctx, cc, []string{CtPasswordField, KeyIdField}, nil); err != nil {
|
||||
return errors.Wrap(ctx, err, op, errors.WithMsg("failed to update bind credential row with rewrapped fields"))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// clientCertificateRewrapFn provides a kms.Rewrapfn for the ClientCertificate type
|
||||
func clientCertificateRewrapFn(ctx context.Context, dataKeyVersionId, scopeId string, reader db.Reader, writer db.Writer, kmsRepo kms.GetWrapperer) error {
|
||||
const op = "ldap.clientCertificateRewrapFn"
|
||||
if dataKeyVersionId == "" {
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "missing data key version id")
|
||||
}
|
||||
if scopeId == "" {
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "missing scope id")
|
||||
}
|
||||
if util.IsNil(reader) {
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "missing database reader")
|
||||
}
|
||||
if util.IsNil(writer) {
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "missing database writer")
|
||||
}
|
||||
if kmsRepo == nil {
|
||||
return errors.New(ctx, errors.InvalidParameter, op, "missing kms repository")
|
||||
}
|
||||
var clientCerts []*ClientCertificate
|
||||
// This is the fastest query we can use without creating a new index on key_id.
|
||||
if err := reader.SearchWhere(ctx, &clientCerts, "key_id=?", []any{dataKeyVersionId}, db.WithLimit(-1)); err != nil {
|
||||
return errors.Wrap(ctx, err, op, errors.WithMsg("failed to query sql for rows that need rewrapping"))
|
||||
}
|
||||
wrapper, err := kmsRepo.GetWrapper(ctx, scopeId, kms.KeyPurposeDatabase)
|
||||
if err != nil {
|
||||
return errors.Wrap(ctx, err, op, errors.WithMsg("failed to fetch kms wrapper for rewrapping"))
|
||||
}
|
||||
for _, cc := range clientCerts {
|
||||
if err := cc.decrypt(ctx, wrapper); err != nil {
|
||||
return errors.Wrap(ctx, err, op, errors.WithMsg("failed to decrypt client certificate"))
|
||||
}
|
||||
if err := cc.encrypt(ctx, wrapper); err != nil {
|
||||
return errors.Wrap(ctx, err, op, errors.WithMsg("failed to re-encrypt client certificate"))
|
||||
}
|
||||
if _, err := writer.Update(ctx, cc, []string{CtCertificateKeyField, KeyIdField}, nil); err != nil {
|
||||
return errors.Wrap(ctx, err, op, errors.WithMsg("failed to update client certificate row with rewrapped fields"))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,616 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/iam"
|
||||
"github.com/hashicorp/boundary/internal/kms"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRewrap_bindCredentialRewrapFn(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCtx := context.Background()
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
testRootWrapper := db.TestWrapper(t)
|
||||
testKms := kms.TestKms(t, conn, testRootWrapper)
|
||||
rw := db.New(conn)
|
||||
org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, testRootWrapper))
|
||||
orgDBWrapper, err := testKms.GetWrapper(testCtx, org.PublicId, kms.KeyPurposeDatabase)
|
||||
require.NoError(t, err)
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
setup func() (string, string, *BindCredential, *AuthMethod)
|
||||
reader db.Reader
|
||||
writer db.Writer
|
||||
kmsRepo kms.GetWrapperer
|
||||
wantErr bool
|
||||
wantErrCode errors.Code
|
||||
wantErrContains string
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
ctx: testCtx,
|
||||
setup: func() (string, string, *BindCredential, *AuthMethod) {
|
||||
am := TestAuthMethod(t, conn, orgDBWrapper, org.PublicId, []string{"ldaps://alice.com"}, WithBindCredential(testCtx, "bind-dn", "bind-password"))
|
||||
bc := allocBindCredential()
|
||||
err = rw.LookupWhere(testCtx, &bc, "ldap_method_id = ?", []any{am.PublicId})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, bc.decrypt(testCtx, orgDBWrapper))
|
||||
assert.NotEmpty(t, bc.GetKeyId())
|
||||
assert.NotEmpty(t, bc.GetPassword())
|
||||
assert.NotEmpty(t, bc.GetPasswordHmac())
|
||||
assert.NotEmpty(t, bc.GetCtPassword())
|
||||
return bc.GetKeyId(), am.GetScopeId(), bc, am
|
||||
},
|
||||
reader: rw,
|
||||
writer: rw,
|
||||
kmsRepo: testKms,
|
||||
},
|
||||
{
|
||||
name: "missing-key-id",
|
||||
ctx: testCtx,
|
||||
setup: func() (string, string, *BindCredential, *AuthMethod) {
|
||||
am := TestAuthMethod(t, conn, orgDBWrapper, org.PublicId, []string{"ldaps://alice.com"}, WithBindCredential(testCtx, "bind-dn", "bind-password"))
|
||||
bc := allocBindCredential()
|
||||
err = rw.LookupWhere(testCtx, &bc, "ldap_method_id = ?", []any{am.PublicId})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, bc.decrypt(testCtx, orgDBWrapper))
|
||||
assert.NotEmpty(t, bc.GetKeyId())
|
||||
assert.NotEmpty(t, bc.GetPassword())
|
||||
assert.NotEmpty(t, bc.GetPasswordHmac())
|
||||
assert.NotEmpty(t, bc.GetCtPassword())
|
||||
return "", am.GetScopeId(), bc, am
|
||||
},
|
||||
reader: rw,
|
||||
writer: rw,
|
||||
kmsRepo: testKms,
|
||||
wantErr: true,
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
wantErrContains: "missing data key version id",
|
||||
},
|
||||
{
|
||||
name: "missing-scope-id",
|
||||
ctx: testCtx,
|
||||
setup: func() (string, string, *BindCredential, *AuthMethod) {
|
||||
am := TestAuthMethod(t, conn, orgDBWrapper, org.PublicId, []string{"ldaps://alice.com"}, WithBindCredential(testCtx, "bind-dn", "bind-password"))
|
||||
bc := allocBindCredential()
|
||||
err = rw.LookupWhere(testCtx, &bc, "ldap_method_id = ?", []any{am.PublicId})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, bc.decrypt(testCtx, orgDBWrapper))
|
||||
assert.NotEmpty(t, bc.GetKeyId())
|
||||
assert.NotEmpty(t, bc.GetPassword())
|
||||
assert.NotEmpty(t, bc.GetPasswordHmac())
|
||||
assert.NotEmpty(t, bc.GetCtPassword())
|
||||
return bc.GetKeyId(), "", bc, am
|
||||
},
|
||||
reader: rw,
|
||||
writer: rw,
|
||||
kmsRepo: testKms,
|
||||
wantErr: true,
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
wantErrContains: "missing scope id",
|
||||
},
|
||||
{
|
||||
name: "missing-reader",
|
||||
ctx: testCtx,
|
||||
setup: func() (string, string, *BindCredential, *AuthMethod) {
|
||||
am := TestAuthMethod(t, conn, orgDBWrapper, org.PublicId, []string{"ldaps://alice.com"}, WithBindCredential(testCtx, "bind-dn", "bind-password"))
|
||||
bc := allocBindCredential()
|
||||
err = rw.LookupWhere(testCtx, &bc, "ldap_method_id = ?", []any{am.PublicId})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, bc.decrypt(testCtx, orgDBWrapper))
|
||||
assert.NotEmpty(t, bc.GetKeyId())
|
||||
assert.NotEmpty(t, bc.GetPassword())
|
||||
assert.NotEmpty(t, bc.GetPasswordHmac())
|
||||
assert.NotEmpty(t, bc.GetCtPassword())
|
||||
return bc.GetKeyId(), am.GetScopeId(), bc, am
|
||||
},
|
||||
reader: nil,
|
||||
writer: rw,
|
||||
kmsRepo: testKms,
|
||||
wantErr: true,
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
wantErrContains: "missing database reader",
|
||||
},
|
||||
{
|
||||
name: "missing-writer",
|
||||
ctx: testCtx,
|
||||
setup: func() (string, string, *BindCredential, *AuthMethod) {
|
||||
am := TestAuthMethod(t, conn, orgDBWrapper, org.PublicId, []string{"ldaps://alice.com"}, WithBindCredential(testCtx, "bind-dn", "bind-password"))
|
||||
bc := allocBindCredential()
|
||||
err = rw.LookupWhere(testCtx, &bc, "ldap_method_id = ?", []any{am.PublicId})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, bc.decrypt(testCtx, orgDBWrapper))
|
||||
assert.NotEmpty(t, bc.GetKeyId())
|
||||
assert.NotEmpty(t, bc.GetPassword())
|
||||
assert.NotEmpty(t, bc.GetPasswordHmac())
|
||||
assert.NotEmpty(t, bc.GetCtPassword())
|
||||
return bc.GetKeyId(), am.GetScopeId(), bc, am
|
||||
},
|
||||
reader: rw,
|
||||
writer: nil,
|
||||
kmsRepo: testKms,
|
||||
wantErr: true,
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
wantErrContains: "missing database writer",
|
||||
},
|
||||
{
|
||||
name: "missing-kms",
|
||||
ctx: testCtx,
|
||||
setup: func() (string, string, *BindCredential, *AuthMethod) {
|
||||
am := TestAuthMethod(t, conn, orgDBWrapper, org.PublicId, []string{"ldaps://alice.com"}, WithBindCredential(testCtx, "bind-dn", "bind-password"))
|
||||
bc := allocBindCredential()
|
||||
err = rw.LookupWhere(testCtx, &bc, "ldap_method_id = ?", []any{am.PublicId})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, bc.decrypt(testCtx, orgDBWrapper))
|
||||
assert.NotEmpty(t, bc.GetKeyId())
|
||||
assert.NotEmpty(t, bc.GetPassword())
|
||||
assert.NotEmpty(t, bc.GetPasswordHmac())
|
||||
assert.NotEmpty(t, bc.GetCtPassword())
|
||||
return bc.GetKeyId(), am.GetScopeId(), bc, am
|
||||
},
|
||||
reader: rw,
|
||||
writer: rw,
|
||||
kmsRepo: nil,
|
||||
wantErr: true,
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
wantErrContains: "missing kms repository",
|
||||
},
|
||||
{
|
||||
name: "GetWrapper-err",
|
||||
ctx: testCtx,
|
||||
setup: func() (string, string, *BindCredential, *AuthMethod) {
|
||||
am := TestAuthMethod(t, conn, orgDBWrapper, org.PublicId, []string{"ldaps://alice.com"}, WithBindCredential(testCtx, "bind-dn", "bind-password"))
|
||||
bc := allocBindCredential()
|
||||
err = rw.LookupWhere(testCtx, &bc, "ldap_method_id = ?", []any{am.PublicId})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, bc.decrypt(testCtx, orgDBWrapper))
|
||||
assert.NotEmpty(t, bc.GetKeyId())
|
||||
assert.NotEmpty(t, bc.GetPassword())
|
||||
assert.NotEmpty(t, bc.GetPasswordHmac())
|
||||
assert.NotEmpty(t, bc.GetCtPassword())
|
||||
return bc.GetKeyId(), am.GetScopeId(), bc, am
|
||||
},
|
||||
reader: rw,
|
||||
writer: rw,
|
||||
kmsRepo: &kms.MockGetWrapperer{
|
||||
GetErr: errors.New(testCtx, errors.Internal, "test", "GetWrapper error"),
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrCode: errors.Internal,
|
||||
wantErrContains: "GetWrapper error",
|
||||
},
|
||||
{
|
||||
name: "encrypt-err",
|
||||
ctx: testCtx,
|
||||
setup: func() (string, string, *BindCredential, *AuthMethod) {
|
||||
am := TestAuthMethod(t, conn, orgDBWrapper, org.PublicId, []string{"ldaps://alice.com"}, WithBindCredential(testCtx, "bind-dn", "bind-password"))
|
||||
bc := allocBindCredential()
|
||||
err = rw.LookupWhere(testCtx, &bc, "ldap_method_id = ?", []any{am.PublicId})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, bc.decrypt(testCtx, orgDBWrapper))
|
||||
assert.NotEmpty(t, bc.GetKeyId())
|
||||
assert.NotEmpty(t, bc.GetPassword())
|
||||
assert.NotEmpty(t, bc.GetPasswordHmac())
|
||||
assert.NotEmpty(t, bc.GetCtPassword())
|
||||
return bc.GetKeyId(), am.GetScopeId(), bc, am
|
||||
},
|
||||
reader: rw,
|
||||
writer: rw,
|
||||
kmsRepo: &kms.MockGetWrapperer{
|
||||
ReturnWrapper: &kms.MockWrapper{
|
||||
EncryptErr: errors.New(testCtx, errors.Encrypt, "test", "encrypt error"),
|
||||
Wrapper: orgDBWrapper,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrCode: errors.Encrypt,
|
||||
wantErrContains: "encrypt error",
|
||||
},
|
||||
{
|
||||
name: "decrypt-err",
|
||||
ctx: testCtx,
|
||||
setup: func() (string, string, *BindCredential, *AuthMethod) {
|
||||
am := TestAuthMethod(t, conn, orgDBWrapper, org.PublicId, []string{"ldaps://alice.com"}, WithBindCredential(testCtx, "bind-dn", "bind-password"))
|
||||
bc := allocBindCredential()
|
||||
err = rw.LookupWhere(testCtx, &bc, "ldap_method_id = ?", []any{am.PublicId})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, bc.decrypt(testCtx, orgDBWrapper))
|
||||
assert.NotEmpty(t, bc.GetKeyId())
|
||||
assert.NotEmpty(t, bc.GetPassword())
|
||||
assert.NotEmpty(t, bc.GetPasswordHmac())
|
||||
assert.NotEmpty(t, bc.GetCtPassword())
|
||||
return bc.GetKeyId(), am.GetScopeId(), bc, am
|
||||
},
|
||||
reader: rw,
|
||||
writer: rw,
|
||||
kmsRepo: &kms.MockGetWrapperer{
|
||||
ReturnWrapper: &kms.MockWrapper{
|
||||
DecryptErr: errors.New(testCtx, errors.Encrypt, "test", "decrypt error"),
|
||||
Wrapper: orgDBWrapper,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrCode: errors.Decrypt,
|
||||
wantErrContains: "decrypt error",
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
|
||||
keyId, scopeId, bc, am := tc.setup()
|
||||
|
||||
// now we can rotate and rewrap
|
||||
assert.NoError(testKms.RotateKeys(testCtx, org.Scope.GetPublicId()))
|
||||
|
||||
// let's do this rewrapping!
|
||||
err := bindCredentialRewrapFn(tc.ctx, keyId, scopeId, tc.reader, tc.writer, tc.kmsRepo)
|
||||
if tc.wantErr {
|
||||
require.Error(err)
|
||||
if tc.wantErrCode != errors.Unknown {
|
||||
assert.True(errors.Match(errors.T(tc.wantErrCode), err))
|
||||
}
|
||||
if tc.wantErrContains != "" {
|
||||
assert.Contains(err.Error(), tc.wantErrContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
|
||||
// fetch the new key version
|
||||
kmsWrapper, err := testKms.GetWrapper(testCtx, org.Scope.GetPublicId(), kms.KeyPurposeDatabase)
|
||||
assert.NoError(err)
|
||||
newKeyVersion, err := kmsWrapper.KeyId(testCtx)
|
||||
assert.NoError(err)
|
||||
|
||||
// get the latest bind cred
|
||||
latestBindCred := allocBindCredential()
|
||||
err = rw.LookupWhere(testCtx, &latestBindCred, "ldap_method_id = ?", []any{am.GetPublicId()})
|
||||
require.NoError(err)
|
||||
require.NoError(latestBindCred.decrypt(testCtx, kmsWrapper))
|
||||
|
||||
// make sure the password and its hmac are correct and that it uses
|
||||
// the newest key version id
|
||||
assert.NotEmpty(latestBindCred.KeyId)
|
||||
assert.Equal(newKeyVersion, latestBindCred.KeyId)
|
||||
assert.Equal([]byte("bind-password"), latestBindCred.Password)
|
||||
assert.NotEqual(bc.CtPassword, latestBindCred.CtPassword)
|
||||
assert.NotEmpty(latestBindCred.PasswordHmac)
|
||||
assert.Equal(bc.PasswordHmac, latestBindCred.PasswordHmac)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewrap_clientCertificateRewrapFn(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCtx := context.Background()
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
testRootWrapper := db.TestWrapper(t)
|
||||
testKms := kms.TestKms(t, conn, testRootWrapper)
|
||||
rw := db.New(conn)
|
||||
org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, testRootWrapper))
|
||||
orgDBWrapper, err := testKms.GetWrapper(testCtx, org.GetPublicId(), kms.KeyPurposeDatabase)
|
||||
require.NoError(t, err)
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
setup func() (string, string, *ClientCertificate, *AuthMethod)
|
||||
reader db.Reader
|
||||
writer db.Writer
|
||||
kmsRepo kms.GetWrapperer
|
||||
wantErr bool
|
||||
wantErrCode errors.Code
|
||||
wantErrContains string
|
||||
}{
|
||||
{
|
||||
name: "success",
|
||||
ctx: testCtx,
|
||||
setup: func() (string, string, *ClientCertificate, *AuthMethod) {
|
||||
_, privKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
require.NoError(t, err)
|
||||
cert, _ := TestGenerateCA(t, "localhost")
|
||||
derPrivKey, err := x509.MarshalPKCS8PrivateKey(privKey)
|
||||
require.NoError(t, err)
|
||||
am := TestAuthMethod(t, conn, orgDBWrapper, org.PublicId, []string{"ldaps://alice.com"}, WithClientCertificate(testCtx, derPrivKey, cert))
|
||||
cc := allocClientCertificate()
|
||||
err = rw.LookupWhere(testCtx, &cc, "ldap_method_id = ?", []any{am.GetPublicId()})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, cc.decrypt(testCtx, orgDBWrapper))
|
||||
assert.NotEmpty(t, cc.GetKeyId())
|
||||
assert.NotEmpty(t, cc.GetCertificateKey())
|
||||
assert.NotEmpty(t, cc.GetCertificateKeyHmac())
|
||||
assert.NotEmpty(t, cc.GetCtCertificateKey())
|
||||
return cc.GetKeyId(), am.GetScopeId(), cc, am
|
||||
},
|
||||
reader: rw,
|
||||
writer: rw,
|
||||
kmsRepo: testKms,
|
||||
},
|
||||
{
|
||||
name: "missing-key-id",
|
||||
ctx: testCtx,
|
||||
setup: func() (string, string, *ClientCertificate, *AuthMethod) {
|
||||
_, privKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
require.NoError(t, err)
|
||||
cert, _ := TestGenerateCA(t, "localhost")
|
||||
derPrivKey, err := x509.MarshalPKCS8PrivateKey(privKey)
|
||||
require.NoError(t, err)
|
||||
am := TestAuthMethod(t, conn, orgDBWrapper, org.PublicId, []string{"ldaps://alice.com"}, WithClientCertificate(testCtx, derPrivKey, cert))
|
||||
cc := allocClientCertificate()
|
||||
err = rw.LookupWhere(testCtx, &cc, "ldap_method_id = ?", []any{am.GetPublicId()})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, cc.decrypt(testCtx, orgDBWrapper))
|
||||
assert.NotEmpty(t, cc.GetKeyId())
|
||||
assert.NotEmpty(t, cc.GetCertificateKey())
|
||||
assert.NotEmpty(t, cc.GetCertificateKeyHmac())
|
||||
assert.NotEmpty(t, cc.GetCtCertificateKey())
|
||||
return "", am.GetScopeId(), cc, am
|
||||
},
|
||||
reader: rw,
|
||||
writer: rw,
|
||||
kmsRepo: testKms,
|
||||
wantErr: true,
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
wantErrContains: "missing data key version id",
|
||||
},
|
||||
{
|
||||
name: "missing-scope-id",
|
||||
ctx: testCtx,
|
||||
setup: func() (string, string, *ClientCertificate, *AuthMethod) {
|
||||
_, privKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
require.NoError(t, err)
|
||||
cert, _ := TestGenerateCA(t, "localhost")
|
||||
derPrivKey, err := x509.MarshalPKCS8PrivateKey(privKey)
|
||||
require.NoError(t, err)
|
||||
am := TestAuthMethod(t, conn, orgDBWrapper, org.PublicId, []string{"ldaps://alice.com"}, WithClientCertificate(testCtx, derPrivKey, cert))
|
||||
cc := allocClientCertificate()
|
||||
err = rw.LookupWhere(testCtx, &cc, "ldap_method_id = ?", []any{am.GetPublicId()})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, cc.decrypt(testCtx, orgDBWrapper))
|
||||
assert.NotEmpty(t, cc.GetKeyId())
|
||||
assert.NotEmpty(t, cc.GetCertificateKey())
|
||||
assert.NotEmpty(t, cc.GetCertificateKeyHmac())
|
||||
assert.NotEmpty(t, cc.GetCtCertificateKey())
|
||||
return cc.GetKeyId(), "", cc, am
|
||||
},
|
||||
reader: rw,
|
||||
writer: rw,
|
||||
kmsRepo: testKms,
|
||||
wantErr: true,
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
wantErrContains: "missing scope id",
|
||||
},
|
||||
{
|
||||
name: "missing-reader",
|
||||
ctx: testCtx,
|
||||
setup: func() (string, string, *ClientCertificate, *AuthMethod) {
|
||||
_, privKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
require.NoError(t, err)
|
||||
cert, _ := TestGenerateCA(t, "localhost")
|
||||
derPrivKey, err := x509.MarshalPKCS8PrivateKey(privKey)
|
||||
require.NoError(t, err)
|
||||
am := TestAuthMethod(t, conn, orgDBWrapper, org.PublicId, []string{"ldaps://alice.com"}, WithClientCertificate(testCtx, derPrivKey, cert))
|
||||
cc := allocClientCertificate()
|
||||
err = rw.LookupWhere(testCtx, &cc, "ldap_method_id = ?", []any{am.GetPublicId()})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, cc.decrypt(testCtx, orgDBWrapper))
|
||||
assert.NotEmpty(t, cc.GetKeyId())
|
||||
assert.NotEmpty(t, cc.GetCertificateKey())
|
||||
assert.NotEmpty(t, cc.GetCertificateKeyHmac())
|
||||
assert.NotEmpty(t, cc.GetCtCertificateKey())
|
||||
return cc.GetKeyId(), am.GetScopeId(), cc, am
|
||||
},
|
||||
reader: nil,
|
||||
writer: rw,
|
||||
kmsRepo: testKms,
|
||||
wantErr: true,
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
wantErrContains: "missing database reader",
|
||||
},
|
||||
{
|
||||
name: "missing-writer",
|
||||
ctx: testCtx,
|
||||
setup: func() (string, string, *ClientCertificate, *AuthMethod) {
|
||||
_, privKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
require.NoError(t, err)
|
||||
cert, _ := TestGenerateCA(t, "localhost")
|
||||
derPrivKey, err := x509.MarshalPKCS8PrivateKey(privKey)
|
||||
require.NoError(t, err)
|
||||
am := TestAuthMethod(t, conn, orgDBWrapper, org.PublicId, []string{"ldaps://alice.com"}, WithClientCertificate(testCtx, derPrivKey, cert))
|
||||
cc := allocClientCertificate()
|
||||
err = rw.LookupWhere(testCtx, &cc, "ldap_method_id = ?", []any{am.GetPublicId()})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, cc.decrypt(testCtx, orgDBWrapper))
|
||||
assert.NotEmpty(t, cc.GetKeyId())
|
||||
assert.NotEmpty(t, cc.GetCertificateKey())
|
||||
assert.NotEmpty(t, cc.GetCertificateKeyHmac())
|
||||
assert.NotEmpty(t, cc.GetCtCertificateKey())
|
||||
return cc.GetKeyId(), am.GetScopeId(), cc, am
|
||||
},
|
||||
reader: rw,
|
||||
writer: nil,
|
||||
kmsRepo: testKms,
|
||||
wantErr: true,
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
wantErrContains: "missing database writer",
|
||||
},
|
||||
{
|
||||
name: "missing-kms",
|
||||
ctx: testCtx,
|
||||
setup: func() (string, string, *ClientCertificate, *AuthMethod) {
|
||||
_, privKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
require.NoError(t, err)
|
||||
cert, _ := TestGenerateCA(t, "localhost")
|
||||
derPrivKey, err := x509.MarshalPKCS8PrivateKey(privKey)
|
||||
require.NoError(t, err)
|
||||
am := TestAuthMethod(t, conn, orgDBWrapper, org.PublicId, []string{"ldaps://alice.com"}, WithClientCertificate(testCtx, derPrivKey, cert))
|
||||
cc := allocClientCertificate()
|
||||
err = rw.LookupWhere(testCtx, &cc, "ldap_method_id = ?", []any{am.GetPublicId()})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, cc.decrypt(testCtx, orgDBWrapper))
|
||||
assert.NotEmpty(t, cc.GetKeyId())
|
||||
assert.NotEmpty(t, cc.GetCertificateKey())
|
||||
assert.NotEmpty(t, cc.GetCertificateKeyHmac())
|
||||
assert.NotEmpty(t, cc.GetCtCertificateKey())
|
||||
return cc.GetKeyId(), am.GetScopeId(), cc, am
|
||||
},
|
||||
reader: rw,
|
||||
writer: rw,
|
||||
kmsRepo: nil,
|
||||
wantErr: true,
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
wantErrContains: "missing kms repository",
|
||||
},
|
||||
{
|
||||
name: "GetWrapper-err",
|
||||
ctx: testCtx,
|
||||
setup: func() (string, string, *ClientCertificate, *AuthMethod) {
|
||||
_, privKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
require.NoError(t, err)
|
||||
cert, _ := TestGenerateCA(t, "localhost")
|
||||
derPrivKey, err := x509.MarshalPKCS8PrivateKey(privKey)
|
||||
require.NoError(t, err)
|
||||
am := TestAuthMethod(t, conn, orgDBWrapper, org.PublicId, []string{"ldaps://alice.com"}, WithClientCertificate(testCtx, derPrivKey, cert))
|
||||
cc := allocClientCertificate()
|
||||
err = rw.LookupWhere(testCtx, &cc, "ldap_method_id = ?", []any{am.GetPublicId()})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, cc.decrypt(testCtx, orgDBWrapper))
|
||||
assert.NotEmpty(t, cc.GetKeyId())
|
||||
assert.NotEmpty(t, cc.GetCertificateKey())
|
||||
assert.NotEmpty(t, cc.GetCertificateKeyHmac())
|
||||
assert.NotEmpty(t, cc.GetCtCertificateKey())
|
||||
return cc.GetKeyId(), am.GetScopeId(), cc, am
|
||||
},
|
||||
reader: rw,
|
||||
writer: rw,
|
||||
kmsRepo: &kms.MockGetWrapperer{
|
||||
GetErr: errors.New(testCtx, errors.Internal, "test", "GetWrapper error"),
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrCode: errors.Internal,
|
||||
wantErrContains: "GetWrapper error",
|
||||
},
|
||||
{
|
||||
name: "encrypt-err",
|
||||
ctx: testCtx,
|
||||
setup: func() (string, string, *ClientCertificate, *AuthMethod) {
|
||||
_, privKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
require.NoError(t, err)
|
||||
cert, _ := TestGenerateCA(t, "localhost")
|
||||
derPrivKey, err := x509.MarshalPKCS8PrivateKey(privKey)
|
||||
require.NoError(t, err)
|
||||
am := TestAuthMethod(t, conn, orgDBWrapper, org.PublicId, []string{"ldaps://alice.com"}, WithClientCertificate(testCtx, derPrivKey, cert))
|
||||
cc := allocClientCertificate()
|
||||
err = rw.LookupWhere(testCtx, &cc, "ldap_method_id = ?", []any{am.GetPublicId()})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, cc.decrypt(testCtx, orgDBWrapper))
|
||||
assert.NotEmpty(t, cc.GetKeyId())
|
||||
assert.NotEmpty(t, cc.GetCertificateKey())
|
||||
assert.NotEmpty(t, cc.GetCertificateKeyHmac())
|
||||
assert.NotEmpty(t, cc.GetCtCertificateKey())
|
||||
return cc.GetKeyId(), am.GetScopeId(), cc, am
|
||||
},
|
||||
reader: rw,
|
||||
writer: rw,
|
||||
kmsRepo: &kms.MockGetWrapperer{
|
||||
ReturnWrapper: &kms.MockWrapper{
|
||||
EncryptErr: errors.New(testCtx, errors.Encrypt, "test", "encrypt error"),
|
||||
Wrapper: orgDBWrapper,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrCode: errors.Encrypt,
|
||||
wantErrContains: "encrypt error",
|
||||
},
|
||||
{
|
||||
name: "decrypt-err",
|
||||
ctx: testCtx,
|
||||
setup: func() (string, string, *ClientCertificate, *AuthMethod) {
|
||||
_, privKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
require.NoError(t, err)
|
||||
cert, _ := TestGenerateCA(t, "localhost")
|
||||
derPrivKey, err := x509.MarshalPKCS8PrivateKey(privKey)
|
||||
require.NoError(t, err)
|
||||
am := TestAuthMethod(t, conn, orgDBWrapper, org.PublicId, []string{"ldaps://alice.com"}, WithClientCertificate(testCtx, derPrivKey, cert))
|
||||
cc := allocClientCertificate()
|
||||
err = rw.LookupWhere(testCtx, &cc, "ldap_method_id = ?", []any{am.GetPublicId()})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, cc.decrypt(testCtx, orgDBWrapper))
|
||||
assert.NotEmpty(t, cc.GetKeyId())
|
||||
assert.NotEmpty(t, cc.GetCertificateKey())
|
||||
assert.NotEmpty(t, cc.GetCertificateKeyHmac())
|
||||
assert.NotEmpty(t, cc.GetCtCertificateKey())
|
||||
return cc.GetKeyId(), am.GetScopeId(), cc, am
|
||||
},
|
||||
reader: rw,
|
||||
writer: rw,
|
||||
kmsRepo: &kms.MockGetWrapperer{
|
||||
ReturnWrapper: &kms.MockWrapper{
|
||||
DecryptErr: errors.New(testCtx, errors.Encrypt, "test", "decrypt error"),
|
||||
Wrapper: orgDBWrapper,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
wantErrCode: errors.Decrypt,
|
||||
wantErrContains: "decrypt error",
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
|
||||
keyId, scopeId, cc, am := tc.setup()
|
||||
|
||||
// now we can rotate and rewrap
|
||||
assert.NoError(testKms.RotateKeys(testCtx, org.Scope.GetPublicId()))
|
||||
|
||||
// let's do this rewrapping!
|
||||
err := clientCertificateRewrapFn(tc.ctx, keyId, scopeId, tc.reader, tc.writer, tc.kmsRepo)
|
||||
if tc.wantErr {
|
||||
require.Error(err)
|
||||
if tc.wantErrCode != errors.Unknown {
|
||||
assert.True(errors.Match(errors.T(tc.wantErrCode), err))
|
||||
}
|
||||
if tc.wantErrContains != "" {
|
||||
assert.Contains(err.Error(), tc.wantErrContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
|
||||
// fetch the new key version
|
||||
kmsWrapper, err := testKms.GetWrapper(testCtx, org.Scope.GetPublicId(), kms.KeyPurposeDatabase)
|
||||
assert.NoError(err)
|
||||
newKeyVersion, err := kmsWrapper.KeyId(testCtx)
|
||||
assert.NoError(err)
|
||||
|
||||
// get the latest bind cred
|
||||
latest := allocClientCertificate()
|
||||
err = rw.LookupWhere(testCtx, &latest, "ldap_method_id = ?", []any{am.GetPublicId()})
|
||||
require.NoError(err)
|
||||
require.NoError(latest.decrypt(testCtx, kmsWrapper))
|
||||
|
||||
// make sure the password and its hmac are correct and that it uses
|
||||
// the newest key version id
|
||||
assert.NotEmpty(latest.KeyId)
|
||||
assert.Equal(newKeyVersion, latest.KeyId)
|
||||
assert.Equal(cc.GetCertificateKey(), latest.CertificateKey)
|
||||
assert.NotEqual(cc.GetCtCertificateKey(), latest.GetCtCertificateKey())
|
||||
assert.NotEmpty(latest.GetCertificateKeyHmac())
|
||||
assert.Equal(cc.GetCertificateKeyHmac(), latest.CertificateKeyHmac)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/authtoken"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/iam"
|
||||
)
|
||||
|
||||
type (
|
||||
// AuthenticatorFactory is used by "service functions" to create a new
|
||||
// ldap.Authenticator (typically an ldap.Repository)
|
||||
AuthenticatorFactory func() (Authenticator, error)
|
||||
|
||||
// LookupUserFactory is used by "service functions" to create a new
|
||||
// LookupUser (typically an iam repo)
|
||||
LookupUserFactory func() (LookupUser, error)
|
||||
|
||||
// AuthTokenCreatorFactory is used by "service functions" to create a new
|
||||
// AuthTokenCreator (typically an auth token repo)
|
||||
AuthTokenCreatorFactory func() (AuthTokenCreator, error)
|
||||
)
|
||||
|
||||
// Authenticate is an ldap domain service function for handling an LDAP
|
||||
// authentication flow. On success, it returns an auth token.
|
||||
//
|
||||
// The service operation includes:
|
||||
// - Authenticate the user against the auth method's configured ldap server.
|
||||
// - Use iam.(Repository).LookupUserWithLogin(...) look up the iam.User matching the Account.
|
||||
// - Use the authtoken.(Repository).CreateAuthToken(...) to create a pending auth token for the authenticated user.
|
||||
func Authenticate(
|
||||
ctx context.Context,
|
||||
authenticatorFn AuthenticatorFactory,
|
||||
lookupUserFn LookupUserFactory,
|
||||
tokenCreatorFn AuthTokenCreatorFactory,
|
||||
authMethodId, loginName, password string,
|
||||
) (*authtoken.AuthToken, error) {
|
||||
const op = "ldap.Authenticate"
|
||||
switch {
|
||||
case authenticatorFn == nil:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing authenticator factory")
|
||||
case lookupUserFn == nil:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing lookup user factory")
|
||||
case tokenCreatorFn == nil:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth token creator factory")
|
||||
case authMethodId == "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth method id")
|
||||
case loginName == "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing login name")
|
||||
case password == "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing password")
|
||||
}
|
||||
|
||||
r, err := authenticatorFn()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
|
||||
acct, err := r.Authenticate(ctx, authMethodId, loginName, password)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
|
||||
l, err := lookupUserFn()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
user, err := l.LookupUserWithLogin(ctx, acct.PublicId)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
|
||||
at, err := tokenCreatorFn()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
token, err := at.CreateAuthToken(ctx, user, acct.PublicId)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
type Authenticator interface {
|
||||
Authenticate(ctx context.Context, authMethodId, loginName, password string) (*Account, error)
|
||||
}
|
||||
|
||||
type LookupUser interface {
|
||||
LookupUserWithLogin(ctx context.Context, accountId string, opt ...iam.Option) (*iam.User, error)
|
||||
}
|
||||
|
||||
type AuthTokenCreator interface {
|
||||
CreateAuthToken(ctx context.Context, withIamUser *iam.User, withAuthAccountId string, opt ...authtoken.Option) (*authtoken.AuthToken, error)
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/authtoken"
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/boundary/internal/iam"
|
||||
"github.com/hashicorp/boundary/internal/kms"
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/jimlambrt/gldap"
|
||||
"github.com/jimlambrt/gldap/testdirectory"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAuthenticate(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCtx := context.Background()
|
||||
testConn, _ := db.TestSetup(t, "postgres")
|
||||
testRw := db.New(testConn)
|
||||
rootWrapper := db.TestWrapper(t)
|
||||
testKms := kms.TestKms(t, testConn, rootWrapper)
|
||||
|
||||
// some standard factories for unit tests
|
||||
authenticatorFn := func() (Authenticator, error) {
|
||||
return NewRepository(testCtx, testRw, testRw, testKms)
|
||||
}
|
||||
lookupUserWithFn := func() (LookupUser, error) {
|
||||
return iam.NewRepository(testRw, testRw, testKms)
|
||||
}
|
||||
tokenCreatorFn := func() (AuthTokenCreator, error) {
|
||||
return authtoken.NewRepository(testRw, testRw, testKms)
|
||||
}
|
||||
iamRepo := iam.TestRepo(t, testConn, rootWrapper)
|
||||
org, _ := iam.TestScopes(t, iamRepo)
|
||||
orgDbWrapper, err := testKms.GetWrapper(testCtx, org.PublicId, kms.KeyPurposeDatabase)
|
||||
require.NoError(t, err)
|
||||
|
||||
logger := hclog.New(&hclog.LoggerOptions{
|
||||
Name: "test-logger",
|
||||
Level: hclog.Error,
|
||||
})
|
||||
td := testdirectory.Start(t,
|
||||
testdirectory.WithDefaults(t, &testdirectory.Defaults{AllowAnonymousBind: true}),
|
||||
testdirectory.WithLogger(t, logger),
|
||||
)
|
||||
tdCerts, err := ParseCertificates(testCtx, td.Cert())
|
||||
require.NoError(t, err)
|
||||
|
||||
groups := []*gldap.Entry{
|
||||
testdirectory.NewGroup(t, "admin", []string{"alice"}),
|
||||
}
|
||||
users := testdirectory.NewUsers(t, []string{"alice", "bob"}, testdirectory.WithMembersOf(t, "admin"))
|
||||
td.SetUsers(users...)
|
||||
td.SetGroups(groups...)
|
||||
|
||||
testPrimaryAuthMethod := TestAuthMethod(t, testConn, orgDbWrapper, org.PublicId,
|
||||
[]string{fmt.Sprintf("ldaps://127.0.0.1:%d", td.Port())},
|
||||
WithCertificates(testCtx, tdCerts...),
|
||||
WithDiscoverDn(testCtx),
|
||||
WithEnableGroups(testCtx),
|
||||
WithUserDn(testCtx, testdirectory.DefaultUserDN),
|
||||
WithGroupDn(testCtx, testdirectory.DefaultGroupDN),
|
||||
)
|
||||
iam.TestSetPrimaryAuthMethod(t, iamRepo, org, testPrimaryAuthMethod.PublicId)
|
||||
|
||||
testNotPrimaryAuthMethod := TestAuthMethod(t, testConn, orgDbWrapper, org.PublicId,
|
||||
[]string{fmt.Sprintf("ldaps://127.0.0.1:%d", td.Port())},
|
||||
WithCertificates(testCtx, tdCerts...),
|
||||
WithDiscoverDn(testCtx),
|
||||
WithEnableGroups(testCtx),
|
||||
WithUserDn(testCtx, testdirectory.DefaultUserDN),
|
||||
WithGroupDn(testCtx, testdirectory.DefaultGroupDN),
|
||||
)
|
||||
|
||||
const (
|
||||
testLoginName = "alice"
|
||||
testPassword = "password"
|
||||
testLoginName2 = "bob"
|
||||
)
|
||||
|
||||
testExistingAcct := TestAccount(t, testConn, testNotPrimaryAuthMethod, testLoginName2)
|
||||
iam.TestUser(t, iamRepo, org.PublicId, iam.WithAccountIds(testExistingAcct.PublicId))
|
||||
|
||||
// order is important in these tests, so we're using a slice
|
||||
tests := []struct {
|
||||
order int
|
||||
name string
|
||||
ctx context.Context
|
||||
authenticatorFn AuthenticatorFactory
|
||||
lookupUserWithLoginFn LookupUserFactory
|
||||
tokenCreatorFn AuthTokenCreatorFactory
|
||||
authMethodId string
|
||||
loginName string
|
||||
password string
|
||||
wantErrMatch *errors.Template
|
||||
wantErrContains string
|
||||
}{
|
||||
{
|
||||
order: 0,
|
||||
name: "success-with-primary-auth-method-auto-create",
|
||||
ctx: testCtx,
|
||||
authenticatorFn: authenticatorFn,
|
||||
lookupUserWithLoginFn: lookupUserWithFn,
|
||||
tokenCreatorFn: tokenCreatorFn,
|
||||
authMethodId: testPrimaryAuthMethod.PublicId,
|
||||
loginName: testLoginName,
|
||||
password: testPassword,
|
||||
},
|
||||
{
|
||||
order: 1,
|
||||
name: "success-with-primary-auth-method-existing-account",
|
||||
ctx: testCtx,
|
||||
authenticatorFn: authenticatorFn,
|
||||
lookupUserWithLoginFn: lookupUserWithFn,
|
||||
tokenCreatorFn: tokenCreatorFn,
|
||||
authMethodId: testPrimaryAuthMethod.PublicId,
|
||||
loginName: testLoginName,
|
||||
password: testPassword,
|
||||
},
|
||||
{
|
||||
order: 2,
|
||||
name: "success-with-non-primary-auth-method",
|
||||
ctx: testCtx,
|
||||
authenticatorFn: authenticatorFn,
|
||||
lookupUserWithLoginFn: lookupUserWithFn,
|
||||
tokenCreatorFn: tokenCreatorFn,
|
||||
authMethodId: testNotPrimaryAuthMethod.PublicId,
|
||||
loginName: testLoginName2,
|
||||
password: testPassword,
|
||||
},
|
||||
{
|
||||
order: 3,
|
||||
name: "err-not-primary-refused-to-create-user",
|
||||
ctx: testCtx,
|
||||
authenticatorFn: authenticatorFn,
|
||||
lookupUserWithLoginFn: lookupUserWithFn,
|
||||
tokenCreatorFn: tokenCreatorFn,
|
||||
authMethodId: testNotPrimaryAuthMethod.PublicId,
|
||||
loginName: testLoginName,
|
||||
password: testPassword,
|
||||
wantErrMatch: errors.T(errors.RecordNotFound),
|
||||
wantErrContains: "auth method is not primary for the scope so refusing to auto-create user",
|
||||
},
|
||||
{
|
||||
order: 4,
|
||||
name: "missing-authenticator-fn",
|
||||
ctx: testCtx,
|
||||
lookupUserWithLoginFn: lookupUserWithFn,
|
||||
tokenCreatorFn: tokenCreatorFn,
|
||||
authMethodId: testNotPrimaryAuthMethod.PublicId,
|
||||
loginName: testLoginName,
|
||||
password: testPassword,
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing authenticator factory",
|
||||
},
|
||||
{
|
||||
order: 5,
|
||||
name: "missing-lookup-user-fn",
|
||||
ctx: testCtx,
|
||||
authenticatorFn: authenticatorFn,
|
||||
tokenCreatorFn: tokenCreatorFn,
|
||||
authMethodId: testNotPrimaryAuthMethod.PublicId,
|
||||
loginName: testLoginName,
|
||||
password: testPassword,
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing lookup user factory",
|
||||
},
|
||||
{
|
||||
order: 6,
|
||||
name: "missing-at-creator-fn",
|
||||
ctx: testCtx,
|
||||
authenticatorFn: authenticatorFn,
|
||||
lookupUserWithLoginFn: lookupUserWithFn,
|
||||
authMethodId: testNotPrimaryAuthMethod.PublicId,
|
||||
loginName: testLoginName,
|
||||
password: testPassword,
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing auth token creator factory",
|
||||
},
|
||||
{
|
||||
order: 7,
|
||||
name: "missing-auth-method-id",
|
||||
ctx: testCtx,
|
||||
authenticatorFn: authenticatorFn,
|
||||
lookupUserWithLoginFn: lookupUserWithFn,
|
||||
tokenCreatorFn: tokenCreatorFn,
|
||||
loginName: testLoginName,
|
||||
password: testPassword,
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing auth method id",
|
||||
},
|
||||
{
|
||||
order: 8,
|
||||
name: "missing-login-name",
|
||||
ctx: testCtx,
|
||||
authenticatorFn: authenticatorFn,
|
||||
lookupUserWithLoginFn: lookupUserWithFn,
|
||||
tokenCreatorFn: tokenCreatorFn,
|
||||
authMethodId: testNotPrimaryAuthMethod.PublicId,
|
||||
password: testPassword,
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing login name",
|
||||
},
|
||||
{
|
||||
order: 9,
|
||||
name: "missing-password",
|
||||
ctx: testCtx,
|
||||
authenticatorFn: authenticatorFn,
|
||||
lookupUserWithLoginFn: lookupUserWithFn,
|
||||
tokenCreatorFn: tokenCreatorFn,
|
||||
authMethodId: testNotPrimaryAuthMethod.PublicId,
|
||||
loginName: testLoginName,
|
||||
wantErrMatch: errors.T(errors.InvalidParameter),
|
||||
wantErrContains: "missing password",
|
||||
},
|
||||
{
|
||||
order: 10,
|
||||
name: "authenticator-fn-err",
|
||||
ctx: testCtx,
|
||||
authenticatorFn: func() (Authenticator, error) {
|
||||
return nil, errors.New(testCtx, errors.Internal, "test", "authenticator-fn-err")
|
||||
},
|
||||
lookupUserWithLoginFn: lookupUserWithFn,
|
||||
tokenCreatorFn: tokenCreatorFn,
|
||||
authMethodId: testPrimaryAuthMethod.PublicId,
|
||||
loginName: testLoginName,
|
||||
password: testPassword,
|
||||
wantErrMatch: errors.T(errors.Internal),
|
||||
wantErrContains: "authenticator-fn-err",
|
||||
},
|
||||
{
|
||||
order: 11,
|
||||
name: "lookup-user-fn-err",
|
||||
ctx: testCtx,
|
||||
authenticatorFn: authenticatorFn,
|
||||
lookupUserWithLoginFn: func() (LookupUser, error) {
|
||||
return nil, errors.New(testCtx, errors.Internal, "test", "lookup-user-fn-err")
|
||||
},
|
||||
tokenCreatorFn: tokenCreatorFn,
|
||||
authMethodId: testPrimaryAuthMethod.PublicId,
|
||||
loginName: testLoginName,
|
||||
password: testPassword,
|
||||
wantErrMatch: errors.T(errors.Internal),
|
||||
wantErrContains: "lookup-user-fn-err",
|
||||
},
|
||||
{
|
||||
order: 12,
|
||||
name: "at-fn-err",
|
||||
ctx: testCtx,
|
||||
authenticatorFn: authenticatorFn,
|
||||
lookupUserWithLoginFn: lookupUserWithFn,
|
||||
tokenCreatorFn: func() (AuthTokenCreator, error) {
|
||||
return nil, errors.New(testCtx, errors.Internal, "test", "at-fn-err")
|
||||
},
|
||||
authMethodId: testPrimaryAuthMethod.PublicId,
|
||||
loginName: testLoginName,
|
||||
password: testPassword,
|
||||
wantErrMatch: errors.T(errors.Internal),
|
||||
wantErrContains: "at-fn-err",
|
||||
},
|
||||
{
|
||||
order: 13,
|
||||
name: "authenticate-err",
|
||||
ctx: testCtx,
|
||||
authenticatorFn: func() (Authenticator, error) {
|
||||
return &mockAuthenticator{authErr: errors.New(testCtx, errors.Internal, "test", "authenticate-err")}, nil
|
||||
},
|
||||
lookupUserWithLoginFn: lookupUserWithFn,
|
||||
tokenCreatorFn: tokenCreatorFn,
|
||||
authMethodId: testPrimaryAuthMethod.PublicId,
|
||||
loginName: testLoginName,
|
||||
password: testPassword,
|
||||
wantErrMatch: errors.T(errors.Internal),
|
||||
wantErrContains: "authenticate-err",
|
||||
},
|
||||
{
|
||||
order: 14,
|
||||
name: "token-err",
|
||||
ctx: testCtx,
|
||||
authenticatorFn: authenticatorFn,
|
||||
lookupUserWithLoginFn: lookupUserWithFn,
|
||||
tokenCreatorFn: func() (AuthTokenCreator, error) {
|
||||
return &mockTokenCreator{tokenErr: errors.New(testCtx, errors.Internal, "test", "token-err")}, nil
|
||||
},
|
||||
authMethodId: testPrimaryAuthMethod.PublicId,
|
||||
loginName: testLoginName,
|
||||
password: testPassword,
|
||||
wantErrMatch: errors.T(errors.Internal),
|
||||
wantErrContains: "token-err",
|
||||
},
|
||||
}
|
||||
for idx, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
require.Equal(tc.order, idx)
|
||||
got, err := Authenticate(tc.ctx, tc.authenticatorFn, tc.lookupUserWithLoginFn, tc.tokenCreatorFn, tc.authMethodId, tc.loginName, tc.password)
|
||||
if tc.wantErrMatch != nil {
|
||||
require.Error(err)
|
||||
assert.Empty(got)
|
||||
assert.Truef(errors.Match(tc.wantErrMatch, err), "unexpected error")
|
||||
if tc.wantErrContains != "" {
|
||||
assert.Contains(err.Error(), tc.wantErrContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.NotEmpty(got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type mockAuthenticator struct {
|
||||
authErr error
|
||||
}
|
||||
|
||||
func (m *mockAuthenticator) Authenticate(ctx context.Context, authMethodId, loginName, password string) (*Account, error) {
|
||||
return nil, m.authErr
|
||||
}
|
||||
|
||||
type mockTokenCreator struct {
|
||||
tokenErr error
|
||||
}
|
||||
|
||||
func (m *mockTokenCreator) CreateAuthToken(ctx context.Context, withIamUser *iam.User, withAuthAccountId string, opt ...authtoken.Option) (*authtoken.AuthToken, error) {
|
||||
return nil, m.tokenErr
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ldap
|
||||
|
||||
// AuthMethodState defines the possible states for an ldap auth method
|
||||
type AuthMethodState string
|
||||
|
||||
const (
|
||||
UnknownState AuthMethodState = "unknown"
|
||||
InactiveState AuthMethodState = "inactive"
|
||||
ActivePrivateState AuthMethodState = "active-private"
|
||||
ActivePublicState AuthMethodState = "active-public"
|
||||
)
|
||||
|
||||
func validState(s string) bool {
|
||||
st := AuthMethodState(s)
|
||||
switch st {
|
||||
case InactiveState, ActivePrivateState, ActivePublicState:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (s AuthMethodState) String() string {
|
||||
return string(s)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ldap
|
||||
|
||||
import "testing"
|
||||
|
||||
func Test_validState(t *testing.T) {
|
||||
tests := []struct {
|
||||
s string
|
||||
want bool
|
||||
}{
|
||||
{"bad", false},
|
||||
{"", false},
|
||||
{"unknown", false},
|
||||
{"inactive", true},
|
||||
{"active-private", true},
|
||||
{"active-public", true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.s, func(t *testing.T) {
|
||||
if got := validState(tt.s); got != tt.want {
|
||||
t.Errorf("validState() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,301 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"math/big"
|
||||
"net"
|
||||
"net/url"
|
||||
"sort"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
wrapping "github.com/hashicorp/go-kms-wrapping/v2"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const TestInvalidPem = `-----BEGIN CERTIFICATE-----
|
||||
MIICUTCCAfugAwIBAgIBADANBgkqhkiG9w0BAQQFADBXMQswCQYDVQQGEwJDTjEL
|
||||
-----END CERTIFICATE-----`
|
||||
|
||||
var testGrpNames = []string{"test-admin", "test-users"}
|
||||
|
||||
// TestEncodeGrpNames will json marshal group names
|
||||
func TestEncodedGrpNames(t *testing.T, names ...string) string {
|
||||
encoded, err := json.Marshal(names)
|
||||
require.NoError(t, err)
|
||||
return string(encoded)
|
||||
}
|
||||
|
||||
// TestAuthMethod creates a new auth method and it's persisted in the database.
|
||||
// See NewAuthMethod for list of supported options.
|
||||
func TestAuthMethod(t testing.TB,
|
||||
conn *db.DB,
|
||||
databaseWrapper wrapping.Wrapper,
|
||||
scopeId string,
|
||||
urls []string,
|
||||
opt ...Option,
|
||||
) *AuthMethod {
|
||||
t.Helper()
|
||||
require.Greater(t, len(urls), 0)
|
||||
testCtx := context.TODO()
|
||||
require := require.New(t)
|
||||
rw := db.New(conn)
|
||||
|
||||
opt = append(opt, WithUrls(testCtx, TestConvertToUrls(t, urls...)...))
|
||||
|
||||
opts, err := getOpts(opt...)
|
||||
require.NoError(err)
|
||||
am, err := NewAuthMethod(testCtx, scopeId, opt...)
|
||||
require.NoError(err)
|
||||
id, err := newAuthMethodId(testCtx)
|
||||
require.NoError(err)
|
||||
am.PublicId = id
|
||||
_, err = rw.DoTx(testCtx, 0, db.ConstBackoff{}, func(r db.Reader, w db.Writer) error {
|
||||
if err := w.Create(testCtx, am); err != nil {
|
||||
return err
|
||||
}
|
||||
for priority, u := range urls {
|
||||
storeUrl, err := NewUrl(testCtx, am.PublicId, priority+1, TestConvertToUrls(t, u)[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.Create(testCtx, storeUrl); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if len(opts.withCertificates) > 0 {
|
||||
for _, pem := range opts.withCertificates {
|
||||
c, err := NewCertificate(testCtx, am.PublicId, pem)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.Create(testCtx, c); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(opts.withAccountAttributeMap) > 0 {
|
||||
for from, to := range opts.withAccountAttributeMap {
|
||||
aam, err := NewAccountAttributeMap(testCtx, am.PublicId, from, AccountToAttribute(string(to)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.Create(testCtx, aam); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if opts.withUserDn != "" || opts.withUserAttr != "" || opts.withUserFilter != "" {
|
||||
uc, err := NewUserEntrySearchConf(testCtx, am.PublicId, opt...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = w.Create(testCtx, uc); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if opts.withGroupDn != "" || opts.withGroupAttr != "" || opts.withGroupFilter != "" {
|
||||
uc, err := NewGroupEntrySearchConf(testCtx, am.PublicId, opt...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = w.Create(testCtx, uc); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if opts.withClientCertificate != "" || len(opts.withClientCertificateKey) != 0 {
|
||||
cc, err := NewClientCertificate(testCtx, am.PublicId, opts.withClientCertificateKey, opts.withClientCertificate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cc.encrypt(testCtx, databaseWrapper); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.Create(testCtx, cc); err != nil {
|
||||
return err
|
||||
}
|
||||
am.ClientCertificateKeyHmac = cc.CertificateKeyHmac
|
||||
}
|
||||
if opts.withBindDn != "" || opts.withBindPassword != "" {
|
||||
bc, err := NewBindCredential(testCtx, am.PublicId, opts.withBindDn, []byte(opts.withBindPassword))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := bc.encrypt(testCtx, databaseWrapper); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := w.Create(testCtx, bc); err != nil {
|
||||
return err
|
||||
}
|
||||
am.BindPasswordHmac = bc.PasswordHmac
|
||||
}
|
||||
return nil
|
||||
})
|
||||
require.NoError(err)
|
||||
|
||||
return am
|
||||
}
|
||||
|
||||
// TestAccount creates a test ldap auth account.
|
||||
func TestAccount(t testing.TB, conn *db.DB, am *AuthMethod, loginName string, opt ...Option) *Account {
|
||||
t.Helper()
|
||||
require := require.New(t)
|
||||
rw := db.New(conn)
|
||||
ctx := context.Background()
|
||||
|
||||
a, err := NewAccount(ctx, am.ScopeId, am.PublicId, loginName, opt...)
|
||||
require.NoError(err)
|
||||
|
||||
id, err := newAccountId(ctx, am.PublicId, loginName)
|
||||
require.NoError(err)
|
||||
a.PublicId = id
|
||||
|
||||
require.NoError(rw.Create(ctx, a))
|
||||
return a
|
||||
}
|
||||
|
||||
// TestManagedGroup creates a test ldap managed group.
|
||||
func TestManagedGroup(t testing.TB, conn *db.DB, am *AuthMethod, grpNames []string, opt ...Option) *ManagedGroup {
|
||||
t.Helper()
|
||||
require := require.New(t)
|
||||
rw := db.New(conn)
|
||||
ctx := context.Background()
|
||||
|
||||
mg, err := NewManagedGroup(ctx, am.PublicId, grpNames, opt...)
|
||||
require.NoError(err)
|
||||
|
||||
id, err := newManagedGroupId(ctx)
|
||||
require.NoError(err)
|
||||
mg.PublicId = id
|
||||
|
||||
require.NoError(rw.Create(ctx, mg, db.WithLookup(true)))
|
||||
return mg
|
||||
}
|
||||
|
||||
// TestGenerateCA will generate a test x509 CA cert, along with it encoded in a
|
||||
// PEM format.
|
||||
func TestGenerateCA(t testing.TB, hosts ...string) (*x509.Certificate, string) {
|
||||
t.Helper()
|
||||
require := require.New(t)
|
||||
|
||||
priv, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
||||
require.NoError(err)
|
||||
|
||||
// ECDSA, ED25519 and RSA subject keys should have the DigitalSignature
|
||||
// KeyUsage bits set in the x509.Certificate template
|
||||
keyUsage := x509.KeyUsageDigitalSignature
|
||||
|
||||
validFor := 2 * time.Minute
|
||||
notBefore := time.Now()
|
||||
notAfter := notBefore.Add(validFor)
|
||||
|
||||
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
||||
require.NoError(err)
|
||||
|
||||
template := x509.Certificate{
|
||||
SerialNumber: serialNumber,
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{"Acme Co"},
|
||||
},
|
||||
NotBefore: notBefore,
|
||||
NotAfter: notAfter,
|
||||
|
||||
KeyUsage: keyUsage,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
|
||||
for _, h := range hosts {
|
||||
if ip := net.ParseIP(h); ip != nil {
|
||||
template.IPAddresses = append(template.IPAddresses, ip)
|
||||
} else {
|
||||
template.DNSNames = append(template.DNSNames, h)
|
||||
}
|
||||
}
|
||||
|
||||
template.IsCA = true
|
||||
template.KeyUsage |= x509.KeyUsageCertSign
|
||||
|
||||
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
|
||||
require.NoError(err)
|
||||
|
||||
c, err := x509.ParseCertificate(derBytes)
|
||||
require.NoError(err)
|
||||
|
||||
return c, string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}))
|
||||
}
|
||||
|
||||
// TestConvertToUrls will convert URL string representations to a slice of
|
||||
// *url.URL
|
||||
func TestConvertToUrls(t testing.TB, urls ...string) []*url.URL {
|
||||
t.Helper()
|
||||
require := require.New(t)
|
||||
require.NotEmpty(urls)
|
||||
var convertedUrls []*url.URL
|
||||
for _, u := range urls {
|
||||
parsed, err := url.Parse(u)
|
||||
require.NoError(err)
|
||||
require.Contains([]string{"ldap", "ldaps"}, parsed.Scheme)
|
||||
convertedUrls = append(convertedUrls, parsed)
|
||||
}
|
||||
return convertedUrls
|
||||
}
|
||||
|
||||
// TestSortAuthMethods will sort the provided auth methods by public id and it
|
||||
// will sort each auth method's embedded value objects
|
||||
func TestSortAuthMethods(t testing.TB, methods []*AuthMethod) {
|
||||
// sort them by public id first...
|
||||
sort.Slice(methods, func(a, b int) bool {
|
||||
return methods[a].PublicId < methods[b].PublicId
|
||||
})
|
||||
|
||||
// sort all the embedded value objects...
|
||||
for _, am := range methods {
|
||||
sort.Slice(am.Urls, func(a, b int) bool {
|
||||
return am.Urls[a] < am.Urls[b]
|
||||
})
|
||||
sort.Slice(am.Certificates, func(a, b int) bool {
|
||||
return am.Certificates[a] < am.Certificates[b]
|
||||
})
|
||||
sort.Slice(am.AccountAttributeMaps, func(a, b int) bool {
|
||||
return am.AccountAttributeMaps[a] < am.AccountAttributeMaps[b]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetAcctManagedGroups will retrieve the managed groups associated with an account.
|
||||
func TestGetAcctManagedGroups(t testing.TB, conn *db.DB, acctId string) []string {
|
||||
t.Helper()
|
||||
testCtx := context.Background()
|
||||
rw := db.New(conn)
|
||||
var memberAccts []*grpMemberAccts
|
||||
require.NoError(t, rw.SearchWhere(testCtx, &memberAccts, "member_id = ?", []any{acctId}))
|
||||
grpIds := make([]string, 0, len(memberAccts))
|
||||
for _, mbrAcct := range memberAccts {
|
||||
grpIds = append(grpIds, mbrAcct.ManagedGroupId)
|
||||
}
|
||||
return grpIds
|
||||
}
|
||||
|
||||
type grpMemberAccts struct {
|
||||
CreateTime time.Time
|
||||
MemberId string
|
||||
ManagedGroupId string
|
||||
ManagedGroupName string
|
||||
}
|
||||
|
||||
func (*grpMemberAccts) TableName() string {
|
||||
return "auth_ldap_managed_group_member_account"
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/iam"
|
||||
"github.com/hashicorp/boundary/internal/kms"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/crypto/ed25519"
|
||||
)
|
||||
|
||||
func Test_testAuthMethod(t *testing.T) {
|
||||
t.Parallel()
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
testWrapper := db.TestWrapper(t)
|
||||
testKms := kms.TestKms(t, conn, testWrapper)
|
||||
org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, testWrapper))
|
||||
databaseWrapper, err := testKms.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase)
|
||||
require.NoError(err)
|
||||
|
||||
testCtx := context.Background()
|
||||
c1, c1Pem := TestGenerateCA(t, "localhost")
|
||||
c2, c2Pem := TestGenerateCA(t, "127.0.0.1")
|
||||
_, privKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
require.NoError(err)
|
||||
derPrivKey, err := x509.MarshalPKCS8PrivateKey(privKey)
|
||||
require.NoError(err)
|
||||
|
||||
am := TestAuthMethod(
|
||||
t, conn, databaseWrapper,
|
||||
org.PublicId,
|
||||
[]string{"ldaps://d1.alice.com", "ldap://d2.alice.com"},
|
||||
WithName(testCtx, "test-name"),
|
||||
WithDescription(testCtx, "test-desc"),
|
||||
WithStartTLS(testCtx),
|
||||
WithInsecureTLS(testCtx),
|
||||
WithDiscoverDn(testCtx),
|
||||
WithAnonGroupSearch(testCtx),
|
||||
WithUpnDomain(testCtx, "alice.com"),
|
||||
WithCertificates(testCtx, c1, c2),
|
||||
WithUserDn(testCtx, "user-dn"),
|
||||
WithUserAttr(testCtx, "user-attr"),
|
||||
WithUserFilter(testCtx, "user-filter"),
|
||||
WithGroupDn(testCtx, "group-dn"),
|
||||
WithGroupAttr(testCtx, "group-attr"),
|
||||
WithGroupFilter(testCtx, "group-filter"),
|
||||
WithClientCertificate(testCtx, derPrivKey, c2), // not a client cert, but good enough for the test.
|
||||
WithBindCredential(testCtx, "bind-dn", "bind-password"),
|
||||
)
|
||||
|
||||
rw := db.New(conn)
|
||||
found := AllocAuthMethod()
|
||||
found.PublicId = am.PublicId
|
||||
rw.LookupById(testCtx, &found)
|
||||
assert.Equal(am.PublicId, found.PublicId)
|
||||
assert.Equal(found.ScopeId, org.PublicId)
|
||||
assert.Equal(found.Name, "test-name")
|
||||
assert.Equal(found.Description, "test-desc")
|
||||
assert.Equal(found.OperationalState, InactiveState.String())
|
||||
assert.Equal(found.Version, uint32(1))
|
||||
assert.True(found.StartTls)
|
||||
assert.True(found.InsecureTls)
|
||||
assert.True(found.DiscoverDn)
|
||||
assert.True(found.AnonGroupSearch)
|
||||
assert.Equal("alice.com", found.UpnDomain)
|
||||
|
||||
foundUrls := []*Url{}
|
||||
err = rw.SearchWhere(testCtx, &foundUrls, "ldap_method_id = ?", []any{found.PublicId}, db.WithOrder("connection_priority asc"))
|
||||
require.NoError(err)
|
||||
assert.Equal("ldaps://d1.alice.com", foundUrls[0].GetServerUrl())
|
||||
assert.Equal(uint32(1), foundUrls[0].ConnectionPriority)
|
||||
assert.Equal("ldap://d2.alice.com", foundUrls[1].GetServerUrl())
|
||||
assert.Equal(uint32(2), foundUrls[1].ConnectionPriority)
|
||||
|
||||
foundCerts := []*Certificate{}
|
||||
err = rw.SearchWhere(testCtx, &foundCerts, "ldap_method_id = ?", []any{found.PublicId}, db.WithOrder("create_time asc"))
|
||||
require.NoError(err)
|
||||
assert.Equal(c1Pem, foundCerts[0].GetCert())
|
||||
assert.Equal(c2Pem, foundCerts[1].GetCert())
|
||||
|
||||
foundUserSearchConf := allocUserEntrySearchConf()
|
||||
err = rw.LookupWhere(testCtx, &foundUserSearchConf, "ldap_method_id = ?", []any{found.PublicId})
|
||||
require.NoError(err)
|
||||
assert.Equal("user-dn", foundUserSearchConf.GetUserDn())
|
||||
assert.Equal("user-attr", foundUserSearchConf.GetUserAttr())
|
||||
assert.Equal("user-filter", foundUserSearchConf.GetUserFilter())
|
||||
|
||||
foundGroupSearchConf := allocGroupEntrySearchConf()
|
||||
err = rw.LookupWhere(testCtx, &foundGroupSearchConf, "ldap_method_id = ?", []any{found.PublicId})
|
||||
require.NoError(err)
|
||||
assert.Equal("group-dn", foundGroupSearchConf.GetGroupDn())
|
||||
assert.Equal("group-attr", foundGroupSearchConf.GetGroupAttr())
|
||||
assert.Equal("group-filter", foundGroupSearchConf.GetGroupFilter())
|
||||
|
||||
foundClientCert := allocClientCertificate()
|
||||
err = rw.LookupWhere(testCtx, &foundClientCert, "ldap_method_id = ?", []any{found.PublicId})
|
||||
require.NoError(err)
|
||||
require.NoError(foundClientCert.decrypt(testCtx, databaseWrapper))
|
||||
assert.NotEmpty(foundClientCert.GetKeyId())
|
||||
assert.NotEmpty(foundClientCert.GetCertificate())
|
||||
assert.NotEmpty(foundClientCert.GetCertificateKey())
|
||||
assert.NotEmpty(foundClientCert.GetCertificateKeyHmac())
|
||||
|
||||
foundBindCred := allocBindCredential()
|
||||
err = rw.LookupWhere(testCtx, &foundBindCred, "ldap_method_id = ?", []any{found.PublicId})
|
||||
require.NoError(err)
|
||||
require.NoError(foundBindCred.decrypt(testCtx, databaseWrapper))
|
||||
assert.NotEmpty(foundBindCred.GetKeyId())
|
||||
assert.NotEmpty(foundBindCred.GetDn())
|
||||
assert.NotEmpty(foundBindCred.GetPassword())
|
||||
assert.NotEmpty(foundBindCred.GetPasswordHmac())
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/auth/ldap/store"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/hashicorp/go-secure-stdlib/strutil"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const urlTableName = "auth_ldap_url"
|
||||
|
||||
// Url represents a required one to many auth method urls. It is assigned to an
|
||||
// LDAP AuthMethod and updates/deletes to that AuthMethod are cascaded to its
|
||||
// Urls. Urls are value objects of an AuthMethod, therefore there's no need for
|
||||
// oplog metadata, since only the AuthMethod will have metadata because it's the
|
||||
// root aggregate.
|
||||
type Url struct {
|
||||
*store.Url
|
||||
tableName string
|
||||
}
|
||||
|
||||
// NewUrl creates a new in memory Url. connectionPriority cannot be less than
|
||||
// one. No options are currently supported.
|
||||
func NewUrl(ctx context.Context, authMethodId string, connectionPriority int, url *url.URL, _ ...Option) (*Url, error) {
|
||||
const op = "ldap.NewUrl"
|
||||
switch {
|
||||
case authMethodId == "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth method id")
|
||||
case connectionPriority < 1:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "connection priority cannot be less than one")
|
||||
case url == nil:
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing url")
|
||||
case !strutil.StrListContainsCaseInsensitive([]string{"ldap", "ldaps"}, url.Scheme):
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("scheme %q is not ldap or ldaps", url.Scheme))
|
||||
}
|
||||
return &Url{
|
||||
Url: &store.Url{
|
||||
LdapMethodId: authMethodId,
|
||||
ServerUrl: url.String(),
|
||||
ConnectionPriority: uint32(connectionPriority),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// allocUrl makes an empty one in memory
|
||||
func allocUrl() *Url {
|
||||
return &Url{
|
||||
Url: &store.Url{},
|
||||
}
|
||||
}
|
||||
|
||||
// clone a Url
|
||||
func (u *Url) clone() *Url {
|
||||
cp := proto.Clone(u.Url)
|
||||
return &Url{
|
||||
Url: cp.(*store.Url),
|
||||
}
|
||||
}
|
||||
|
||||
// TableName returns the table name
|
||||
func (u *Url) TableName() string {
|
||||
if u.tableName != "" {
|
||||
return u.tableName
|
||||
}
|
||||
return urlTableName
|
||||
}
|
||||
|
||||
// SetTableName sets the table name.
|
||||
func (u *Url) SetTableName(n string) {
|
||||
u.tableName = n
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/auth/ldap/store"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func TestNewUrl(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCtx := context.Background()
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
authMethodId string
|
||||
priority int
|
||||
url *url.URL
|
||||
want *Url
|
||||
wantErr bool
|
||||
wantErrCode errors.Code
|
||||
wantErrContains string
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
ctx: testCtx,
|
||||
authMethodId: "test-id",
|
||||
priority: 10,
|
||||
url: TestConvertToUrls(t, "ldaps://alice.com")[0],
|
||||
want: &Url{
|
||||
Url: &store.Url{
|
||||
LdapMethodId: "test-id",
|
||||
ConnectionPriority: 10,
|
||||
ServerUrl: "ldaps://alice.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing-auth-method-id",
|
||||
ctx: testCtx,
|
||||
url: TestConvertToUrls(t, "ldaps://alice.com")[0],
|
||||
wantErr: true,
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
wantErrContains: "missing auth method id",
|
||||
},
|
||||
{
|
||||
name: "missing-url",
|
||||
ctx: testCtx,
|
||||
priority: 1,
|
||||
authMethodId: "test-id",
|
||||
wantErr: true,
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
wantErrContains: "missing url",
|
||||
},
|
||||
{
|
||||
name: "invalid-scheme",
|
||||
ctx: testCtx,
|
||||
authMethodId: "test-id",
|
||||
priority: 1,
|
||||
url: func() *url.URL {
|
||||
parsed, err := url.Parse("https://alice.com")
|
||||
require.NoError(t, err)
|
||||
return parsed
|
||||
}(),
|
||||
wantErr: true,
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
wantErrContains: "scheme \"https\" is not ldap or ldaps",
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
got, err := NewUrl(tc.ctx, tc.authMethodId, tc.priority, tc.url)
|
||||
if tc.wantErr {
|
||||
require.Error(err)
|
||||
assert.Nil(got)
|
||||
if tc.wantErrCode != errors.Unknown {
|
||||
assert.True(errors.Match(errors.T(tc.wantErrCode), err))
|
||||
}
|
||||
if tc.wantErrContains != "" {
|
||||
assert.Contains(err.Error(), tc.wantErrContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUrl_SetTableName(t *testing.T) {
|
||||
t.Parallel()
|
||||
defaultTableName := urlTableName
|
||||
tests := []struct {
|
||||
name string
|
||||
setNameTo string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "new-name",
|
||||
setNameTo: "new-name",
|
||||
want: "new-name",
|
||||
},
|
||||
{
|
||||
name: "reset to default",
|
||||
setNameTo: "",
|
||||
want: defaultTableName,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
def := allocUrl()
|
||||
require.Equal(defaultTableName, def.TableName())
|
||||
m := allocUrl()
|
||||
m.SetTableName(tt.setNameTo)
|
||||
assert.Equal(tt.want, m.TableName())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUrl_clone(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCtx := context.Background()
|
||||
const priorityOfOne = 1
|
||||
t.Run("valid", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
u, err := NewUrl(testCtx, "test-id", priorityOfOne, TestConvertToUrls(t, "ldaps://alice.com")[0])
|
||||
require.NoError(err)
|
||||
cp := u.clone()
|
||||
assert.True(proto.Equal(cp.Url, u.Url))
|
||||
})
|
||||
t.Run("not-equal", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
u, err := NewUrl(testCtx, "test-id", priorityOfOne, TestConvertToUrls(t, "ldaps://alice.com")[0])
|
||||
require.NoError(err)
|
||||
|
||||
u2, err := NewUrl(testCtx, "test-id", priorityOfOne, TestConvertToUrls(t, "ldaps://bob.com")[0])
|
||||
require.NoError(err)
|
||||
|
||||
cp := u.clone()
|
||||
assert.True(!proto.Equal(cp.Url, u2.Url))
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/auth/ldap/store"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
const userEntrySearchConfTableName = "auth_ldap_user_entry_search"
|
||||
|
||||
// UserEntrySearchConf represent a set of optional configuration fields used to
|
||||
// search for user entries. It is assigned to an LDAP AuthMethod and
|
||||
// updates/deletes to that AuthMethod are cascaded to its UserEntrySearchConf.
|
||||
// UserEntrySearchConf are value objects of an AuthMethod, therefore there's no
|
||||
// need for oplog metadata, since only the AuthMethod will have metadata because
|
||||
// it's the root aggregate.
|
||||
type UserEntrySearchConf struct {
|
||||
*store.UserEntrySearchConf
|
||||
tableName string
|
||||
}
|
||||
|
||||
// NewUserEntrySearchConf creates a new in memory NewUserEntrySearchConf.
|
||||
// Supported options are: WithUserDn, WithUserAttr, WithUserFilter and all other
|
||||
// options are ignored.
|
||||
func NewUserEntrySearchConf(ctx context.Context, authMethodId string, opt ...Option) (*UserEntrySearchConf, error) {
|
||||
const op = "ldap.NewUserEntrySearchConf"
|
||||
opts, err := getOpts(opt...)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
switch {
|
||||
case authMethodId == "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth method id")
|
||||
case opts.withUserDn == "" && opts.withUserAttr == "" && opts.withUserFilter == "":
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "you must supply either dn, attr, or filter")
|
||||
}
|
||||
return &UserEntrySearchConf{
|
||||
UserEntrySearchConf: &store.UserEntrySearchConf{
|
||||
LdapMethodId: authMethodId,
|
||||
UserDn: opts.withUserDn,
|
||||
UserAttr: opts.withUserAttr,
|
||||
UserFilter: opts.withUserFilter,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// allocUserEntrySearchConf makes an empty one in memory
|
||||
func allocUserEntrySearchConf() *UserEntrySearchConf {
|
||||
return &UserEntrySearchConf{
|
||||
UserEntrySearchConf: &store.UserEntrySearchConf{},
|
||||
}
|
||||
}
|
||||
|
||||
// clone a UserEntrySearchConf
|
||||
func (uc *UserEntrySearchConf) clone() *UserEntrySearchConf {
|
||||
cp := proto.Clone(uc.UserEntrySearchConf)
|
||||
return &UserEntrySearchConf{
|
||||
UserEntrySearchConf: cp.(*store.UserEntrySearchConf),
|
||||
}
|
||||
}
|
||||
|
||||
// TableName returns the table name
|
||||
func (uc *UserEntrySearchConf) TableName() string {
|
||||
if uc.tableName != "" {
|
||||
return uc.tableName
|
||||
}
|
||||
return userEntrySearchConfTableName
|
||||
}
|
||||
|
||||
// SetTableName sets the table name.
|
||||
func (uc *UserEntrySearchConf) SetTableName(n string) {
|
||||
uc.tableName = n
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/auth/ldap/store"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func TestNewUserEntrySearchConf(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCtx := context.Background()
|
||||
tests := []struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
authMethodId string
|
||||
opts []Option
|
||||
want *UserEntrySearchConf
|
||||
wantErr bool
|
||||
wantErrCode errors.Code
|
||||
wantErrContains string
|
||||
}{
|
||||
{
|
||||
name: "valid",
|
||||
ctx: testCtx,
|
||||
authMethodId: "test-id",
|
||||
opts: []Option{
|
||||
WithUserDn(testCtx, "dn"),
|
||||
WithUserAttr(testCtx, "attr"),
|
||||
WithUserFilter(testCtx, "filter"),
|
||||
},
|
||||
want: &UserEntrySearchConf{
|
||||
UserEntrySearchConf: &store.UserEntrySearchConf{
|
||||
LdapMethodId: "test-id",
|
||||
UserDn: "dn",
|
||||
UserAttr: "attr",
|
||||
UserFilter: "filter",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "just-dn",
|
||||
ctx: testCtx,
|
||||
authMethodId: "test-id",
|
||||
opts: []Option{
|
||||
WithUserDn(testCtx, "dn"),
|
||||
},
|
||||
want: &UserEntrySearchConf{
|
||||
UserEntrySearchConf: &store.UserEntrySearchConf{
|
||||
LdapMethodId: "test-id",
|
||||
UserDn: "dn",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "just-attr",
|
||||
ctx: testCtx,
|
||||
authMethodId: "test-id",
|
||||
opts: []Option{
|
||||
WithUserAttr(testCtx, "attr"),
|
||||
},
|
||||
want: &UserEntrySearchConf{
|
||||
UserEntrySearchConf: &store.UserEntrySearchConf{
|
||||
LdapMethodId: "test-id",
|
||||
UserAttr: "attr",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "just-filter",
|
||||
ctx: testCtx,
|
||||
authMethodId: "test-id",
|
||||
opts: []Option{
|
||||
WithUserFilter(testCtx, "filter"),
|
||||
},
|
||||
want: &UserEntrySearchConf{
|
||||
UserEntrySearchConf: &store.UserEntrySearchConf{
|
||||
LdapMethodId: "test-id",
|
||||
UserFilter: "filter",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing-auth-method-id",
|
||||
ctx: testCtx,
|
||||
opts: []Option{WithUserDn(testCtx, "dn")},
|
||||
wantErr: true,
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
wantErrContains: "missing auth method id",
|
||||
},
|
||||
{
|
||||
name: "no-opts",
|
||||
ctx: testCtx,
|
||||
authMethodId: "test-id",
|
||||
wantErr: true,
|
||||
wantErrCode: errors.InvalidParameter,
|
||||
wantErrContains: "you must supply either dn, attr, or filter",
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
got, err := NewUserEntrySearchConf(tc.ctx, tc.authMethodId, tc.opts...)
|
||||
if tc.wantErr {
|
||||
require.Error(err)
|
||||
assert.Nil(got)
|
||||
if tc.wantErrCode != errors.Unknown {
|
||||
assert.True(errors.Match(errors.T(tc.wantErrCode), err))
|
||||
}
|
||||
if tc.wantErrContains != "" {
|
||||
assert.Contains(err.Error(), tc.wantErrContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
assert.Equal(tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserEntrySearchConf_SetTableName(t *testing.T) {
|
||||
t.Parallel()
|
||||
defaultTableName := userEntrySearchConfTableName
|
||||
tests := []struct {
|
||||
name string
|
||||
setNameTo string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "new-name",
|
||||
setNameTo: "new-name",
|
||||
want: "new-name",
|
||||
},
|
||||
{
|
||||
name: "reset to default",
|
||||
setNameTo: "",
|
||||
want: defaultTableName,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
def := allocUserEntrySearchConf()
|
||||
require.Equal(defaultTableName, def.TableName())
|
||||
m := allocUserEntrySearchConf()
|
||||
m.SetTableName(tt.setNameTo)
|
||||
assert.Equal(tt.want, m.TableName())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserEntrySearchCon_clone(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCtx := context.Background()
|
||||
t.Run("valid", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
uc, err := NewUserEntrySearchConf(testCtx, "test-id", WithUserDn(testCtx, "dn"))
|
||||
require.NoError(err)
|
||||
cp := uc.clone()
|
||||
assert.True(proto.Equal(cp.UserEntrySearchConf, uc.UserEntrySearchConf))
|
||||
})
|
||||
t.Run("not-equal", func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
uc, err := NewUserEntrySearchConf(testCtx, "test-id", WithUserDn(testCtx, "dn"))
|
||||
require.NoError(err)
|
||||
|
||||
uc2, err := NewUserEntrySearchConf(testCtx, "test-id", WithUserDn(testCtx, "dn2"))
|
||||
require.NoError(err)
|
||||
|
||||
cp := uc.clone()
|
||||
assert.True(!proto.Equal(cp.UserEntrySearchConf, uc2.UserEntrySearchConf))
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/db/timestamp"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// ManagedGroupMemberAccount represents an entry from
|
||||
// auth_managed_group_member_account. These are used to determine the account
|
||||
// ids where are a member of managed groups. See: oidc and ldap managed groups
|
||||
// as well as iam role grants.
|
||||
type ManagedGroupMemberAccount struct {
|
||||
CreateTime *timestamp.Timestamp
|
||||
MemberId string
|
||||
ManagedGroupId string
|
||||
tableName string
|
||||
}
|
||||
|
||||
// SetTableName sets the table name.
|
||||
func (a *ManagedGroupMemberAccount) SetTableName(n string) {
|
||||
a.tableName = n
|
||||
}
|
||||
|
||||
// TableName returns the table name.
|
||||
func (a *ManagedGroupMemberAccount) TableName() string {
|
||||
if a.tableName != "" {
|
||||
return a.tableName
|
||||
}
|
||||
return "auth_managed_group_member_account"
|
||||
}
|
||||
|
||||
// TestSortManagedGroupMemberAccounts simply sorts them by public id to make
|
||||
// comparisons a bit easier.
|
||||
func TestSortManagedGroupMemberAccounts(t testing.TB, m []*ManagedGroupMemberAccount) {
|
||||
sort.Slice(m, func(a, b int) bool {
|
||||
return m[a].MemberId < m[b].MemberId
|
||||
})
|
||||
}
|
||||
|
||||
// TestManagedGroupMemberAccounts retrieves the accounts with membership in the
|
||||
// specified managed group.
|
||||
func TestManagedGroupMemberAccounts(t *testing.T, conn *db.DB, managedGroupId string) []*ManagedGroupMemberAccount {
|
||||
var mgmAccts []*ManagedGroupMemberAccount
|
||||
ctx := context.Background()
|
||||
rw := db.New(conn)
|
||||
err := rw.SearchWhere(ctx, &mgmAccts, "managed_group_id = ?", []any{managedGroupId})
|
||||
require.NoError(t, err)
|
||||
TestSortManagedGroupMemberAccounts(t, mgmAccts)
|
||||
return mgmAccts
|
||||
}
|
||||
@@ -74,6 +74,11 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) {
|
||||
Command: base.NewCommand(ui),
|
||||
}, nil
|
||||
},
|
||||
"authenticate ldap": func() (cli.Command, error) {
|
||||
return &authenticate.LdapCommand{
|
||||
Command: base.NewCommand(ui),
|
||||
}, nil
|
||||
},
|
||||
|
||||
"accounts": func() (cli.Command, error) {
|
||||
return &accountscmd.Command{
|
||||
@@ -128,6 +133,12 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) {
|
||||
Func: "create",
|
||||
}, nil
|
||||
},
|
||||
"accounts create ldap": func() (cli.Command, error) {
|
||||
return &accountscmd.LdapCommand{
|
||||
Command: base.NewCommand(ui),
|
||||
Func: "create",
|
||||
}, nil
|
||||
},
|
||||
"accounts update": func() (cli.Command, error) {
|
||||
return &accountscmd.Command{
|
||||
Command: base.NewCommand(ui),
|
||||
@@ -146,6 +157,12 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) {
|
||||
Func: "update",
|
||||
}, nil
|
||||
},
|
||||
"accounts update ldap": func() (cli.Command, error) {
|
||||
return &accountscmd.LdapCommand{
|
||||
Command: base.NewCommand(ui),
|
||||
Func: "update",
|
||||
}, nil
|
||||
},
|
||||
|
||||
"auth-methods": func() (cli.Command, error) {
|
||||
return &authmethodscmd.Command{
|
||||
@@ -188,6 +205,12 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) {
|
||||
Func: "create",
|
||||
}, nil
|
||||
},
|
||||
"auth-methods create ldap": func() (cli.Command, error) {
|
||||
return &authmethodscmd.LdapCommand{
|
||||
Command: base.NewCommand(ui),
|
||||
Func: "create",
|
||||
}, nil
|
||||
},
|
||||
"auth-methods update": func() (cli.Command, error) {
|
||||
return &authmethodscmd.Command{
|
||||
Command: base.NewCommand(ui),
|
||||
@@ -206,6 +229,12 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) {
|
||||
Func: "update",
|
||||
}, nil
|
||||
},
|
||||
"auth-methods update ldap": func() (cli.Command, error) {
|
||||
return &authmethodscmd.LdapCommand{
|
||||
Command: base.NewCommand(ui),
|
||||
Func: "update",
|
||||
}, nil
|
||||
},
|
||||
"auth-methods change-state oidc": func() (cli.Command, error) {
|
||||
return &authmethodscmd.OidcCommand{
|
||||
Command: base.NewCommand(ui),
|
||||
@@ -817,6 +846,12 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) {
|
||||
Func: "create",
|
||||
}, nil
|
||||
},
|
||||
"managed-groups create ldap": func() (cli.Command, error) {
|
||||
return &managedgroupscmd.LdapCommand{
|
||||
Command: base.NewCommand(ui),
|
||||
Func: "create",
|
||||
}, nil
|
||||
},
|
||||
"managed-groups update": func() (cli.Command, error) {
|
||||
return &managedgroupscmd.Command{
|
||||
Command: base.NewCommand(ui),
|
||||
@@ -829,6 +864,12 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) {
|
||||
Func: "update",
|
||||
}, nil
|
||||
},
|
||||
"managed-groups update ldap": func() (cli.Command, error) {
|
||||
return &managedgroupscmd.LdapCommand{
|
||||
Command: base.NewCommand(ui),
|
||||
Func: "update",
|
||||
}, nil
|
||||
},
|
||||
|
||||
"roles": func() (cli.Command, error) {
|
||||
return &rolescmd.Command{
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
// Code generated by "make cli"; DO NOT EDIT.
|
||||
package accountscmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/boundary/api"
|
||||
"github.com/hashicorp/boundary/api/accounts"
|
||||
"github.com/hashicorp/boundary/internal/cmd/base"
|
||||
"github.com/hashicorp/boundary/internal/cmd/common"
|
||||
"github.com/hashicorp/go-secure-stdlib/strutil"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
|
||||
func initLdapFlags() {
|
||||
flagsOnce.Do(func() {
|
||||
extraFlags := extraLdapActionsFlagsMapFunc()
|
||||
for k, v := range extraFlags {
|
||||
flagsLdapMap[k] = append(flagsLdapMap[k], v...)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var (
|
||||
_ cli.Command = (*LdapCommand)(nil)
|
||||
_ cli.CommandAutocomplete = (*LdapCommand)(nil)
|
||||
)
|
||||
|
||||
type LdapCommand struct {
|
||||
*base.Command
|
||||
|
||||
Func string
|
||||
|
||||
plural string
|
||||
|
||||
extraLdapCmdVars
|
||||
}
|
||||
|
||||
func (c *LdapCommand) AutocompleteArgs() complete.Predictor {
|
||||
initLdapFlags()
|
||||
return complete.PredictAnything
|
||||
}
|
||||
|
||||
func (c *LdapCommand) AutocompleteFlags() complete.Flags {
|
||||
initLdapFlags()
|
||||
return c.Flags().Completions()
|
||||
}
|
||||
|
||||
func (c *LdapCommand) Synopsis() string {
|
||||
if extra := extraLdapSynopsisFunc(c); extra != "" {
|
||||
return extra
|
||||
}
|
||||
|
||||
synopsisStr := "account"
|
||||
|
||||
synopsisStr = fmt.Sprintf("%s %s", "ldap-type", synopsisStr)
|
||||
|
||||
return common.SynopsisFunc(c.Func, synopsisStr)
|
||||
}
|
||||
|
||||
func (c *LdapCommand) Help() string {
|
||||
initLdapFlags()
|
||||
|
||||
var helpStr string
|
||||
helpMap := common.HelpMap("account")
|
||||
|
||||
switch c.Func {
|
||||
|
||||
default:
|
||||
|
||||
helpStr = c.extraLdapHelpFunc(helpMap)
|
||||
|
||||
}
|
||||
|
||||
// Keep linter from complaining if we don't actually generate code using it
|
||||
_ = helpMap
|
||||
return helpStr
|
||||
}
|
||||
|
||||
var flagsLdapMap = map[string][]string{
|
||||
|
||||
"create": {"auth-method-id", "name", "description"},
|
||||
|
||||
"update": {"id", "name", "description", "version"},
|
||||
}
|
||||
|
||||
func (c *LdapCommand) Flags() *base.FlagSets {
|
||||
if len(flagsLdapMap[c.Func]) == 0 {
|
||||
return c.FlagSet(base.FlagSetNone)
|
||||
}
|
||||
|
||||
set := c.FlagSet(base.FlagSetHTTP | base.FlagSetClient | base.FlagSetOutputFormat)
|
||||
f := set.NewFlagSet("Command Options")
|
||||
common.PopulateCommonFlags(c.Command, f, "ldap-type account", flagsLdapMap, c.Func)
|
||||
|
||||
extraLdapFlagsFunc(c, set, f)
|
||||
|
||||
return set
|
||||
}
|
||||
|
||||
func (c *LdapCommand) Run(args []string) int {
|
||||
initLdapFlags()
|
||||
|
||||
switch c.Func {
|
||||
case "":
|
||||
return cli.RunResultHelp
|
||||
|
||||
}
|
||||
|
||||
c.plural = "ldap-type account"
|
||||
switch c.Func {
|
||||
case "list":
|
||||
c.plural = "ldap-type accounts"
|
||||
}
|
||||
|
||||
f := c.Flags()
|
||||
|
||||
if err := f.Parse(args); err != nil {
|
||||
c.PrintCliError(err)
|
||||
return base.CommandUserError
|
||||
}
|
||||
|
||||
if strutil.StrListContains(flagsLdapMap[c.Func], "id") && c.FlagId == "" {
|
||||
c.PrintCliError(errors.New("ID is required but not passed in via -id"))
|
||||
return base.CommandUserError
|
||||
}
|
||||
|
||||
var opts []accounts.Option
|
||||
|
||||
if strutil.StrListContains(flagsLdapMap[c.Func], "auth-method-id") {
|
||||
switch c.Func {
|
||||
|
||||
case "create":
|
||||
if c.FlagAuthMethodId == "" {
|
||||
c.PrintCliError(errors.New("AuthMethod ID must be passed in via -auth-method-id or BOUNDARY_AUTH_METHOD_ID"))
|
||||
return base.CommandUserError
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
client, err := c.Client()
|
||||
if c.WrapperCleanupFunc != nil {
|
||||
defer func() {
|
||||
if err := c.WrapperCleanupFunc(); err != nil {
|
||||
c.PrintCliError(fmt.Errorf("Error cleaning kms wrapper: %w", err))
|
||||
}
|
||||
}()
|
||||
}
|
||||
if err != nil {
|
||||
c.PrintCliError(fmt.Errorf("Error creating API client: %w", err))
|
||||
return base.CommandCliError
|
||||
}
|
||||
accountsClient := accounts.NewClient(client)
|
||||
|
||||
switch c.FlagName {
|
||||
case "":
|
||||
case "null":
|
||||
opts = append(opts, accounts.DefaultName())
|
||||
default:
|
||||
opts = append(opts, accounts.WithName(c.FlagName))
|
||||
}
|
||||
|
||||
switch c.FlagDescription {
|
||||
case "":
|
||||
case "null":
|
||||
opts = append(opts, accounts.DefaultDescription())
|
||||
default:
|
||||
opts = append(opts, accounts.WithDescription(c.FlagDescription))
|
||||
}
|
||||
|
||||
if c.FlagFilter != "" {
|
||||
opts = append(opts, accounts.WithFilter(c.FlagFilter))
|
||||
}
|
||||
|
||||
var version uint32
|
||||
|
||||
switch c.Func {
|
||||
|
||||
case "update":
|
||||
switch c.FlagVersion {
|
||||
case 0:
|
||||
opts = append(opts, accounts.WithAutomaticVersioning(true))
|
||||
default:
|
||||
version = uint32(c.FlagVersion)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ok := extraLdapFlagsHandlingFunc(c, f, &opts); !ok {
|
||||
return base.CommandUserError
|
||||
}
|
||||
|
||||
var resp *api.Response
|
||||
var item *accounts.Account
|
||||
|
||||
var createResult *accounts.AccountCreateResult
|
||||
|
||||
var updateResult *accounts.AccountUpdateResult
|
||||
|
||||
switch c.Func {
|
||||
|
||||
case "create":
|
||||
createResult, err = accountsClient.Create(c.Context, c.FlagAuthMethodId, opts...)
|
||||
if exitCode := c.checkFuncError(err); exitCode > 0 {
|
||||
return exitCode
|
||||
}
|
||||
resp = createResult.GetResponse()
|
||||
item = createResult.GetItem()
|
||||
|
||||
case "update":
|
||||
updateResult, err = accountsClient.Update(c.Context, c.FlagId, version, opts...)
|
||||
if exitCode := c.checkFuncError(err); exitCode > 0 {
|
||||
return exitCode
|
||||
}
|
||||
resp = updateResult.GetResponse()
|
||||
item = updateResult.GetItem()
|
||||
|
||||
}
|
||||
|
||||
resp, item, err = executeExtraLdapActions(c, resp, item, err, accountsClient, version, opts)
|
||||
if exitCode := c.checkFuncError(err); exitCode > 0 {
|
||||
return exitCode
|
||||
}
|
||||
|
||||
output, err := printCustomLdapActionOutput(c)
|
||||
if err != nil {
|
||||
c.PrintCliError(err)
|
||||
return base.CommandUserError
|
||||
}
|
||||
if output {
|
||||
return base.CommandSuccess
|
||||
}
|
||||
|
||||
switch c.Func {
|
||||
|
||||
}
|
||||
|
||||
switch base.Format(c.UI) {
|
||||
case "table":
|
||||
c.UI.Output(printItemTable(item, resp))
|
||||
|
||||
case "json":
|
||||
if ok := c.PrintJsonItem(resp); !ok {
|
||||
return base.CommandCliError
|
||||
}
|
||||
}
|
||||
|
||||
return base.CommandSuccess
|
||||
}
|
||||
|
||||
func (c *LdapCommand) checkFuncError(err error) int {
|
||||
if err == nil {
|
||||
return 0
|
||||
}
|
||||
if apiErr := api.AsServerError(err); apiErr != nil {
|
||||
c.PrintApiError(apiErr, fmt.Sprintf("Error from controller when performing %s on %s", c.Func, c.plural))
|
||||
return base.CommandApiError
|
||||
}
|
||||
c.PrintCliError(fmt.Errorf("Error trying to %s %s: %s", c.Func, c.plural, err.Error()))
|
||||
return base.CommandCliError
|
||||
}
|
||||
|
||||
var (
|
||||
extraLdapActionsFlagsMapFunc = func() map[string][]string { return nil }
|
||||
extraLdapSynopsisFunc = func(*LdapCommand) string { return "" }
|
||||
extraLdapFlagsFunc = func(*LdapCommand, *base.FlagSets, *base.FlagSet) {}
|
||||
extraLdapFlagsHandlingFunc = func(*LdapCommand, *base.FlagSets, *[]accounts.Option) bool { return true }
|
||||
executeExtraLdapActions = func(_ *LdapCommand, inResp *api.Response, inItem *accounts.Account, inErr error, _ *accounts.Client, _ uint32, _ []accounts.Option) (*api.Response, *accounts.Account, error) {
|
||||
return inResp, inItem, inErr
|
||||
}
|
||||
printCustomLdapActionOutput = func(*LdapCommand) (bool, error) { return false, nil }
|
||||
)
|
||||
@@ -0,0 +1,89 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package accountscmd
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/boundary/api/accounts"
|
||||
"github.com/hashicorp/boundary/internal/cmd/base"
|
||||
)
|
||||
|
||||
const (
|
||||
loginNameFlagName = "login-name"
|
||||
)
|
||||
|
||||
func init() {
|
||||
extraLdapActionsFlagsMapFunc = extraLdapActionsFlagsMapFuncImpl
|
||||
extraLdapFlagsFunc = extraLdapFlagsFuncImpl
|
||||
extraLdapFlagsHandlingFunc = extraLdapFlagsHandlingFuncImpl
|
||||
}
|
||||
|
||||
func extraLdapActionsFlagsMapFuncImpl() map[string][]string {
|
||||
return map[string][]string{
|
||||
"create": {loginNameFlagName},
|
||||
}
|
||||
}
|
||||
|
||||
type extraLdapCmdVars struct {
|
||||
flagLoginName string
|
||||
}
|
||||
|
||||
func (c *LdapCommand) extraLdapHelpFunc(helpMap map[string]func() string) string {
|
||||
var helpStr string
|
||||
switch c.Func {
|
||||
case "create":
|
||||
helpStr = base.WrapForHelpText([]string{
|
||||
"Usage: boundary accounts create ldap [options] [args]",
|
||||
"",
|
||||
" Create an ldap-type account. Example:",
|
||||
"",
|
||||
` $ boundary accounts create ldap -login-name prodops -description "ldap account for ProdOps"`,
|
||||
"",
|
||||
"",
|
||||
})
|
||||
|
||||
case "update":
|
||||
helpStr = base.WrapForHelpText([]string{
|
||||
"Usage: boundary accounts update ldap [options] [args]",
|
||||
"",
|
||||
" Update an ldap-type account given its ID. Example:",
|
||||
"",
|
||||
` $ boundary accounts update ldap -id acctldap_1234567890 -name "devops" -description "ldap account for DevOps"`,
|
||||
"",
|
||||
"",
|
||||
})
|
||||
}
|
||||
return helpStr + c.Flags().Help()
|
||||
}
|
||||
|
||||
func extraLdapFlagsFuncImpl(c *LdapCommand, set *base.FlagSets, _ *base.FlagSet) {
|
||||
f := set.NewFlagSet("Ldap Account Options")
|
||||
|
||||
for _, name := range flagsLdapMap[c.Func] {
|
||||
switch name {
|
||||
case loginNameFlagName:
|
||||
f.StringVar(&base.StringVar{
|
||||
Name: loginNameFlagName,
|
||||
Target: &c.flagLoginName,
|
||||
Usage: "The login name for the account.",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func extraLdapFlagsHandlingFuncImpl(c *LdapCommand, _ *base.FlagSets, opts *[]accounts.Option) bool {
|
||||
switch c.flagLoginName {
|
||||
case "null", "":
|
||||
if c.Func == "create" {
|
||||
c.UI.Error("Login-name must be passed in via -login-name")
|
||||
return false
|
||||
}
|
||||
default:
|
||||
if c.Func != "create" {
|
||||
c.UI.Error("-login-name can only be set when creating an ldap account")
|
||||
return false
|
||||
}
|
||||
*opts = append(*opts, accounts.WithLdapAccountLoginName(c.flagLoginName))
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -49,6 +49,10 @@ func (c *Command) Help() string {
|
||||
"",
|
||||
" $ boundary authenticate oidc -auth-method-id amoidc_1234567890",
|
||||
"",
|
||||
" Authenticate with an LDAP auth method:",
|
||||
"",
|
||||
" $ boundary authenticate ldap -auth-method-id amldap_1234567890",
|
||||
"",
|
||||
" Please see the auth method subcommand help for detailed usage information.",
|
||||
}) + c.Flags().Help()
|
||||
}
|
||||
@@ -107,8 +111,12 @@ func (c *Command) Run(args []string) int {
|
||||
cmd := OidcCommand{Command: c.Command, Opts: []common.Option{common.WithSkipScopeIdFlag(true)}}
|
||||
cmd.Run([]string{})
|
||||
|
||||
case strings.HasPrefix(c.FlagAuthMethodId, globals.LdapAuthMethodPrefix):
|
||||
cmd := LdapCommand{Command: c.Command, Opts: []common.Option{common.WithSkipScopeIdFlag(true)}}
|
||||
cmd.Run([]string{})
|
||||
|
||||
default:
|
||||
c.PrintCliError(fmt.Errorf("The primary auth method was of an unsupported type. The given ID was %s; only 'ampw' (password) and 'amoidc' (OIDC) auth method prefixes are supported.", c.FlagAuthMethodId))
|
||||
c.PrintCliError(fmt.Errorf("The primary auth method was of an unsupported type. The given ID was %s; only 'ampw' (password), 'amoidc' (OIDC) and 'amldap' (LDAP) auth method prefixes are supported.", c.FlagAuthMethodId))
|
||||
return cli.RunResultHelp
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package authenticate
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/boundary/api"
|
||||
"github.com/hashicorp/boundary/api/authmethods"
|
||||
"github.com/hashicorp/boundary/globals"
|
||||
"github.com/hashicorp/boundary/internal/cmd/base"
|
||||
"github.com/hashicorp/boundary/internal/cmd/common"
|
||||
"github.com/hashicorp/boundary/internal/types/scope"
|
||||
"github.com/hashicorp/go-secure-stdlib/parseutil"
|
||||
"github.com/hashicorp/go-secure-stdlib/password"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/mitchellh/go-wordwrap"
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
|
||||
var (
|
||||
_ cli.Command = (*PasswordCommand)(nil)
|
||||
_ cli.CommandAutocomplete = (*PasswordCommand)(nil)
|
||||
)
|
||||
|
||||
type LdapCommand struct {
|
||||
*base.Command
|
||||
|
||||
flagLoginName string
|
||||
flagPassword string
|
||||
Opts []common.Option
|
||||
parsedOpts *common.Options
|
||||
}
|
||||
|
||||
func (c *LdapCommand) Synopsis() string {
|
||||
return wordwrap.WrapString("Invoke the ldap auth method to authenticate with Boundary", base.TermWidth)
|
||||
}
|
||||
|
||||
func (c *LdapCommand) Help() string {
|
||||
return base.WrapForHelpText([]string{
|
||||
"Usage: boundary authenticate ldap [options] [args]",
|
||||
"",
|
||||
" Invoke the ldap auth method to authenticate the Boundary CLI. Example:",
|
||||
"",
|
||||
` $ boundary authenticate ldap -auth-method-id amldap_1234567890 -login-name foo`,
|
||||
"",
|
||||
"",
|
||||
}) + c.Flags().Help()
|
||||
}
|
||||
|
||||
func (c *LdapCommand) Flags() *base.FlagSets {
|
||||
set := c.FlagSet(base.FlagSetHTTP | base.FlagSetClient | base.FlagSetOutputFormat)
|
||||
f := set.NewFlagSet("Command Options")
|
||||
|
||||
f.StringVar(&base.StringVar{
|
||||
Name: "login-name",
|
||||
Target: &c.flagLoginName,
|
||||
EnvVar: envLoginName,
|
||||
Usage: "The login name corresponding to an account within the given auth method.",
|
||||
})
|
||||
|
||||
f.StringVar(&base.StringVar{
|
||||
Name: "password",
|
||||
Target: &c.flagPassword,
|
||||
EnvVar: envPassword,
|
||||
Usage: "The password associated with the login name. If blank, the command will prompt for the password to be entered interactively in a non-echoing way. Otherwise, this can refer to a file on disk (file://) from which a password will be read or an env var (env://) from which the password will be read.",
|
||||
})
|
||||
|
||||
f.StringVar(&base.StringVar{
|
||||
Name: "auth-method-id",
|
||||
EnvVar: "BOUNDARY_AUTH_METHOD_ID",
|
||||
Target: &c.FlagAuthMethodId,
|
||||
Usage: "The auth-method resource to use for the operation.",
|
||||
})
|
||||
|
||||
if c.parsedOpts == nil || !c.parsedOpts.WithSkipScopeIdFlag {
|
||||
f.StringVar(&base.StringVar{
|
||||
Name: "scope-id",
|
||||
EnvVar: "BOUNDARY_SCOPE_ID",
|
||||
Target: &c.FlagScopeId,
|
||||
Usage: "The scope ID to use for the operation.",
|
||||
})
|
||||
}
|
||||
|
||||
return set
|
||||
}
|
||||
|
||||
func (c *LdapCommand) AutocompleteArgs() complete.Predictor {
|
||||
return complete.PredictAnything
|
||||
}
|
||||
|
||||
func (c *LdapCommand) AutocompleteFlags() complete.Flags {
|
||||
return c.Flags().Completions()
|
||||
}
|
||||
|
||||
func (c *LdapCommand) Run(args []string) int {
|
||||
opts, err := common.GetOpts(c.Opts...)
|
||||
if err != nil {
|
||||
c.PrintCliError(err)
|
||||
return base.CommandCliError
|
||||
}
|
||||
c.parsedOpts = opts
|
||||
|
||||
f := c.Flags()
|
||||
if err := f.Parse(args); err != nil {
|
||||
c.PrintCliError(err)
|
||||
return base.CommandUserError
|
||||
}
|
||||
|
||||
switch c.flagLoginName {
|
||||
case "":
|
||||
var input string
|
||||
fmt.Print("Please enter the login name: ")
|
||||
_, err := fmt.Scanln(&input)
|
||||
if err != nil {
|
||||
c.UI.Error(fmt.Sprintf("An error occurred attempting to read the login name. The raw error message is shown below but usually this is because you attempted to pipe a value into the command or you are executing outside of a terminal (TTY). The raw error was:\n\n%s", err.Error()))
|
||||
return base.CommandUserError
|
||||
}
|
||||
c.flagLoginName = strings.TrimSpace(input)
|
||||
}
|
||||
|
||||
switch c.flagPassword {
|
||||
case "":
|
||||
fmt.Print("Please enter the password (it will be hidden): ")
|
||||
value, err := password.Read(os.Stdin)
|
||||
fmt.Print("\n")
|
||||
if err != nil {
|
||||
c.UI.Error(fmt.Sprintf("An error occurred attempting to read the password. The raw error message is shown below but usually this is because you attempted to pipe a value into the command or you are executing outside of a terminal (TTY). The raw error was:\n\n%s", err.Error()))
|
||||
return base.CommandUserError
|
||||
}
|
||||
c.flagPassword = strings.TrimSpace(value)
|
||||
|
||||
default:
|
||||
password, err := parseutil.MustParsePath(c.flagPassword)
|
||||
switch {
|
||||
case err == nil:
|
||||
case errors.Is(err, parseutil.ErrNotParsed):
|
||||
c.UI.Error("Password flag must be used with env:// or file:// syntax or left empty for an interactive prompt")
|
||||
return base.CommandUserError
|
||||
default:
|
||||
c.UI.Error(fmt.Sprintf("Error parsing password flag: %v", err))
|
||||
return base.CommandUserError
|
||||
}
|
||||
c.flagPassword = password
|
||||
}
|
||||
|
||||
client, err := c.Client(base.WithNoTokenScope(), base.WithNoTokenValue())
|
||||
if c.WrapperCleanupFunc != nil {
|
||||
defer func() {
|
||||
if err := c.WrapperCleanupFunc(); err != nil {
|
||||
c.PrintCliError(fmt.Errorf("Error cleaning kms wrapper: %w", err))
|
||||
}
|
||||
}()
|
||||
}
|
||||
if err != nil {
|
||||
c.PrintCliError(fmt.Errorf("Error creating API client: %w", err))
|
||||
return base.CommandCliError
|
||||
}
|
||||
|
||||
aClient := authmethods.NewClient(client)
|
||||
|
||||
// if auth method ID isn't passed on the CLI, try looking up the primary auth method ID
|
||||
if c.FlagAuthMethodId == "" {
|
||||
// if flag for scope is empty try looking up global
|
||||
if c.FlagScopeId == "" {
|
||||
c.FlagScopeId = scope.Global.String()
|
||||
}
|
||||
|
||||
pri, err := getPrimaryAuthMethodId(c.Context, aClient, c.FlagScopeId, globals.LdapAuthMethodPrefix)
|
||||
if err != nil {
|
||||
c.PrintCliError(err)
|
||||
return base.CommandUserError
|
||||
}
|
||||
|
||||
c.FlagAuthMethodId = pri
|
||||
}
|
||||
|
||||
result, err := aClient.Authenticate(c.Context, c.FlagAuthMethodId, "login",
|
||||
map[string]any{
|
||||
"login_name": c.flagLoginName,
|
||||
"password": c.flagPassword,
|
||||
})
|
||||
if err != nil {
|
||||
if apiErr := api.AsServerError(err); apiErr != nil {
|
||||
c.PrintApiError(apiErr, "Error from controller when performing authentication")
|
||||
return base.CommandApiError
|
||||
}
|
||||
c.PrintCliError(fmt.Errorf("Error trying to perform authentication: %w", err))
|
||||
return base.CommandCliError
|
||||
}
|
||||
|
||||
return saveAndOrPrintToken(c.Command, result)
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
// Code generated by "make cli"; DO NOT EDIT.
|
||||
package authmethodscmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/boundary/api"
|
||||
"github.com/hashicorp/boundary/api/authmethods"
|
||||
"github.com/hashicorp/boundary/internal/cmd/base"
|
||||
"github.com/hashicorp/boundary/internal/cmd/common"
|
||||
"github.com/hashicorp/go-secure-stdlib/strutil"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
|
||||
func initLdapFlags() {
|
||||
flagsOnce.Do(func() {
|
||||
extraFlags := extraLdapActionsFlagsMapFunc()
|
||||
for k, v := range extraFlags {
|
||||
flagsLdapMap[k] = append(flagsLdapMap[k], v...)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var (
|
||||
_ cli.Command = (*LdapCommand)(nil)
|
||||
_ cli.CommandAutocomplete = (*LdapCommand)(nil)
|
||||
)
|
||||
|
||||
type LdapCommand struct {
|
||||
*base.Command
|
||||
|
||||
Func string
|
||||
|
||||
plural string
|
||||
|
||||
extraLdapCmdVars
|
||||
}
|
||||
|
||||
func (c *LdapCommand) AutocompleteArgs() complete.Predictor {
|
||||
initLdapFlags()
|
||||
return complete.PredictAnything
|
||||
}
|
||||
|
||||
func (c *LdapCommand) AutocompleteFlags() complete.Flags {
|
||||
initLdapFlags()
|
||||
return c.Flags().Completions()
|
||||
}
|
||||
|
||||
func (c *LdapCommand) Synopsis() string {
|
||||
if extra := extraLdapSynopsisFunc(c); extra != "" {
|
||||
return extra
|
||||
}
|
||||
|
||||
synopsisStr := "auth method"
|
||||
|
||||
synopsisStr = fmt.Sprintf("%s %s", "ldap-type", synopsisStr)
|
||||
|
||||
return common.SynopsisFunc(c.Func, synopsisStr)
|
||||
}
|
||||
|
||||
func (c *LdapCommand) Help() string {
|
||||
initLdapFlags()
|
||||
|
||||
var helpStr string
|
||||
helpMap := common.HelpMap("auth method")
|
||||
|
||||
switch c.Func {
|
||||
|
||||
default:
|
||||
|
||||
helpStr = c.extraLdapHelpFunc(helpMap)
|
||||
|
||||
}
|
||||
|
||||
// Keep linter from complaining if we don't actually generate code using it
|
||||
_ = helpMap
|
||||
return helpStr
|
||||
}
|
||||
|
||||
var flagsLdapMap = map[string][]string{
|
||||
|
||||
"create": {"scope-id", "name", "description"},
|
||||
|
||||
"update": {"id", "name", "description", "version"},
|
||||
}
|
||||
|
||||
func (c *LdapCommand) Flags() *base.FlagSets {
|
||||
if len(flagsLdapMap[c.Func]) == 0 {
|
||||
return c.FlagSet(base.FlagSetNone)
|
||||
}
|
||||
|
||||
set := c.FlagSet(base.FlagSetHTTP | base.FlagSetClient | base.FlagSetOutputFormat)
|
||||
f := set.NewFlagSet("Command Options")
|
||||
common.PopulateCommonFlags(c.Command, f, "ldap-type auth method", flagsLdapMap, c.Func)
|
||||
|
||||
extraLdapFlagsFunc(c, set, f)
|
||||
|
||||
return set
|
||||
}
|
||||
|
||||
func (c *LdapCommand) Run(args []string) int {
|
||||
initLdapFlags()
|
||||
|
||||
switch c.Func {
|
||||
case "":
|
||||
return cli.RunResultHelp
|
||||
|
||||
}
|
||||
|
||||
c.plural = "ldap-type auth method"
|
||||
switch c.Func {
|
||||
case "list":
|
||||
c.plural = "ldap-type auth methods"
|
||||
}
|
||||
|
||||
f := c.Flags()
|
||||
|
||||
if err := f.Parse(args); err != nil {
|
||||
c.PrintCliError(err)
|
||||
return base.CommandUserError
|
||||
}
|
||||
|
||||
if strutil.StrListContains(flagsLdapMap[c.Func], "id") && c.FlagId == "" {
|
||||
c.PrintCliError(errors.New("ID is required but not passed in via -id"))
|
||||
return base.CommandUserError
|
||||
}
|
||||
|
||||
var opts []authmethods.Option
|
||||
|
||||
if strutil.StrListContains(flagsLdapMap[c.Func], "scope-id") {
|
||||
switch c.Func {
|
||||
|
||||
case "create":
|
||||
if c.FlagScopeId == "" {
|
||||
c.PrintCliError(errors.New("Scope ID must be passed in via -scope-id or BOUNDARY_SCOPE_ID"))
|
||||
return base.CommandUserError
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
client, err := c.Client()
|
||||
if c.WrapperCleanupFunc != nil {
|
||||
defer func() {
|
||||
if err := c.WrapperCleanupFunc(); err != nil {
|
||||
c.PrintCliError(fmt.Errorf("Error cleaning kms wrapper: %w", err))
|
||||
}
|
||||
}()
|
||||
}
|
||||
if err != nil {
|
||||
c.PrintCliError(fmt.Errorf("Error creating API client: %w", err))
|
||||
return base.CommandCliError
|
||||
}
|
||||
authmethodsClient := authmethods.NewClient(client)
|
||||
|
||||
switch c.FlagName {
|
||||
case "":
|
||||
case "null":
|
||||
opts = append(opts, authmethods.DefaultName())
|
||||
default:
|
||||
opts = append(opts, authmethods.WithName(c.FlagName))
|
||||
}
|
||||
|
||||
switch c.FlagDescription {
|
||||
case "":
|
||||
case "null":
|
||||
opts = append(opts, authmethods.DefaultDescription())
|
||||
default:
|
||||
opts = append(opts, authmethods.WithDescription(c.FlagDescription))
|
||||
}
|
||||
|
||||
switch c.FlagRecursive {
|
||||
case true:
|
||||
opts = append(opts, authmethods.WithRecursive(true))
|
||||
}
|
||||
|
||||
if c.FlagFilter != "" {
|
||||
opts = append(opts, authmethods.WithFilter(c.FlagFilter))
|
||||
}
|
||||
|
||||
var version uint32
|
||||
|
||||
switch c.Func {
|
||||
|
||||
case "update":
|
||||
switch c.FlagVersion {
|
||||
case 0:
|
||||
opts = append(opts, authmethods.WithAutomaticVersioning(true))
|
||||
default:
|
||||
version = uint32(c.FlagVersion)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ok := extraLdapFlagsHandlingFunc(c, f, &opts); !ok {
|
||||
return base.CommandUserError
|
||||
}
|
||||
|
||||
var resp *api.Response
|
||||
var item *authmethods.AuthMethod
|
||||
|
||||
var createResult *authmethods.AuthMethodCreateResult
|
||||
|
||||
var updateResult *authmethods.AuthMethodUpdateResult
|
||||
|
||||
switch c.Func {
|
||||
|
||||
case "create":
|
||||
createResult, err = authmethodsClient.Create(c.Context, "ldap", c.FlagScopeId, opts...)
|
||||
if exitCode := c.checkFuncError(err); exitCode > 0 {
|
||||
return exitCode
|
||||
}
|
||||
resp = createResult.GetResponse()
|
||||
item = createResult.GetItem()
|
||||
|
||||
case "update":
|
||||
updateResult, err = authmethodsClient.Update(c.Context, c.FlagId, version, opts...)
|
||||
if exitCode := c.checkFuncError(err); exitCode > 0 {
|
||||
return exitCode
|
||||
}
|
||||
resp = updateResult.GetResponse()
|
||||
item = updateResult.GetItem()
|
||||
|
||||
}
|
||||
|
||||
resp, item, err = executeExtraLdapActions(c, resp, item, err, authmethodsClient, version, opts)
|
||||
if exitCode := c.checkFuncError(err); exitCode > 0 {
|
||||
return exitCode
|
||||
}
|
||||
|
||||
output, err := printCustomLdapActionOutput(c)
|
||||
if err != nil {
|
||||
c.PrintCliError(err)
|
||||
return base.CommandUserError
|
||||
}
|
||||
if output {
|
||||
return base.CommandSuccess
|
||||
}
|
||||
|
||||
switch c.Func {
|
||||
|
||||
}
|
||||
|
||||
switch base.Format(c.UI) {
|
||||
case "table":
|
||||
c.UI.Output(printItemTable(item, resp))
|
||||
|
||||
case "json":
|
||||
if ok := c.PrintJsonItem(resp); !ok {
|
||||
return base.CommandCliError
|
||||
}
|
||||
}
|
||||
|
||||
return base.CommandSuccess
|
||||
}
|
||||
|
||||
func (c *LdapCommand) checkFuncError(err error) int {
|
||||
if err == nil {
|
||||
return 0
|
||||
}
|
||||
if apiErr := api.AsServerError(err); apiErr != nil {
|
||||
c.PrintApiError(apiErr, fmt.Sprintf("Error from controller when performing %s on %s", c.Func, c.plural))
|
||||
return base.CommandApiError
|
||||
}
|
||||
c.PrintCliError(fmt.Errorf("Error trying to %s %s: %s", c.Func, c.plural, err.Error()))
|
||||
return base.CommandCliError
|
||||
}
|
||||
|
||||
var (
|
||||
extraLdapActionsFlagsMapFunc = func() map[string][]string { return nil }
|
||||
extraLdapSynopsisFunc = func(*LdapCommand) string { return "" }
|
||||
extraLdapFlagsFunc = func(*LdapCommand, *base.FlagSets, *base.FlagSet) {}
|
||||
extraLdapFlagsHandlingFunc = func(*LdapCommand, *base.FlagSets, *[]authmethods.Option) bool { return true }
|
||||
executeExtraLdapActions = func(_ *LdapCommand, inResp *api.Response, inItem *authmethods.AuthMethod, inErr error, _ *authmethods.Client, _ uint32, _ []authmethods.Option) (*api.Response, *authmethods.AuthMethod, error) {
|
||||
return inResp, inItem, inErr
|
||||
}
|
||||
printCustomLdapActionOutput = func(*LdapCommand) (bool, error) { return false, nil }
|
||||
)
|
||||
@@ -0,0 +1,510 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package authmethodscmd
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/hashicorp/boundary/api/authmethods"
|
||||
"github.com/hashicorp/boundary/internal/cmd/base"
|
||||
"github.com/hashicorp/go-secure-stdlib/parseutil"
|
||||
)
|
||||
|
||||
func init() {
|
||||
extraLdapActionsFlagsMapFunc = extraLdapActionsFlagsMapFuncImpl
|
||||
extraLdapFlagsFunc = extraLdapFlagsFuncImpl
|
||||
extraLdapFlagsHandlingFunc = extraLdapFlagHandlingFuncImpl
|
||||
}
|
||||
|
||||
type extraLdapCmdVars struct {
|
||||
flagState string
|
||||
flagUrls []string
|
||||
flagInsecureTls bool
|
||||
flagDiscoverDn bool
|
||||
flagAnonGroupSearch bool
|
||||
flagUpnDomain string
|
||||
flagStartTls bool
|
||||
flagUserDn string
|
||||
flagUserAttr string
|
||||
flagUserFilter string
|
||||
flagEnableGroups bool
|
||||
flagGroupDn string
|
||||
flagGroupAttr string
|
||||
flagGroupFilter string
|
||||
flagCertificates []string
|
||||
flagClientCertificate string
|
||||
flagClientCertificateKey string
|
||||
flagBindDn string
|
||||
flagBindPassword string
|
||||
flagUseTokenGroups bool
|
||||
flagAccountAttributeMaps []string
|
||||
}
|
||||
|
||||
const (
|
||||
urlsFlagName = "urls"
|
||||
insecureTlsFlagName = "insecure-tls"
|
||||
discoverDnFlagName = "discover-dn"
|
||||
anonGroupSearchFlagName = "anon-group-search"
|
||||
upnDomainFlagName = "upn-domain"
|
||||
startTlsFlagName = "start-tls"
|
||||
userDnFlagName = "user-dn"
|
||||
userAttrFlagName = "user-attr"
|
||||
userFilterFlagName = "user-filter"
|
||||
enableGroupsFlagName = "enable-groups"
|
||||
groupDnFlagName = "group-dn"
|
||||
groupAttrFlagName = "group-attr"
|
||||
groupFilterFlagName = "group-filter"
|
||||
certificatesFlagName = "certificate"
|
||||
clientCertificateFlagName = "client-certificate"
|
||||
clientCertificateKeyFlagName = "client-certificate-key"
|
||||
bindDnFlagName = "bind-dn"
|
||||
bindPasswordFlagName = "bind-password"
|
||||
useTokenGroupsFlagName = "use-token-groups"
|
||||
accountAttributeMaps = "account-attribute-map"
|
||||
)
|
||||
|
||||
func extraLdapActionsFlagsMapFuncImpl() map[string][]string {
|
||||
flags := map[string][]string{
|
||||
"create": {
|
||||
urlsFlagName,
|
||||
insecureTlsFlagName,
|
||||
discoverDnFlagName,
|
||||
anonGroupSearchFlagName,
|
||||
upnDomainFlagName,
|
||||
startTlsFlagName,
|
||||
userDnFlagName,
|
||||
userAttrFlagName,
|
||||
userFilterFlagName,
|
||||
enableGroupsFlagName,
|
||||
groupDnFlagName,
|
||||
groupAttrFlagName,
|
||||
groupFilterFlagName,
|
||||
certificatesFlagName,
|
||||
clientCertificateFlagName,
|
||||
clientCertificateKeyFlagName,
|
||||
bindDnFlagName,
|
||||
bindPasswordFlagName,
|
||||
useTokenGroupsFlagName,
|
||||
accountAttributeMaps,
|
||||
stateFlagName,
|
||||
},
|
||||
}
|
||||
flags["update"] = flags["create"]
|
||||
return flags
|
||||
}
|
||||
|
||||
func extraLdapFlagsFuncImpl(c *LdapCommand, set *base.FlagSets, _ *base.FlagSet) {
|
||||
f := set.NewFlagSet("LDAP Auth Method Options")
|
||||
|
||||
for _, name := range flagsLdapMap[c.Func] {
|
||||
switch name {
|
||||
case urlsFlagName:
|
||||
f.StringSliceVar(&base.StringSliceVar{
|
||||
Name: urlsFlagName,
|
||||
Target: &c.flagUrls,
|
||||
Usage: "The LDAP URLs that specify LDAP servers to connect to (required). May be specified multiple times.",
|
||||
})
|
||||
case startTlsFlagName:
|
||||
f.BoolVar(&base.BoolVar{
|
||||
Name: startTlsFlagName,
|
||||
Target: &c.flagStartTls,
|
||||
Usage: "Issue StartTLS command after connecting (optional).",
|
||||
})
|
||||
case insecureTlsFlagName:
|
||||
f.BoolVar(&base.BoolVar{
|
||||
Name: insecureTlsFlagName,
|
||||
Target: &c.flagInsecureTls,
|
||||
Usage: "Skip the LDAP server SSL certificate validation (optional) - insecure and use with caution.",
|
||||
})
|
||||
case discoverDnFlagName:
|
||||
f.BoolVar(&base.BoolVar{
|
||||
Name: discoverDnFlagName,
|
||||
Target: &c.flagDiscoverDn,
|
||||
Usage: "Use anon bind to discover the bind DN of a user (optional).",
|
||||
})
|
||||
case anonGroupSearchFlagName:
|
||||
f.BoolVar(&base.BoolVar{
|
||||
Name: anonGroupSearchFlagName,
|
||||
Target: &c.flagAnonGroupSearch,
|
||||
Usage: "Use anon bind when performing LDAP group searches (optional).",
|
||||
})
|
||||
case upnDomainFlagName:
|
||||
f.StringVar(&base.StringVar{
|
||||
Name: upnDomainFlagName,
|
||||
Target: &c.flagUpnDomain,
|
||||
Usage: "The userPrincipalDomain used to construct the UPN string for the authenticating user (optional).",
|
||||
})
|
||||
case userDnFlagName:
|
||||
f.StringVar(&base.StringVar{
|
||||
Name: userDnFlagName,
|
||||
Target: &c.flagUserDn,
|
||||
Usage: "The base DN under which to perform user search (optional).",
|
||||
})
|
||||
case userAttrFlagName:
|
||||
f.StringVar(&base.StringVar{
|
||||
Name: userAttrFlagName,
|
||||
Target: &c.flagUserAttr,
|
||||
Usage: "The attribute on user entry matching the username passed when authenticating (optional).",
|
||||
})
|
||||
case userFilterFlagName:
|
||||
f.StringVar(&base.StringVar{
|
||||
Name: userFilterFlagName,
|
||||
Target: &c.flagUserFilter,
|
||||
Usage: "A go template used to construct a LDAP user search filter (optional).",
|
||||
})
|
||||
case enableGroupsFlagName:
|
||||
f.BoolVar(&base.BoolVar{
|
||||
Name: enableGroupsFlagName,
|
||||
Target: &c.flagEnableGroups,
|
||||
Usage: "Find the authenticated user's groups during authentication (optional).",
|
||||
})
|
||||
case groupDnFlagName:
|
||||
f.StringVar(&base.StringVar{
|
||||
Name: groupDnFlagName,
|
||||
Target: &c.flagGroupDn,
|
||||
Usage: "The base DN under which to perform group search.",
|
||||
})
|
||||
case groupAttrFlagName:
|
||||
f.StringVar(&base.StringVar{
|
||||
Name: groupAttrFlagName,
|
||||
Target: &c.flagGroupAttr,
|
||||
Usage: "The attribute that enumerates a user's group membership from entries returned by a group search (optional).",
|
||||
})
|
||||
case groupFilterFlagName:
|
||||
f.StringVar(&base.StringVar{
|
||||
Name: groupFilterFlagName,
|
||||
Target: &c.flagGroupFilter,
|
||||
Usage: "A go template used to construct a LDAP group search filter (optional).",
|
||||
})
|
||||
case certificatesFlagName:
|
||||
f.StringSliceVar(&base.StringSliceVar{
|
||||
Name: certificatesFlagName,
|
||||
Target: &c.flagCertificates,
|
||||
Usage: "PEM-encoded X.509 CA certificate in ASN.1 DER form that can be used as a trust anchor when connecting to an LDAP server(optional). This may be specified multiple times",
|
||||
})
|
||||
case clientCertificateFlagName:
|
||||
f.StringVar(&base.StringVar{
|
||||
Name: clientCertificateFlagName,
|
||||
Target: &c.flagClientCertificate,
|
||||
Usage: "PEM-encoded X.509 client certificate in ASN.1 DER form that can be used to authenticate against an LDAP server(optional).",
|
||||
})
|
||||
case clientCertificateKeyFlagName:
|
||||
f.StringVar(&base.StringVar{
|
||||
Name: clientCertificateKeyFlagName,
|
||||
Target: &c.flagClientCertificateKey,
|
||||
Usage: "PEM-encoded X.509 client certificate key in PKCS #8, ASN.1 DER form used with the client certificate (optional).",
|
||||
})
|
||||
case bindDnFlagName:
|
||||
f.StringVar(&base.StringVar{
|
||||
Name: bindDnFlagName,
|
||||
Target: &c.flagBindDn,
|
||||
Usage: "The distinguished name of entry to bind when performing user and group searches (optional).",
|
||||
})
|
||||
case bindPasswordFlagName:
|
||||
f.StringVar(&base.StringVar{
|
||||
Name: bindPasswordFlagName,
|
||||
Target: &c.flagBindPassword,
|
||||
Usage: "The password to use along with bind-dn performing user and group searches (optional).",
|
||||
})
|
||||
case useTokenGroupsFlagName:
|
||||
f.BoolVar(&base.BoolVar{
|
||||
Name: useTokenGroupsFlagName,
|
||||
Target: &c.flagUseTokenGroups,
|
||||
Usage: "Use the Active Directory tokenGroups constructed attribute of the user to find the group memberships (optional).",
|
||||
})
|
||||
case stateFlagName:
|
||||
f.StringVar(&base.StringVar{
|
||||
Name: stateFlagName,
|
||||
Target: &c.flagState,
|
||||
Usage: "The desired operational state of the auth method.",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *LdapCommand) extraLdapHelpFunc(helpMap map[string]func() string) string {
|
||||
var helpStr string
|
||||
switch c.Func {
|
||||
case "create":
|
||||
helpStr = base.WrapForHelpText([]string{
|
||||
"Usage: boundary auth-methods create ldap [options] [args]",
|
||||
"",
|
||||
" Create an ldap-type auth method. Example:",
|
||||
"",
|
||||
` $ boundary auth-methods create ldap -name prodops -description "LDAP auth-method for ProdOps"`,
|
||||
"",
|
||||
"",
|
||||
})
|
||||
|
||||
case "update":
|
||||
helpStr = base.WrapForHelpText([]string{
|
||||
"Usage: boundary auth-methods update ldap [options] [args]",
|
||||
"",
|
||||
" Update an ldap-type auth method given its ID. Example:",
|
||||
"",
|
||||
` $ boundary auth-methods update ldap -id amldap_1234567890 -name "devops" -description "LDAP auth-method for DevOps"`,
|
||||
"",
|
||||
"",
|
||||
})
|
||||
}
|
||||
return helpStr + c.Flags().Help()
|
||||
}
|
||||
|
||||
func extraLdapFlagHandlingFuncImpl(c *LdapCommand, _ *base.FlagSets, opts *[]authmethods.Option) bool {
|
||||
switch {
|
||||
case len(c.flagUrls) == 0:
|
||||
case len(c.flagUrls) == 1 && c.flagUrls[0] == "null":
|
||||
c.UI.Error(fmt.Sprintf("There must be at least one %q", urlsFlagName))
|
||||
return false
|
||||
default:
|
||||
for _, urlString := range c.flagUrls {
|
||||
u, err := url.Parse(urlString)
|
||||
if err != nil {
|
||||
c.UI.Error(fmt.Sprintf("Error parsing URL %q: %s", urlString, err))
|
||||
return false
|
||||
}
|
||||
switch u.Scheme {
|
||||
case "ldap", "ldaps":
|
||||
default:
|
||||
c.UI.Error(fmt.Sprintf("scheme in url %q is neither ldap nor ldaps", urlString))
|
||||
return false
|
||||
}
|
||||
}
|
||||
*opts = append(*opts, authmethods.WithLdapAuthMethodUrls(c.flagUrls))
|
||||
}
|
||||
|
||||
switch c.flagStartTls {
|
||||
case true:
|
||||
*opts = append(*opts, authmethods.WithLdapAuthMethodStartTls(true))
|
||||
default:
|
||||
*opts = append(*opts, authmethods.WithLdapAuthMethodStartTls(false))
|
||||
}
|
||||
|
||||
switch c.flagInsecureTls {
|
||||
case true:
|
||||
*opts = append(*opts, authmethods.WithLdapAuthMethodInsecureTls(true))
|
||||
default:
|
||||
*opts = append(*opts, authmethods.WithLdapAuthMethodInsecureTls(false))
|
||||
}
|
||||
|
||||
switch c.flagDiscoverDn {
|
||||
case true:
|
||||
*opts = append(*opts, authmethods.WithLdapAuthMethodDiscoverDn(true))
|
||||
default:
|
||||
*opts = append(*opts, authmethods.WithLdapAuthMethodDiscoverDn(false))
|
||||
}
|
||||
|
||||
switch c.flagAnonGroupSearch {
|
||||
case true:
|
||||
*opts = append(*opts, authmethods.WithLdapAuthMethodAnonGroupSearch(true))
|
||||
default:
|
||||
*opts = append(*opts, authmethods.WithLdapAuthMethodAnonGroupSearch(false))
|
||||
}
|
||||
|
||||
switch c.flagUpnDomain {
|
||||
case "":
|
||||
case "null":
|
||||
*opts = append(*opts, authmethods.DefaultLdapAuthMethodUpnDomain())
|
||||
default:
|
||||
*opts = append(*opts, authmethods.WithLdapAuthMethodUpnDomain(c.flagUpnDomain))
|
||||
}
|
||||
|
||||
switch c.flagUserDn {
|
||||
case "":
|
||||
case "null":
|
||||
*opts = append(*opts, authmethods.DefaultLdapAuthMethodUserDn())
|
||||
default:
|
||||
*opts = append(*opts, authmethods.WithLdapAuthMethodUserDn(c.flagUserDn))
|
||||
}
|
||||
|
||||
switch c.flagUserAttr {
|
||||
case "":
|
||||
case "null":
|
||||
*opts = append(*opts, authmethods.DefaultLdapAuthMethodUserAttr())
|
||||
default:
|
||||
*opts = append(*opts, authmethods.WithLdapAuthMethodUserAttr(c.flagUserAttr))
|
||||
}
|
||||
|
||||
switch c.flagUserFilter {
|
||||
case "":
|
||||
case "null":
|
||||
*opts = append(*opts, authmethods.DefaultLdapAuthMethodUserFilter())
|
||||
default:
|
||||
*opts = append(*opts, authmethods.WithLdapAuthMethodUserFilter(c.flagUserFilter))
|
||||
}
|
||||
|
||||
switch c.flagEnableGroups {
|
||||
case true:
|
||||
*opts = append(*opts, authmethods.WithLdapAuthMethodEnableGroups(true))
|
||||
default:
|
||||
*opts = append(*opts, authmethods.WithLdapAuthMethodEnableGroups(false))
|
||||
}
|
||||
|
||||
switch c.flagGroupDn {
|
||||
case "":
|
||||
case "null":
|
||||
*opts = append(*opts, authmethods.DefaultLdapAuthMethodGroupDn())
|
||||
default:
|
||||
*opts = append(*opts, authmethods.WithLdapAuthMethodGroupDn(c.flagGroupDn))
|
||||
}
|
||||
|
||||
switch c.flagGroupAttr {
|
||||
case "":
|
||||
case "null":
|
||||
*opts = append(*opts, authmethods.DefaultLdapAuthMethodGroupAttr())
|
||||
default:
|
||||
*opts = append(*opts, authmethods.WithLdapAuthMethodGroupAttr(c.flagGroupAttr))
|
||||
}
|
||||
|
||||
switch c.flagGroupFilter {
|
||||
case "":
|
||||
case "null":
|
||||
*opts = append(*opts, authmethods.DefaultLdapAuthMethodGroupFilter())
|
||||
default:
|
||||
*opts = append(*opts, authmethods.WithLdapAuthMethodGroupFilter(c.flagGroupFilter))
|
||||
}
|
||||
|
||||
switch {
|
||||
case len(c.flagCertificates) == 0:
|
||||
case len(c.flagCertificates) == 1 && c.flagCertificates[0] == "null":
|
||||
*opts = append(*opts, authmethods.DefaultLdapAuthMethodCertificates())
|
||||
default:
|
||||
pems := make([]string, 0, len(c.flagCertificates))
|
||||
for _, certFlag := range c.flagCertificates {
|
||||
p, err := parseutil.MustParsePath(certFlag)
|
||||
switch {
|
||||
case err == nil:
|
||||
if validationErr := validateCerts(p); validationErr != nil {
|
||||
c.UI.Error(fmt.Sprintf("invalid certificate in %q: %s", certFlag, validationErr.Error()))
|
||||
return false
|
||||
}
|
||||
pems = append(pems, p)
|
||||
case errors.Is(err, parseutil.ErrNotParsed):
|
||||
c.UI.Error("Certificate flag must be used with env:// or file:// syntax")
|
||||
return false
|
||||
default:
|
||||
c.UI.Error(fmt.Sprintf("Error parsing certificate flag: %v", err))
|
||||
return false
|
||||
}
|
||||
}
|
||||
*opts = append(*opts, authmethods.WithLdapAuthMethodCertificates(pems))
|
||||
}
|
||||
|
||||
switch c.flagClientCertificate {
|
||||
case "":
|
||||
case "null":
|
||||
*opts = append(*opts, authmethods.DefaultLdapAuthMethodClientCertificate())
|
||||
default:
|
||||
p, err := parseutil.MustParsePath(c.flagClientCertificate)
|
||||
switch {
|
||||
case err == nil:
|
||||
if validationErr := validateCerts(p); validationErr != nil {
|
||||
c.UI.Error(fmt.Sprintf("invalid client certificate in %q: %s", c.flagClientCertificate, validationErr.Error()))
|
||||
return false
|
||||
}
|
||||
*opts = append(*opts, authmethods.WithLdapAuthMethodClientCertificate(p))
|
||||
case errors.Is(err, parseutil.ErrNotParsed):
|
||||
c.UI.Error("Client certificate flag must be used with env:// or file:// syntax")
|
||||
return false
|
||||
default:
|
||||
c.UI.Error(fmt.Sprintf("Error parsing client certificate flag: %v", err))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
switch c.flagClientCertificateKey {
|
||||
case "":
|
||||
case "null":
|
||||
*opts = append(*opts, authmethods.DefaultLdapAuthMethodClientCertificateKey())
|
||||
default:
|
||||
key, err := parseutil.MustParsePath(c.flagClientCertificateKey)
|
||||
switch {
|
||||
case err == nil:
|
||||
*opts = append(*opts, authmethods.WithLdapAuthMethodClientCertificateKey(key))
|
||||
case errors.Is(err, parseutil.ErrNotParsed):
|
||||
c.UI.Error("Client certificate key flag must be used with env:// or file:// syntax")
|
||||
return false
|
||||
default:
|
||||
c.UI.Error(fmt.Sprintf("Error parsing client certificate key flag: %v", err))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
switch c.flagBindDn {
|
||||
case "":
|
||||
case "null":
|
||||
*opts = append(*opts, authmethods.DefaultLdapAuthMethodBindDn())
|
||||
default:
|
||||
*opts = append(*opts, authmethods.WithLdapAuthMethodBindDn(c.flagBindDn))
|
||||
}
|
||||
|
||||
switch c.flagBindPassword {
|
||||
case "":
|
||||
case "null":
|
||||
*opts = append(*opts, authmethods.DefaultLdapAuthMethodBindPassword())
|
||||
default:
|
||||
password, err := parseutil.MustParsePath(c.flagBindPassword)
|
||||
switch {
|
||||
case err == nil:
|
||||
*opts = append(*opts, authmethods.WithLdapAuthMethodBindPassword(password))
|
||||
case errors.Is(err, parseutil.ErrNotParsed):
|
||||
c.UI.Error("Bind password flag must be used with env:// or file:// syntax")
|
||||
return false
|
||||
default:
|
||||
c.UI.Error(fmt.Sprintf("Error parsing bind password flag: %v", err))
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
switch c.flagUseTokenGroups {
|
||||
case true:
|
||||
*opts = append(*opts, authmethods.WithLdapAuthMethodUseTokenGroups(true))
|
||||
default:
|
||||
*opts = append(*opts, authmethods.WithLdapAuthMethodUseTokenGroups(false))
|
||||
}
|
||||
|
||||
switch {
|
||||
case len(c.flagAccountAttributeMaps) == 0:
|
||||
case len(c.flagAccountAttributeMaps) == 1 && c.flagAccountAttributeMaps[0] == "null":
|
||||
*opts = append(*opts, authmethods.DefaultLdapAuthMethodAccountAttributeMaps())
|
||||
default:
|
||||
*opts = append(*opts, authmethods.WithLdapAuthMethodAccountAttributeMaps(c.flagAccountAttributeMaps))
|
||||
}
|
||||
|
||||
switch c.flagState {
|
||||
case "":
|
||||
// there is a default value during "create", so it's okay to not
|
||||
// specify a state
|
||||
case "null":
|
||||
c.UI.Error("State is required, you cannot set it to null")
|
||||
return false
|
||||
default:
|
||||
*opts = append(*opts, authmethods.WithLdapAuthMethodState(c.flagState))
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func validateCerts(pems ...string) error {
|
||||
if len(pems) == 0 {
|
||||
return errors.New("no PEMs provided")
|
||||
}
|
||||
for _, p := range pems {
|
||||
if p == "" {
|
||||
return errors.New("empty certificate PEM")
|
||||
}
|
||||
block, _ := pem.Decode([]byte(p))
|
||||
if block == nil {
|
||||
return errors.New("failed to parse certificate: invalid PEM encoding")
|
||||
}
|
||||
_, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse certificate: invalid block: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package managedgroupscmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/boundary/api/managedgroups"
|
||||
"github.com/hashicorp/boundary/internal/cmd/base"
|
||||
)
|
||||
|
||||
const (
|
||||
groupNamesFlagName = "group-names"
|
||||
)
|
||||
|
||||
type extraLdapCmdVars struct {
|
||||
flagGroupNames []string
|
||||
}
|
||||
|
||||
func init() {
|
||||
extraLdapFlagsFunc = extraLdapFlagsFuncImpl
|
||||
extraLdapActionsFlagsMapFunc = extraLdapActionsFlagsMapFuncImpl
|
||||
extraLdapFlagsHandlingFunc = extraLdapFlagsHandlingFuncImpl
|
||||
}
|
||||
|
||||
func extraLdapActionsFlagsMapFuncImpl() map[string][]string {
|
||||
return map[string][]string{
|
||||
"create": {groupNamesFlagName},
|
||||
"update": {groupNamesFlagName},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *LdapCommand) extraLdapHelpFunc(helpMap map[string]func() string) string {
|
||||
var helpStr string
|
||||
switch c.Func {
|
||||
case "create":
|
||||
helpStr = base.WrapForHelpText([]string{
|
||||
"Usage: boundary managed-groups create ldap [options] [args]",
|
||||
"",
|
||||
" Create a ldap-type managed group. Example:",
|
||||
"",
|
||||
` $ boundary managed-groups create ldap -group-names admin -description "Ldap managed group for ProdOps"`,
|
||||
"",
|
||||
"",
|
||||
})
|
||||
|
||||
case "update":
|
||||
helpStr = base.WrapForHelpText([]string{
|
||||
"Usage: boundary managed-groups update ldap [options] [args]",
|
||||
"",
|
||||
" Update an ldap-type managed group given its ID. Example:",
|
||||
"",
|
||||
` $ boundary managed-groups update ldap -id acctldap_1234567890 -name "devops" -description "Ldap managed group for DevOps"`,
|
||||
"",
|
||||
"",
|
||||
})
|
||||
}
|
||||
return helpStr + c.Flags().Help()
|
||||
}
|
||||
|
||||
func extraLdapFlagsFuncImpl(c *LdapCommand, _ *base.FlagSets, f *base.FlagSet) {
|
||||
for _, name := range flagsLdapMap[c.Func] {
|
||||
switch name {
|
||||
case groupNamesFlagName:
|
||||
f.StringSliceVar(&base.StringSliceVar{
|
||||
Name: groupNamesFlagName,
|
||||
Target: &c.flagGroupNames,
|
||||
Usage: "The LDAP group names against which an LDAP account's associated groups (discovered during login) will be evaluated to determine membership (required). May be specified multiple times",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func extraLdapFlagsHandlingFuncImpl(c *LdapCommand, _ *base.FlagSets, opts *[]managedgroups.Option) bool {
|
||||
switch {
|
||||
case len(c.flagGroupNames) == 0:
|
||||
case len(c.flagGroupNames) == 1 && c.flagGroupNames[0] == "null":
|
||||
c.UI.Error(fmt.Sprintf("There must be at least one %q", groupNamesFlagName))
|
||||
return false
|
||||
default:
|
||||
*opts = append(*opts, managedgroups.WithLdapManagedGroupGroupNames(c.flagGroupNames))
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
// Code generated by "make cli"; DO NOT EDIT.
|
||||
package managedgroupscmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/boundary/api"
|
||||
"github.com/hashicorp/boundary/api/managedgroups"
|
||||
"github.com/hashicorp/boundary/internal/cmd/base"
|
||||
"github.com/hashicorp/boundary/internal/cmd/common"
|
||||
"github.com/hashicorp/go-secure-stdlib/strutil"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/posener/complete"
|
||||
)
|
||||
|
||||
func initLdapFlags() {
|
||||
flagsOnce.Do(func() {
|
||||
extraFlags := extraLdapActionsFlagsMapFunc()
|
||||
for k, v := range extraFlags {
|
||||
flagsLdapMap[k] = append(flagsLdapMap[k], v...)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var (
|
||||
_ cli.Command = (*LdapCommand)(nil)
|
||||
_ cli.CommandAutocomplete = (*LdapCommand)(nil)
|
||||
)
|
||||
|
||||
type LdapCommand struct {
|
||||
*base.Command
|
||||
|
||||
Func string
|
||||
|
||||
plural string
|
||||
|
||||
extraLdapCmdVars
|
||||
}
|
||||
|
||||
func (c *LdapCommand) AutocompleteArgs() complete.Predictor {
|
||||
initLdapFlags()
|
||||
return complete.PredictAnything
|
||||
}
|
||||
|
||||
func (c *LdapCommand) AutocompleteFlags() complete.Flags {
|
||||
initLdapFlags()
|
||||
return c.Flags().Completions()
|
||||
}
|
||||
|
||||
func (c *LdapCommand) Synopsis() string {
|
||||
if extra := extraLdapSynopsisFunc(c); extra != "" {
|
||||
return extra
|
||||
}
|
||||
|
||||
synopsisStr := "managed group"
|
||||
|
||||
synopsisStr = fmt.Sprintf("%s %s", "ldap-type", synopsisStr)
|
||||
|
||||
return common.SynopsisFunc(c.Func, synopsisStr)
|
||||
}
|
||||
|
||||
func (c *LdapCommand) Help() string {
|
||||
initLdapFlags()
|
||||
|
||||
var helpStr string
|
||||
helpMap := common.HelpMap("managed group")
|
||||
|
||||
switch c.Func {
|
||||
|
||||
default:
|
||||
|
||||
helpStr = c.extraLdapHelpFunc(helpMap)
|
||||
|
||||
}
|
||||
|
||||
// Keep linter from complaining if we don't actually generate code using it
|
||||
_ = helpMap
|
||||
return helpStr
|
||||
}
|
||||
|
||||
var flagsLdapMap = map[string][]string{
|
||||
|
||||
"create": {"auth-method-id", "name", "description"},
|
||||
|
||||
"update": {"id", "name", "description", "version"},
|
||||
}
|
||||
|
||||
func (c *LdapCommand) Flags() *base.FlagSets {
|
||||
if len(flagsLdapMap[c.Func]) == 0 {
|
||||
return c.FlagSet(base.FlagSetNone)
|
||||
}
|
||||
|
||||
set := c.FlagSet(base.FlagSetHTTP | base.FlagSetClient | base.FlagSetOutputFormat)
|
||||
f := set.NewFlagSet("Command Options")
|
||||
common.PopulateCommonFlags(c.Command, f, "ldap-type managed group", flagsLdapMap, c.Func)
|
||||
|
||||
extraLdapFlagsFunc(c, set, f)
|
||||
|
||||
return set
|
||||
}
|
||||
|
||||
func (c *LdapCommand) Run(args []string) int {
|
||||
initLdapFlags()
|
||||
|
||||
switch c.Func {
|
||||
case "":
|
||||
return cli.RunResultHelp
|
||||
|
||||
}
|
||||
|
||||
c.plural = "ldap-type managed group"
|
||||
switch c.Func {
|
||||
case "list":
|
||||
c.plural = "ldap-type managed groups"
|
||||
}
|
||||
|
||||
f := c.Flags()
|
||||
|
||||
if err := f.Parse(args); err != nil {
|
||||
c.PrintCliError(err)
|
||||
return base.CommandUserError
|
||||
}
|
||||
|
||||
if strutil.StrListContains(flagsLdapMap[c.Func], "id") && c.FlagId == "" {
|
||||
c.PrintCliError(errors.New("ID is required but not passed in via -id"))
|
||||
return base.CommandUserError
|
||||
}
|
||||
|
||||
var opts []managedgroups.Option
|
||||
|
||||
if strutil.StrListContains(flagsLdapMap[c.Func], "auth-method-id") {
|
||||
switch c.Func {
|
||||
|
||||
case "create":
|
||||
if c.FlagAuthMethodId == "" {
|
||||
c.PrintCliError(errors.New("AuthMethod ID must be passed in via -auth-method-id or BOUNDARY_AUTH_METHOD_ID"))
|
||||
return base.CommandUserError
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
client, err := c.Client()
|
||||
if c.WrapperCleanupFunc != nil {
|
||||
defer func() {
|
||||
if err := c.WrapperCleanupFunc(); err != nil {
|
||||
c.PrintCliError(fmt.Errorf("Error cleaning kms wrapper: %w", err))
|
||||
}
|
||||
}()
|
||||
}
|
||||
if err != nil {
|
||||
c.PrintCliError(fmt.Errorf("Error creating API client: %w", err))
|
||||
return base.CommandCliError
|
||||
}
|
||||
managedgroupsClient := managedgroups.NewClient(client)
|
||||
|
||||
switch c.FlagName {
|
||||
case "":
|
||||
case "null":
|
||||
opts = append(opts, managedgroups.DefaultName())
|
||||
default:
|
||||
opts = append(opts, managedgroups.WithName(c.FlagName))
|
||||
}
|
||||
|
||||
switch c.FlagDescription {
|
||||
case "":
|
||||
case "null":
|
||||
opts = append(opts, managedgroups.DefaultDescription())
|
||||
default:
|
||||
opts = append(opts, managedgroups.WithDescription(c.FlagDescription))
|
||||
}
|
||||
|
||||
if c.FlagFilter != "" {
|
||||
opts = append(opts, managedgroups.WithFilter(c.FlagFilter))
|
||||
}
|
||||
|
||||
var version uint32
|
||||
|
||||
switch c.Func {
|
||||
|
||||
case "update":
|
||||
switch c.FlagVersion {
|
||||
case 0:
|
||||
opts = append(opts, managedgroups.WithAutomaticVersioning(true))
|
||||
default:
|
||||
version = uint32(c.FlagVersion)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if ok := extraLdapFlagsHandlingFunc(c, f, &opts); !ok {
|
||||
return base.CommandUserError
|
||||
}
|
||||
|
||||
var resp *api.Response
|
||||
var item *managedgroups.ManagedGroup
|
||||
|
||||
var createResult *managedgroups.ManagedGroupCreateResult
|
||||
|
||||
var updateResult *managedgroups.ManagedGroupUpdateResult
|
||||
|
||||
switch c.Func {
|
||||
|
||||
case "create":
|
||||
createResult, err = managedgroupsClient.Create(c.Context, c.FlagAuthMethodId, opts...)
|
||||
if exitCode := c.checkFuncError(err); exitCode > 0 {
|
||||
return exitCode
|
||||
}
|
||||
resp = createResult.GetResponse()
|
||||
item = createResult.GetItem()
|
||||
|
||||
case "update":
|
||||
updateResult, err = managedgroupsClient.Update(c.Context, c.FlagId, version, opts...)
|
||||
if exitCode := c.checkFuncError(err); exitCode > 0 {
|
||||
return exitCode
|
||||
}
|
||||
resp = updateResult.GetResponse()
|
||||
item = updateResult.GetItem()
|
||||
|
||||
}
|
||||
|
||||
resp, item, err = executeExtraLdapActions(c, resp, item, err, managedgroupsClient, version, opts)
|
||||
if exitCode := c.checkFuncError(err); exitCode > 0 {
|
||||
return exitCode
|
||||
}
|
||||
|
||||
output, err := printCustomLdapActionOutput(c)
|
||||
if err != nil {
|
||||
c.PrintCliError(err)
|
||||
return base.CommandUserError
|
||||
}
|
||||
if output {
|
||||
return base.CommandSuccess
|
||||
}
|
||||
|
||||
switch c.Func {
|
||||
|
||||
}
|
||||
|
||||
switch base.Format(c.UI) {
|
||||
case "table":
|
||||
c.UI.Output(printItemTable(item, resp))
|
||||
|
||||
case "json":
|
||||
if ok := c.PrintJsonItem(resp); !ok {
|
||||
return base.CommandCliError
|
||||
}
|
||||
}
|
||||
|
||||
return base.CommandSuccess
|
||||
}
|
||||
|
||||
func (c *LdapCommand) checkFuncError(err error) int {
|
||||
if err == nil {
|
||||
return 0
|
||||
}
|
||||
if apiErr := api.AsServerError(err); apiErr != nil {
|
||||
c.PrintApiError(apiErr, fmt.Sprintf("Error from controller when performing %s on %s", c.Func, c.plural))
|
||||
return base.CommandApiError
|
||||
}
|
||||
c.PrintCliError(fmt.Errorf("Error trying to %s %s: %s", c.Func, c.plural, err.Error()))
|
||||
return base.CommandCliError
|
||||
}
|
||||
|
||||
var (
|
||||
extraLdapActionsFlagsMapFunc = func() map[string][]string { return nil }
|
||||
extraLdapSynopsisFunc = func(*LdapCommand) string { return "" }
|
||||
extraLdapFlagsFunc = func(*LdapCommand, *base.FlagSets, *base.FlagSet) {}
|
||||
extraLdapFlagsHandlingFunc = func(*LdapCommand, *base.FlagSets, *[]managedgroups.Option) bool { return true }
|
||||
executeExtraLdapActions = func(_ *LdapCommand, inResp *api.Response, inItem *managedgroups.ManagedGroup, inErr error, _ *managedgroups.Client, _ uint32, _ []managedgroups.Option) (*api.Response, *managedgroups.ManagedGroup, error) {
|
||||
return inResp, inItem, inErr
|
||||
}
|
||||
printCustomLdapActionOutput = func(*LdapCommand) (bool, error) { return false, nil }
|
||||
)
|
||||
@@ -130,6 +130,20 @@ var inputStructs = map[string][]*cmdInfo{
|
||||
HasDescription: true,
|
||||
VersionedActions: []string{"update"},
|
||||
},
|
||||
{
|
||||
ResourceType: resource.Account.String(),
|
||||
Pkg: "accounts",
|
||||
StdActions: []string{"create", "update"},
|
||||
SubActionPrefix: "ldap",
|
||||
HasExtraCommandVars: true,
|
||||
SkipNormalHelp: true,
|
||||
HasExtraHelpFunc: true,
|
||||
HasId: true,
|
||||
HasName: true,
|
||||
Container: "AuthMethod",
|
||||
HasDescription: true,
|
||||
VersionedActions: []string{"update"},
|
||||
},
|
||||
},
|
||||
"authmethods": {
|
||||
{
|
||||
@@ -170,6 +184,21 @@ var inputStructs = map[string][]*cmdInfo{
|
||||
VersionedActions: []string{"update", "change-state"},
|
||||
NeedsSubtypeInCreate: true,
|
||||
},
|
||||
{
|
||||
ResourceType: resource.AuthMethod.String(),
|
||||
Pkg: "authmethods",
|
||||
StdActions: []string{"create", "update"},
|
||||
SubActionPrefix: "ldap",
|
||||
HasExtraCommandVars: true,
|
||||
SkipNormalHelp: true,
|
||||
HasExtraHelpFunc: true,
|
||||
HasId: true,
|
||||
HasName: true,
|
||||
HasDescription: true,
|
||||
Container: "Scope",
|
||||
VersionedActions: []string{"update"},
|
||||
NeedsSubtypeInCreate: true,
|
||||
},
|
||||
},
|
||||
"authtokens": {
|
||||
{
|
||||
@@ -483,6 +512,20 @@ var inputStructs = map[string][]*cmdInfo{
|
||||
HasDescription: true,
|
||||
VersionedActions: []string{"update"},
|
||||
},
|
||||
{
|
||||
ResourceType: resource.ManagedGroup.String(),
|
||||
Pkg: "managedgroups",
|
||||
StdActions: []string{"create", "update"},
|
||||
SubActionPrefix: "ldap",
|
||||
HasExtraCommandVars: true,
|
||||
SkipNormalHelp: true,
|
||||
HasExtraHelpFunc: true,
|
||||
HasId: true,
|
||||
HasName: true,
|
||||
Container: "AuthMethod",
|
||||
HasDescription: true,
|
||||
VersionedActions: []string{"update"},
|
||||
},
|
||||
},
|
||||
"roles": {
|
||||
{
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/hashicorp/boundary/api/recovery"
|
||||
"github.com/hashicorp/boundary/globals"
|
||||
"github.com/hashicorp/boundary/internal/auth"
|
||||
"github.com/hashicorp/boundary/internal/auth/ldap"
|
||||
"github.com/hashicorp/boundary/internal/auth/oidc"
|
||||
"github.com/hashicorp/boundary/internal/auth/password"
|
||||
"github.com/hashicorp/boundary/internal/daemon/controller/common"
|
||||
@@ -104,6 +105,7 @@ type verifier struct {
|
||||
serversRepoFn common.ServersRepoFactory
|
||||
passwordAuthRepoFn common.PasswordAuthRepoFactory
|
||||
oidcAuthRepoFn common.OidcAuthRepoFactory
|
||||
ldapAuthRepoFn common.LdapAuthRepoFactory
|
||||
kms *kms.Kms
|
||||
requestInfo *authpb.RequestInfo
|
||||
res *perms.Resource
|
||||
@@ -128,6 +130,7 @@ func NewVerifierContextWithAccounts(ctx context.Context,
|
||||
serversRepoFn common.ServersRepoFactory,
|
||||
passwordAuthRepoFn common.PasswordAuthRepoFactory,
|
||||
oidcAuthRepoFn common.OidcAuthRepoFactory,
|
||||
ldapAuthRepoFn common.LdapAuthRepoFactory,
|
||||
kms *kms.Kms,
|
||||
requestInfo *authpb.RequestInfo,
|
||||
) context.Context {
|
||||
@@ -137,6 +140,7 @@ func NewVerifierContextWithAccounts(ctx context.Context,
|
||||
serversRepoFn: serversRepoFn,
|
||||
passwordAuthRepoFn: passwordAuthRepoFn,
|
||||
oidcAuthRepoFn: oidcAuthRepoFn,
|
||||
ldapAuthRepoFn: ldapAuthRepoFn,
|
||||
kms: kms,
|
||||
requestInfo: requestInfo,
|
||||
})
|
||||
@@ -153,7 +157,7 @@ func NewVerifierContext(ctx context.Context,
|
||||
kms *kms.Kms,
|
||||
requestInfo *authpb.RequestInfo,
|
||||
) context.Context {
|
||||
return NewVerifierContextWithAccounts(ctx, iamRepoFn, authTokenRepoFn, serversRepoFn, nil, nil, kms, requestInfo)
|
||||
return NewVerifierContextWithAccounts(ctx, iamRepoFn, authTokenRepoFn, serversRepoFn, nil, nil, nil, kms, requestInfo)
|
||||
}
|
||||
|
||||
// Verify takes in a context that has expected parameters as values and runs an
|
||||
@@ -547,7 +551,7 @@ func (v verifier) performAuthCheck(ctx context.Context) (
|
||||
userData.User.Email = util.Pointer(u.Email)
|
||||
userData.User.FullName = util.Pointer(u.FullName)
|
||||
|
||||
if userData.Account.Id != nil && *userData.Account.Id != "" && v.passwordAuthRepoFn != nil && v.oidcAuthRepoFn != nil {
|
||||
if userData.Account.Id != nil && *userData.Account.Id != "" && v.passwordAuthRepoFn != nil && v.oidcAuthRepoFn != nil && v.ldapAuthRepoFn != nil {
|
||||
const domain = "auth"
|
||||
var acct auth.Account
|
||||
var err error
|
||||
@@ -566,6 +570,13 @@ func (v verifier) performAuthCheck(ctx context.Context) (
|
||||
return
|
||||
}
|
||||
acct, err = repo.LookupAccount(ctx, *userData.Account.Id)
|
||||
case ldap.Subtype:
|
||||
repo, repoErr := v.ldapAuthRepoFn()
|
||||
if repoErr != nil {
|
||||
retErr = errors.Wrap(ctx, repoErr, op, errors.WithMsg("failed to get ldap auth repo"))
|
||||
return
|
||||
}
|
||||
acct, err = repo.LookupAccount(ctx, *userData.Account.Id)
|
||||
default:
|
||||
retErr = errors.Wrap(ctx, err, op, errors.WithMsg("unrecognized account id type"))
|
||||
return
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/boundary/internal/auth/ldap"
|
||||
"github.com/hashicorp/boundary/internal/auth/oidc"
|
||||
"github.com/hashicorp/boundary/internal/auth/password"
|
||||
credstatic "github.com/hashicorp/boundary/internal/credential/static"
|
||||
@@ -22,6 +23,7 @@ type (
|
||||
StaticCredentialRepoFactory = func() (*credstatic.Repository, error)
|
||||
IamRepoFactory = iam.IamRepoFactory
|
||||
OidcAuthRepoFactory = oidc.OidcRepoFactory
|
||||
LdapAuthRepoFactory = ldap.RepoFactory
|
||||
PasswordAuthRepoFactory func() (*password.Repository, error)
|
||||
ServersRepoFactory func() (*server.Repository, error)
|
||||
StaticRepoFactory func() (*static.Repository, error)
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/auth/ldap"
|
||||
"github.com/hashicorp/boundary/internal/auth/oidc"
|
||||
"github.com/hashicorp/boundary/internal/auth/password"
|
||||
"github.com/hashicorp/boundary/internal/authtoken"
|
||||
@@ -117,6 +118,7 @@ type Controller struct {
|
||||
StaticCredentialRepoFn common.StaticCredentialRepoFactory
|
||||
IamRepoFn common.IamRepoFactory
|
||||
OidcRepoFn common.OidcAuthRepoFactory
|
||||
LdapRepoFn common.LdapAuthRepoFactory
|
||||
PasswordAuthRepoFn common.PasswordAuthRepoFactory
|
||||
ServersRepoFn common.ServersRepoFactory
|
||||
SessionRepoFn session.RepositoryFactory
|
||||
@@ -362,6 +364,9 @@ func New(ctx context.Context, conf *Config) (*Controller, error) {
|
||||
c.OidcRepoFn = func() (*oidc.Repository, error) {
|
||||
return oidc.NewRepository(ctx, dbase, dbase, c.kms)
|
||||
}
|
||||
c.LdapRepoFn = func() (*ldap.Repository, error) {
|
||||
return ldap.NewRepository(ctx, dbase, dbase, c.kms)
|
||||
}
|
||||
c.PasswordAuthRepoFn = func() (*password.Repository, error) {
|
||||
return password.NewRepository(dbase, dbase, c.kms)
|
||||
}
|
||||
|
||||
@@ -72,6 +72,7 @@ func newGrpcServer(
|
||||
serversRepoFn common.ServersRepoFactory,
|
||||
passwordAuthRepoFn common.PasswordAuthRepoFactory,
|
||||
oidcAuthRepoFn common.OidcAuthRepoFactory,
|
||||
ldapAuthRepoFn common.LdapAuthRepoFactory,
|
||||
kms *kms.Kms,
|
||||
eventer *event.Eventer,
|
||||
) (*grpc.Server, string, error) {
|
||||
@@ -80,7 +81,7 @@ func newGrpcServer(
|
||||
if err != nil {
|
||||
return nil, "", errors.Wrap(ctx, err, op, errors.WithMsg("unable to generate gateway ticket"))
|
||||
}
|
||||
requestCtxInterceptor, err := requestCtxInterceptor(ctx, iamRepoFn, authTokenRepoFn, serversRepoFn, passwordAuthRepoFn, oidcAuthRepoFn, kms, ticket, eventer)
|
||||
requestCtxInterceptor, err := requestCtxInterceptor(ctx, iamRepoFn, authTokenRepoFn, serversRepoFn, passwordAuthRepoFn, oidcAuthRepoFn, ldapAuthRepoFn, kms, ticket, eventer)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
@@ -154,14 +154,14 @@ func (c *Controller) registerGrpcServices(s *grpc.Server) error {
|
||||
services.RegisterHostServiceServer(s, hs)
|
||||
}
|
||||
if _, ok := currentServices[services.AccountService_ServiceDesc.ServiceName]; !ok {
|
||||
accts, err := accounts.NewService(c.PasswordAuthRepoFn, c.OidcRepoFn)
|
||||
accts, err := accounts.NewService(c.baseContext, c.PasswordAuthRepoFn, c.OidcRepoFn, c.LdapRepoFn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create account handler service: %w", err)
|
||||
}
|
||||
services.RegisterAccountServiceServer(s, accts)
|
||||
}
|
||||
if _, ok := currentServices[services.AuthMethodService_ServiceDesc.ServiceName]; !ok {
|
||||
authMethods, err := authmethods.NewService(c.kms, c.PasswordAuthRepoFn, c.OidcRepoFn, c.IamRepoFn, c.AuthTokenRepoFn)
|
||||
authMethods, err := authmethods.NewService(c.kms, c.PasswordAuthRepoFn, c.OidcRepoFn, c.IamRepoFn, c.AuthTokenRepoFn, c.LdapRepoFn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create auth method handler service: %w", err)
|
||||
}
|
||||
@@ -229,7 +229,7 @@ func (c *Controller) registerGrpcServices(s *grpc.Server) error {
|
||||
services.RegisterSessionServiceServer(s, ss)
|
||||
}
|
||||
if _, ok := currentServices[services.ManagedGroupService_ServiceDesc.ServiceName]; !ok {
|
||||
mgs, err := managed_groups.NewService(c.OidcRepoFn)
|
||||
mgs, err := managed_groups.NewService(c.baseContext, c.OidcRepoFn, c.LdapRepoFn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create managed groups handler service: %w", err)
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"github.com/hashicorp/boundary/globals"
|
||||
"github.com/hashicorp/boundary/internal/auth"
|
||||
"github.com/hashicorp/boundary/internal/auth/ldap"
|
||||
"github.com/hashicorp/boundary/internal/auth/oidc"
|
||||
oidcstore "github.com/hashicorp/boundary/internal/auth/oidc/store"
|
||||
"github.com/hashicorp/boundary/internal/auth/password"
|
||||
@@ -52,6 +53,13 @@ const (
|
||||
nameClaimField = "attributes.full_name"
|
||||
emailClaimField = "attributes.email"
|
||||
|
||||
// ldap field names
|
||||
loginAttrField = "attributes.login_name"
|
||||
nameAttrField = "attributes.full_name"
|
||||
emailAttrField = "attributes.email"
|
||||
dnAttrField = "attributes.dn"
|
||||
memberOfAttrField = "attributes.member_of_groups"
|
||||
|
||||
domain = "auth"
|
||||
)
|
||||
|
||||
@@ -76,6 +84,12 @@ var (
|
||||
action.Update,
|
||||
action.Delete,
|
||||
},
|
||||
ldap.Subtype: {
|
||||
action.NoOp,
|
||||
action.Read,
|
||||
action.Update,
|
||||
action.Delete,
|
||||
},
|
||||
}
|
||||
|
||||
// CollectionActions contains the set of actions that can be performed on
|
||||
@@ -102,20 +116,23 @@ type Service struct {
|
||||
|
||||
pwRepoFn common.PasswordAuthRepoFactory
|
||||
oidcRepoFn common.OidcAuthRepoFactory
|
||||
ldapRepoFn common.LdapAuthRepoFactory
|
||||
}
|
||||
|
||||
var _ pbs.AccountServiceServer = (*Service)(nil)
|
||||
|
||||
// NewService returns a account service which handles account related requests to boundary.
|
||||
func NewService(pwRepo common.PasswordAuthRepoFactory, oidcRepo common.OidcAuthRepoFactory) (Service, error) {
|
||||
func NewService(ctx context.Context, pwRepo common.PasswordAuthRepoFactory, oidcRepo common.OidcAuthRepoFactory, ldapRepo common.LdapAuthRepoFactory) (Service, error) {
|
||||
const op = "accounts.NewService"
|
||||
if pwRepo == nil {
|
||||
return Service{}, errors.NewDeprecated(errors.InvalidParameter, op, "missing password repository")
|
||||
switch {
|
||||
case pwRepo == nil:
|
||||
return Service{}, errors.New(ctx, errors.InvalidParameter, op, "missing password repository")
|
||||
case oidcRepo == nil:
|
||||
return Service{}, errors.New(ctx, errors.InvalidParameter, op, "missing oidc repository")
|
||||
case ldapRepo == nil:
|
||||
return Service{}, errors.New(ctx, errors.InvalidParameter, op, "missing ldap repository")
|
||||
}
|
||||
if oidcRepo == nil {
|
||||
return Service{}, errors.NewDeprecated(errors.InvalidParameter, op, "missing oidc repository provided")
|
||||
}
|
||||
return Service{pwRepoFn: pwRepo, oidcRepoFn: oidcRepo}, nil
|
||||
return Service{pwRepoFn: pwRepo, oidcRepoFn: oidcRepo, ldapRepoFn: ldapRepo}, nil
|
||||
}
|
||||
|
||||
// ListAccounts implements the interface pbs.AccountServiceServer.
|
||||
@@ -434,6 +451,29 @@ func (s Service) getFromRepo(ctx context.Context, id string) (auth.Account, []st
|
||||
mgIds = append(mgIds, mg.GetManagedGroupId())
|
||||
}
|
||||
acct = a
|
||||
case ldap.Subtype:
|
||||
repo, err := s.ldapRepoFn()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
a, err := repo.LookupAccount(ctx, id)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if err != nil {
|
||||
if errors.IsNotFoundError(err) {
|
||||
return nil, nil, handlers.NotFoundErrorf("LDAP account %q doesn't exist.", id)
|
||||
}
|
||||
return nil, nil, err
|
||||
}
|
||||
mgs, err := repo.ListManagedGroupMembershipsByMember(ctx, a.GetPublicId())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for _, mg := range mgs {
|
||||
mgIds = append(mgIds, mg.GetManagedGroupId())
|
||||
}
|
||||
acct = a
|
||||
default:
|
||||
return nil, nil, handlers.NotFoundErrorf("Unrecognized id.")
|
||||
}
|
||||
@@ -515,6 +555,37 @@ func (s Service) createOidcInRepo(ctx context.Context, am auth.AuthMethod, item
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s Service) createLdapInRepo(ctx context.Context, am auth.AuthMethod, item *pb.Account) (*ldap.Account, error) {
|
||||
const op = "accounts.(Service).createLdapInRepo"
|
||||
if item == nil {
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing item")
|
||||
}
|
||||
var opts []ldap.Option
|
||||
if item.GetName() != nil {
|
||||
opts = append(opts, ldap.WithName(ctx, item.GetName().GetValue()))
|
||||
}
|
||||
if item.GetDescription() != nil {
|
||||
opts = append(opts, ldap.WithDescription(ctx, item.GetDescription().GetValue()))
|
||||
}
|
||||
a, err := ldap.NewAccount(ctx, am.GetScopeId(), am.GetPublicId(), item.GetLdapAccountAttributes().GetLoginName(), opts...)
|
||||
if err != nil {
|
||||
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to build account for creation: %v.", err)
|
||||
}
|
||||
repo, err := s.ldapRepoFn()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out, err := repo.CreateAccount(ctx, a)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to create account"))
|
||||
}
|
||||
if out == nil {
|
||||
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create account but no error returned from repository.")
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s Service) createInRepo(ctx context.Context, am auth.AuthMethod, item *pb.Account) (auth.Account, error) {
|
||||
const op = "accounts.(Service).createInRepo"
|
||||
if item == nil {
|
||||
@@ -540,7 +611,17 @@ func (s Service) createInRepo(ctx context.Context, am auth.AuthMethod, item *pb.
|
||||
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create account but no error returned from repository.")
|
||||
}
|
||||
out = am
|
||||
case ldap.Subtype:
|
||||
am, err := s.createLdapInRepo(ctx, am, item)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
if am == nil {
|
||||
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create ldap account but no error returned from repository.")
|
||||
}
|
||||
out = am
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
@@ -611,6 +692,63 @@ func (s Service) updateOidcInRepo(ctx context.Context, scopeId, amId, id string,
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s Service) updateLdapInRepo(ctx context.Context, scopeId, amId, id string, mask []string, item *pb.Account) (*ldap.Account, error) {
|
||||
const op = "accounts.(Service).updateLdapInRepo"
|
||||
switch {
|
||||
case item == nil:
|
||||
return nil, handlers.ApiErrorWithCodeAndMessage(codes.InvalidArgument, "nil account.")
|
||||
case scopeId == "":
|
||||
return nil, handlers.ApiErrorWithCodeAndMessage(codes.InvalidArgument, "missing scope id")
|
||||
case amId == "":
|
||||
return nil, handlers.ApiErrorWithCodeAndMessage(codes.InvalidArgument, "missing auth method id")
|
||||
case id == "":
|
||||
return nil, handlers.ApiErrorWithCodeAndMessage(codes.InvalidArgument, "missing id")
|
||||
case len(mask) == 0:
|
||||
return nil, handlers.ApiErrorWithCodeAndMessage(codes.InvalidArgument, "missing mask.")
|
||||
}
|
||||
u := ldap.AllocAccount()
|
||||
u.PublicId = id
|
||||
u.ScopeId = scopeId
|
||||
u.AuthMethodId = amId
|
||||
if item.GetName() != nil {
|
||||
u.Name = item.GetName().GetValue()
|
||||
}
|
||||
if item.GetDescription() != nil {
|
||||
u.Description = item.GetDescription().GetValue()
|
||||
}
|
||||
|
||||
// we don't need a mask mgr, since none of the attributes fields are
|
||||
// updatable. Just a simple split on commas looking for multiple paths in
|
||||
// one mask string
|
||||
dbMask := []string{}
|
||||
for _, v := range mask {
|
||||
vSplit := strings.Split(v, ",")
|
||||
for _, m := range vSplit {
|
||||
switch m {
|
||||
case globals.NameField, globals.DescriptionField:
|
||||
dbMask = append(dbMask, vSplit...)
|
||||
case globals.VersionField:
|
||||
// no-op
|
||||
default:
|
||||
return nil, handlers.InvalidArgumentErrorf("No valid fields included in the update mask.", map[string]string{"update_mask": "No valid fields provided in the update mask."})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
repo, err := s.ldapRepoFn()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
out, rowsUpdated, err := repo.UpdateAccount(ctx, scopeId, u, item.GetVersion(), dbMask)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to update account"))
|
||||
}
|
||||
if rowsUpdated == 0 {
|
||||
return nil, handlers.NotFoundErrorf("Account %q doesn't exist or incorrect version provided.", id)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s Service) updateInRepo(ctx context.Context, scopeId, authMethodId string, req *pbs.UpdateAccountRequest) (auth.Account, error) {
|
||||
const op = "accounts.(Service).updateInRepo"
|
||||
var out auth.Account
|
||||
@@ -633,6 +771,15 @@ func (s Service) updateInRepo(ctx context.Context, scopeId, authMethodId string,
|
||||
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to update account but no error returned from repository.")
|
||||
}
|
||||
out = a
|
||||
case ldap.Subtype:
|
||||
a, err := s.updateLdapInRepo(ctx, scopeId, authMethodId, req.GetId(), req.GetUpdateMask().GetPaths(), req.GetItem())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
if a == nil {
|
||||
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to update account but no error returned from repository.")
|
||||
}
|
||||
out = a
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -654,6 +801,12 @@ func (s Service) deleteFromRepo(ctx context.Context, scopeId, id string) (bool,
|
||||
return false, iErr
|
||||
}
|
||||
rows, err = repo.DeleteAccount(ctx, scopeId, id)
|
||||
case ldap.Subtype:
|
||||
repo, iErr := s.ldapRepoFn()
|
||||
if iErr != nil {
|
||||
return false, iErr
|
||||
}
|
||||
rows, err = repo.DeleteAccount(ctx, id)
|
||||
}
|
||||
if err != nil {
|
||||
if errors.IsNotFoundError(err) {
|
||||
@@ -693,6 +846,18 @@ func (s Service) listFromRepo(ctx context.Context, authMethodId string) ([]auth.
|
||||
for _, a := range oidcl {
|
||||
outUl = append(outUl, a)
|
||||
}
|
||||
case ldap.Subtype:
|
||||
ldapRepo, err := s.ldapRepoFn()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
ldapList, err := ldapRepo.ListAccounts(ctx, authMethodId)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
for _, a := range ldapList {
|
||||
outUl = append(outUl, a)
|
||||
}
|
||||
}
|
||||
return outUl, nil
|
||||
}
|
||||
@@ -756,6 +921,11 @@ func (s Service) parentAndAuthResult(ctx context.Context, id string, a action.Ty
|
||||
res.Error = err
|
||||
return nil, res
|
||||
}
|
||||
ldapRepo, err := s.ldapRepoFn()
|
||||
if err != nil {
|
||||
res.Error = err
|
||||
return nil, res
|
||||
}
|
||||
|
||||
var parentId string
|
||||
opts := []requestauth.Option{requestauth.WithType(resource.Account), requestauth.WithAction(a)}
|
||||
@@ -786,6 +956,17 @@ func (s Service) parentAndAuthResult(ctx context.Context, id string, a action.Ty
|
||||
return nil, res
|
||||
}
|
||||
parentId = acct.GetAuthMethodId()
|
||||
case ldap.Subtype:
|
||||
acct, err := ldapRepo.LookupAccount(ctx, id)
|
||||
if err != nil {
|
||||
res.Error = err
|
||||
return nil, res
|
||||
}
|
||||
if acct == nil {
|
||||
res.Error = handlers.NotFoundError()
|
||||
return nil, res
|
||||
}
|
||||
parentId = acct.GetAuthMethodId()
|
||||
}
|
||||
opts = append(opts, requestauth.WithId(id))
|
||||
}
|
||||
@@ -814,6 +995,17 @@ func (s Service) parentAndAuthResult(ctx context.Context, id string, a action.Ty
|
||||
return nil, res
|
||||
}
|
||||
authMeth = am
|
||||
case ldap.Subtype:
|
||||
am, err := ldapRepo.LookupAuthMethod(ctx, parentId)
|
||||
if err != nil {
|
||||
res.Error = err
|
||||
return nil, res
|
||||
}
|
||||
if am == nil {
|
||||
res.Error = handlers.NotFoundError()
|
||||
return nil, res
|
||||
}
|
||||
authMeth = am
|
||||
}
|
||||
opts = append(opts, requestauth.WithScopeId(authMeth.GetScopeId()), requestauth.WithPin(parentId))
|
||||
return authMeth, requestauth.Verify(ctx, opts...)
|
||||
@@ -908,6 +1100,29 @@ func toProto(ctx context.Context, in auth.Account, opt ...handlers.Option) (*pb.
|
||||
}
|
||||
}
|
||||
out.Attrs = attrs
|
||||
case *ldap.Account:
|
||||
if outputFields.Has(globals.TypeField) {
|
||||
out.Type = ldap.Subtype.String()
|
||||
}
|
||||
if !outputFields.Has(globals.AttributesField) {
|
||||
break
|
||||
}
|
||||
attrs := &pb.Account_LdapAccountAttributes{
|
||||
LdapAccountAttributes: &pb.LdapAccountAttributes{
|
||||
LoginName: i.GetLoginName(),
|
||||
FullName: i.GetFullName(),
|
||||
Email: i.GetEmail(),
|
||||
Dn: i.GetDn(),
|
||||
},
|
||||
}
|
||||
if encodedGroups := i.GetMemberOfGroups(); encodedGroups != "" {
|
||||
var decodedGroups []string
|
||||
if err := json.Unmarshal([]byte(encodedGroups), &decodedGroups); err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op, errors.WithMsg(""))
|
||||
}
|
||||
attrs.LdapAccountAttributes.MemberOfGroups = decodedGroups
|
||||
}
|
||||
out.Attrs = attrs
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
@@ -946,7 +1161,7 @@ func validateGetRequest(req *pbs.GetAccountRequest) error {
|
||||
if req == nil {
|
||||
return errors.NewDeprecated(errors.InvalidParameter, op, "nil request")
|
||||
}
|
||||
return handlers.ValidateGetRequest(handlers.NoopValidatorFn, req, globals.PasswordAccountPreviousPrefix, globals.PasswordAccountPrefix, globals.OidcAccountPrefix)
|
||||
return handlers.ValidateGetRequest(handlers.NoopValidatorFn, req, globals.PasswordAccountPreviousPrefix, globals.PasswordAccountPrefix, globals.OidcAccountPrefix, globals.LdapAccountPrefix)
|
||||
}
|
||||
|
||||
func validateCreateRequest(req *pbs.CreateAccountRequest) error {
|
||||
@@ -1001,6 +1216,31 @@ func validateCreateRequest(req *pbs.CreateAccountRequest) error {
|
||||
badFields[emailClaimField] = "This is a read only field."
|
||||
}
|
||||
}
|
||||
case ldap.Subtype:
|
||||
if req.GetItem().GetType() != "" && req.GetItem().GetType() != ldap.Subtype.String() {
|
||||
badFields[typeField] = "Doesn't match the parent resource's type."
|
||||
}
|
||||
attrs := req.GetItem().GetLdapAccountAttributes()
|
||||
switch {
|
||||
case attrs == nil:
|
||||
badFields["attributes"] = "This is a required field."
|
||||
default:
|
||||
if attrs.GetLoginName() == "" {
|
||||
badFields[loginAttrField] = "This is a required field for this type."
|
||||
}
|
||||
if attrs.GetFullName() != "" {
|
||||
badFields[nameAttrField] = "This is a read only field."
|
||||
}
|
||||
if attrs.GetEmail() != "" {
|
||||
badFields[emailAttrField] = "This is a read only field."
|
||||
}
|
||||
if attrs.GetDn() != "" {
|
||||
badFields[dnAttrField] = "This is a read only field."
|
||||
}
|
||||
if len(attrs.GetMemberOfGroups()) > 0 {
|
||||
badFields[memberOfAttrField] = "This is a read only field."
|
||||
}
|
||||
}
|
||||
default:
|
||||
badFields[authMethodIdField] = "Unknown auth method type from ID."
|
||||
}
|
||||
@@ -1036,9 +1276,28 @@ func validateUpdateRequest(req *pbs.UpdateAccountRequest) error {
|
||||
if handlers.MaskContains(req.GetUpdateMask().GetPaths(), nameClaimField) {
|
||||
badFields[nameClaimField] = "Field is read only."
|
||||
}
|
||||
case ldap.Subtype:
|
||||
if req.GetItem().GetType() != "" && req.GetItem().GetType() != ldap.Subtype.String() {
|
||||
badFields[typeField] = "Cannot modify the resource type."
|
||||
}
|
||||
if handlers.MaskContains(req.GetUpdateMask().GetPaths(), loginAttrField) {
|
||||
badFields[loginAttrField] = "Field cannot be updated."
|
||||
}
|
||||
if handlers.MaskContains(req.GetUpdateMask().GetPaths(), nameAttrField) {
|
||||
badFields[nameAttrField] = "Field cannot be updated."
|
||||
}
|
||||
if handlers.MaskContains(req.GetUpdateMask().GetPaths(), emailAttrField) {
|
||||
badFields[emailAttrField] = "Field cannot be updated."
|
||||
}
|
||||
if handlers.MaskContains(req.GetUpdateMask().GetPaths(), dnAttrField) {
|
||||
badFields[dnAttrField] = "Field cannot be updated."
|
||||
}
|
||||
if handlers.MaskContains(req.GetUpdateMask().GetPaths(), memberOfAttrField) {
|
||||
badFields[memberOfAttrField] = "Field cannot be updated."
|
||||
}
|
||||
}
|
||||
return badFields
|
||||
}, globals.PasswordAccountPreviousPrefix, globals.PasswordAccountPrefix, globals.OidcAccountPrefix)
|
||||
}, globals.PasswordAccountPreviousPrefix, globals.PasswordAccountPrefix, globals.OidcAccountPrefix, globals.LdapAccountPrefix)
|
||||
}
|
||||
|
||||
func validateDeleteRequest(req *pbs.DeleteAccountRequest) error {
|
||||
@@ -1046,7 +1305,7 @@ func validateDeleteRequest(req *pbs.DeleteAccountRequest) error {
|
||||
if req == nil {
|
||||
return errors.NewDeprecated(errors.InvalidParameter, op, "nil request")
|
||||
}
|
||||
return handlers.ValidateDeleteRequest(handlers.NoopValidatorFn, req, globals.PasswordAccountPreviousPrefix, globals.PasswordAccountPrefix, globals.OidcAccountPrefix)
|
||||
return handlers.ValidateDeleteRequest(handlers.NoopValidatorFn, req, globals.PasswordAccountPreviousPrefix, globals.PasswordAccountPrefix, globals.OidcAccountPrefix, globals.LdapAccountPrefix)
|
||||
}
|
||||
|
||||
func validateListRequest(req *pbs.ListAccountsRequest) error {
|
||||
@@ -1055,7 +1314,7 @@ func validateListRequest(req *pbs.ListAccountsRequest) error {
|
||||
return errors.NewDeprecated(errors.InvalidParameter, op, "nil request")
|
||||
}
|
||||
badFields := map[string]string{}
|
||||
if !handlers.ValidId(handlers.Id(req.GetAuthMethodId()), globals.PasswordAuthMethodPrefix, globals.OidcAuthMethodPrefix) {
|
||||
if !handlers.ValidId(handlers.Id(req.GetAuthMethodId()), globals.PasswordAuthMethodPrefix, globals.OidcAuthMethodPrefix, globals.LdapAuthMethodPrefix) {
|
||||
badFields[authMethodIdField] = "Invalid formatted identifier."
|
||||
}
|
||||
if _, err := handlers.NewFilter(req.GetFilter()); err != nil {
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -9,6 +9,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/globals"
|
||||
"github.com/hashicorp/boundary/internal/auth/ldap"
|
||||
"github.com/hashicorp/boundary/internal/auth/oidc"
|
||||
"github.com/hashicorp/boundary/internal/auth/password"
|
||||
pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services"
|
||||
@@ -52,6 +53,14 @@ func TestValidateCreateRequest(t *testing.T) {
|
||||
},
|
||||
errContains: fieldError(typeField, "Doesn't match the parent resource's type."),
|
||||
},
|
||||
{
|
||||
name: "mismatched pw authmethod ldap type",
|
||||
item: &pb.Account{
|
||||
Type: ldap.Subtype.String(),
|
||||
AuthMethodId: globals.PasswordAuthMethodPrefix + "_1234567890",
|
||||
},
|
||||
errContains: fieldError(typeField, "Doesn't match the parent resource's type."),
|
||||
},
|
||||
{
|
||||
name: "missing oidc attributes",
|
||||
item: &pb.Account{
|
||||
@@ -93,6 +102,69 @@ func TestValidateCreateRequest(t *testing.T) {
|
||||
},
|
||||
errContains: fieldError(emailClaimField, "This is a read only field."),
|
||||
},
|
||||
{
|
||||
name: "missing ldap attributes",
|
||||
item: &pb.Account{
|
||||
Type: ldap.Subtype.String(),
|
||||
AuthMethodId: globals.LdapAuthMethodPrefix + "_1234567890",
|
||||
},
|
||||
errContains: fieldError(attributesField, "This is a required field."),
|
||||
},
|
||||
{
|
||||
name: "missing login name for ldap type",
|
||||
item: &pb.Account{
|
||||
Type: ldap.Subtype.String(),
|
||||
AuthMethodId: globals.LdapAuthMethodPrefix + "_1234567890",
|
||||
Attrs: &pb.Account_LdapAccountAttributes{
|
||||
LdapAccountAttributes: &pb.LdapAccountAttributes{},
|
||||
},
|
||||
},
|
||||
errContains: fieldError(loginAttrField, "This is a required field for this type."),
|
||||
},
|
||||
{
|
||||
name: "read only full name attr field",
|
||||
item: &pb.Account{
|
||||
Type: ldap.Subtype.String(),
|
||||
AuthMethodId: globals.LdapAuthMethodPrefix + "_1234567890",
|
||||
Attrs: &pb.Account_LdapAccountAttributes{
|
||||
LdapAccountAttributes: &pb.LdapAccountAttributes{FullName: "something"},
|
||||
},
|
||||
},
|
||||
errContains: fieldError(nameAttrField, "This is a read only field."),
|
||||
},
|
||||
{
|
||||
name: "read only email attr field",
|
||||
item: &pb.Account{
|
||||
Type: ldap.Subtype.String(),
|
||||
AuthMethodId: globals.LdapAuthMethodPrefix + "_1234567890",
|
||||
Attrs: &pb.Account_LdapAccountAttributes{
|
||||
LdapAccountAttributes: &pb.LdapAccountAttributes{Email: "something"},
|
||||
},
|
||||
},
|
||||
errContains: fieldError(emailAttrField, "This is a read only field."),
|
||||
},
|
||||
{
|
||||
name: "read only dn attr field",
|
||||
item: &pb.Account{
|
||||
Type: ldap.Subtype.String(),
|
||||
AuthMethodId: globals.LdapAuthMethodPrefix + "_1234567890",
|
||||
Attrs: &pb.Account_LdapAccountAttributes{
|
||||
LdapAccountAttributes: &pb.LdapAccountAttributes{Dn: "something"},
|
||||
},
|
||||
},
|
||||
errContains: fieldError(dnAttrField, "This is a read only field."),
|
||||
},
|
||||
{
|
||||
name: "read only member of attr field",
|
||||
item: &pb.Account{
|
||||
Type: ldap.Subtype.String(),
|
||||
AuthMethodId: globals.LdapAuthMethodPrefix + "_1234567890",
|
||||
Attrs: &pb.Account_LdapAccountAttributes{
|
||||
LdapAccountAttributes: &pb.LdapAccountAttributes{MemberOfGroups: []string{"something"}},
|
||||
},
|
||||
},
|
||||
errContains: fieldError(memberOfAttrField, "This is a read only field."),
|
||||
},
|
||||
{
|
||||
name: "missing password attributes",
|
||||
item: &pb.Account{
|
||||
@@ -134,6 +206,16 @@ func TestValidateCreateRequest(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no ldap errors",
|
||||
item: &pb.Account{
|
||||
Type: ldap.Subtype.String(),
|
||||
AuthMethodId: globals.LdapAuthMethodPrefix + "_1234567890",
|
||||
Attrs: &pb.Account_LdapAccountAttributes{
|
||||
LdapAccountAttributes: &pb.LdapAccountAttributes{LoginName: "no oidc errors"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
|
||||
@@ -5,12 +5,14 @@ package authmethods
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/boundary/globals"
|
||||
"github.com/hashicorp/boundary/internal/auth"
|
||||
"github.com/hashicorp/boundary/internal/auth/ldap"
|
||||
"github.com/hashicorp/boundary/internal/auth/oidc"
|
||||
"github.com/hashicorp/boundary/internal/auth/password"
|
||||
"github.com/hashicorp/boundary/internal/authtoken"
|
||||
@@ -86,12 +88,13 @@ type Service struct {
|
||||
oidcRepoFn common.OidcAuthRepoFactory
|
||||
iamRepoFn common.IamRepoFactory
|
||||
atRepoFn common.AuthTokenRepoFactory
|
||||
ldapRepoFn common.LdapAuthRepoFactory
|
||||
}
|
||||
|
||||
var _ pbs.AuthMethodServiceServer = (*Service)(nil)
|
||||
|
||||
// NewService returns a auth method service which handles auth method related requests to boundary.
|
||||
func NewService(kms *kms.Kms, pwRepoFn common.PasswordAuthRepoFactory, oidcRepoFn common.OidcAuthRepoFactory, iamRepoFn common.IamRepoFactory, atRepoFn common.AuthTokenRepoFactory, opt ...handlers.Option) (Service, error) {
|
||||
func NewService(kms *kms.Kms, pwRepoFn common.PasswordAuthRepoFactory, oidcRepoFn common.OidcAuthRepoFactory, iamRepoFn common.IamRepoFactory, atRepoFn common.AuthTokenRepoFactory, ldapRepoFn common.LdapAuthRepoFactory, opt ...handlers.Option) (Service, error) {
|
||||
const op = "authmethods.NewService"
|
||||
if kms == nil {
|
||||
return Service{}, errors.NewDeprecated(errors.InvalidParameter, op, "missing kms")
|
||||
@@ -102,13 +105,16 @@ func NewService(kms *kms.Kms, pwRepoFn common.PasswordAuthRepoFactory, oidcRepoF
|
||||
if oidcRepoFn == nil {
|
||||
return Service{}, fmt.Errorf("nil oidc repository provided")
|
||||
}
|
||||
if ldapRepoFn == nil {
|
||||
return Service{}, fmt.Errorf("nil ldap repository provided")
|
||||
}
|
||||
if iamRepoFn == nil {
|
||||
return Service{}, errors.NewDeprecated(errors.InvalidParameter, op, "missing iam repository")
|
||||
}
|
||||
if atRepoFn == nil {
|
||||
return Service{}, fmt.Errorf("nil auth token repository provided")
|
||||
}
|
||||
s := Service{kms: kms, pwRepoFn: pwRepoFn, oidcRepoFn: oidcRepoFn, iamRepoFn: iamRepoFn, atRepoFn: atRepoFn}
|
||||
s := Service{kms: kms, pwRepoFn: pwRepoFn, oidcRepoFn: oidcRepoFn, iamRepoFn: iamRepoFn, atRepoFn: atRepoFn, ldapRepoFn: ldapRepoFn}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
@@ -427,6 +433,10 @@ func (s Service) Authenticate(ctx context.Context, req *pbs.AuthenticateRequest)
|
||||
if err := validateAuthenticateOidcRequest(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case ldap.Subtype:
|
||||
if err := validateAuthenticateLdapRequest(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
authResults := s.authResult(ctx, req.GetAuthMethodId(), action.Authenticate)
|
||||
@@ -440,6 +450,8 @@ func (s Service) Authenticate(ctx context.Context, req *pbs.AuthenticateRequest)
|
||||
|
||||
case oidc.Subtype:
|
||||
return s.authenticateOidc(ctx, req, &authResults)
|
||||
case ldap.Subtype:
|
||||
return s.authenticateLdap(ctx, req, &authResults)
|
||||
}
|
||||
return nil, errors.New(ctx, errors.Internal, op, "Invalid auth method subtype not caught in validation function.")
|
||||
}
|
||||
@@ -462,6 +474,13 @@ func (s Service) getFromRepo(ctx context.Context, id string) (auth.AuthMethod, e
|
||||
}
|
||||
am, lookupErr = repo.LookupAuthMethod(ctx, id)
|
||||
|
||||
case ldap.Subtype:
|
||||
repo, err := s.ldapRepoFn()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
am, lookupErr = repo.LookupAuthMethod(ctx, id)
|
||||
|
||||
default:
|
||||
return nil, handlers.NotFoundErrorf("Unrecognized id.")
|
||||
}
|
||||
@@ -511,6 +530,19 @@ func (s Service) listFromRepo(ctx context.Context, scopeIds []string, authResult
|
||||
for _, item := range pl {
|
||||
outUl = append(outUl, item)
|
||||
}
|
||||
|
||||
ldapRepo, err := s.ldapRepoFn()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
ll, err := ldapRepo.ListAuthMethods(ctx, scopeIds, ldap.WithUnauthenticatedUser(ctx, reqCtx.UserId == globals.AnonymousUserId))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
for _, item := range ll {
|
||||
outUl = append(outUl, item)
|
||||
}
|
||||
|
||||
return outUl, nil
|
||||
}
|
||||
|
||||
@@ -536,6 +568,15 @@ func (s Service) createInRepo(ctx context.Context, scopeId string, item *pb.Auth
|
||||
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create auth method but no error returned from repository.")
|
||||
}
|
||||
out = am
|
||||
case ldap.Subtype:
|
||||
am, err := s.createLdapInRepo(ctx, scopeId, item)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
if am == nil {
|
||||
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create auth method but no error returned from repository.")
|
||||
}
|
||||
out = am
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -567,6 +608,24 @@ func (s Service) updateInRepo(ctx context.Context, scopeId string, req *pbs.Upda
|
||||
}
|
||||
am = oam
|
||||
dryRun = dr
|
||||
|
||||
case ldap.Subtype:
|
||||
lam, err := s.updateLdapInRepo(ctx, scopeId, req.GetId(), req.GetUpdateMask().GetPaths(), req.GetItem())
|
||||
if err != nil {
|
||||
_, apiErr := err.(*handlers.ApiError)
|
||||
switch {
|
||||
case apiErr:
|
||||
return nil, false, err
|
||||
case errors.Match(errors.T(errors.InvalidParameter), err):
|
||||
return nil, false, handlers.ApiErrorWithCodeAndMessage(codes.InvalidArgument, err.Error())
|
||||
default:
|
||||
return nil, false, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
}
|
||||
if lam == nil {
|
||||
return nil, false, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to update auth method but no error returned from repository.")
|
||||
}
|
||||
am = lam
|
||||
}
|
||||
|
||||
return am, dryRun, nil
|
||||
@@ -590,6 +649,14 @@ func (s Service) deleteFromRepo(ctx context.Context, scopeId, id string) (bool,
|
||||
return false, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
rows, dErr = repo.DeleteAuthMethod(ctx, id)
|
||||
case ldap.Subtype:
|
||||
repo, err := s.ldapRepoFn()
|
||||
if err != nil {
|
||||
return false, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
rows, dErr = repo.DeleteAuthMethod(ctx, id)
|
||||
default:
|
||||
return false, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("invalid auth method subtype: %q", subtypes.SubtypeFromId(domain, id)))
|
||||
}
|
||||
|
||||
if dErr != nil {
|
||||
@@ -697,6 +764,22 @@ func (s Service) authResult(ctx context.Context, id string, a action.Type) reque
|
||||
return res
|
||||
}
|
||||
authMeth = am
|
||||
case ldap.Subtype:
|
||||
repo, err := s.ldapRepoFn()
|
||||
if err != nil {
|
||||
res.Error = err
|
||||
return res
|
||||
}
|
||||
am, err := repo.LookupAuthMethod(ctx, id)
|
||||
if err != nil {
|
||||
res.Error = err
|
||||
return res
|
||||
}
|
||||
if am == nil {
|
||||
res.Error = handlers.NotFoundError()
|
||||
return res
|
||||
}
|
||||
authMeth = am
|
||||
default:
|
||||
res.Error = errors.New(ctx, errors.InvalidPublicId, op, "unrecognized auth method type")
|
||||
return res
|
||||
@@ -801,6 +884,60 @@ func toAuthMethodProto(ctx context.Context, in auth.AuthMethod, opt ...handlers.
|
||||
out.Attrs = &pb.AuthMethod_OidcAuthMethodsAttributes{
|
||||
OidcAuthMethodsAttributes: attrs,
|
||||
}
|
||||
case *ldap.AuthMethod:
|
||||
if outputFields.Has(globals.TypeField) {
|
||||
out.Type = ldap.Subtype.String()
|
||||
}
|
||||
if !outputFields.Has(globals.AttributesField) {
|
||||
break
|
||||
}
|
||||
attrs := &pb.LdapAuthMethodAttributes{
|
||||
State: i.GetOperationalState(),
|
||||
StartTls: i.GetStartTls(),
|
||||
InsecureTls: i.GetInsecureTls(),
|
||||
DiscoverDn: i.GetDiscoverDn(),
|
||||
AnonGroupSearch: i.GetAnonGroupSearch(),
|
||||
Urls: i.GetUrls(),
|
||||
EnableGroups: i.GetEnableGroups(),
|
||||
Certificates: i.GetCertificates(),
|
||||
ClientCertificateKeyHmac: base64.RawURLEncoding.EncodeToString(i.GetClientCertificateKeyHmac()),
|
||||
BindPasswordHmac: base64.RawURLEncoding.EncodeToString(i.GetBindPasswordHmac()),
|
||||
UseTokenGroups: i.GetUseTokenGroups(),
|
||||
}
|
||||
if i.GetUpnDomain() != "" {
|
||||
attrs.UpnDomain = wrapperspb.String(i.GetUpnDomain())
|
||||
}
|
||||
if i.GetUserDn() != "" {
|
||||
attrs.UserDn = wrapperspb.String(i.GetUserDn())
|
||||
}
|
||||
if i.GetUserAttr() != "" {
|
||||
attrs.UserAttr = wrapperspb.String(i.GetUserAttr())
|
||||
}
|
||||
if i.GetUserFilter() != "" {
|
||||
attrs.UserFilter = wrapperspb.String(i.GetUserFilter())
|
||||
}
|
||||
if i.GetGroupDn() != "" {
|
||||
attrs.GroupDn = wrapperspb.String(i.GetGroupDn())
|
||||
}
|
||||
if i.GetGroupAttr() != "" {
|
||||
attrs.GroupAttr = wrapperspb.String(i.GetGroupAttr())
|
||||
}
|
||||
if i.GetGroupFilter() != "" {
|
||||
attrs.GroupFilter = wrapperspb.String(i.GetGroupFilter())
|
||||
}
|
||||
if i.GetClientCertificate() != "" {
|
||||
attrs.ClientCertificate = wrapperspb.String(i.GetClientCertificate())
|
||||
}
|
||||
if i.GetBindDn() != "" {
|
||||
attrs.BindDn = wrapperspb.String(i.GetBindDn())
|
||||
}
|
||||
if len(i.GetAccountAttributeMaps()) > 0 {
|
||||
attrs.AccountAttributeMaps = i.GetAccountAttributeMaps()
|
||||
}
|
||||
|
||||
out.Attrs = &pb.AuthMethod_LdapAuthMethodsAttributes{
|
||||
LdapAuthMethodsAttributes: attrs,
|
||||
}
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
@@ -829,7 +966,7 @@ func validateGetRequest(req *pbs.GetAuthMethodRequest) error {
|
||||
if req == nil {
|
||||
return errors.NewDeprecated(errors.InvalidParameter, op, "Missing request")
|
||||
}
|
||||
return handlers.ValidateGetRequest(handlers.NoopValidatorFn, req, globals.PasswordAuthMethodPrefix, globals.OidcAuthMethodPrefix)
|
||||
return handlers.ValidateGetRequest(handlers.NoopValidatorFn, req, globals.PasswordAuthMethodPrefix, globals.OidcAuthMethodPrefix, globals.LdapAuthMethodPrefix)
|
||||
}
|
||||
|
||||
func validateCreateRequest(ctx context.Context, req *pbs.CreateAuthMethodRequest) error {
|
||||
@@ -925,6 +1062,11 @@ func validateCreateRequest(ctx context.Context, req *pbs.CreateAuthMethodRequest
|
||||
}
|
||||
}
|
||||
}
|
||||
case ldap.Subtype:
|
||||
if len(req.GetItem().GetLdapAuthMethodsAttributes().GetUrls()) == 0 {
|
||||
badFields[urlsField] = "At least one URL is required"
|
||||
}
|
||||
validateLdapAttributes(ctx, req.GetItem().GetLdapAuthMethodsAttributes(), badFields)
|
||||
default:
|
||||
badFields[typeField] = fmt.Sprintf("This is a required field and must be %q.", password.Subtype.String())
|
||||
}
|
||||
@@ -1051,11 +1193,16 @@ func validateUpdateRequest(ctx context.Context, req *pbs.UpdateAuthMethodRequest
|
||||
}
|
||||
}
|
||||
}
|
||||
case ldap.Subtype:
|
||||
if req.GetItem().GetType() != "" && subtypes.SubtypeFromType(domain, req.GetItem().GetType()) != ldap.Subtype {
|
||||
badFields[typeField] = "Cannot modify the resource type."
|
||||
}
|
||||
validateLdapAttributes(ctx, req.GetItem().GetLdapAuthMethodsAttributes(), badFields)
|
||||
default:
|
||||
badFields["id"] = "Incorrectly formatted identifier."
|
||||
}
|
||||
return badFields
|
||||
}, globals.PasswordAuthMethodPrefix, globals.OidcAuthMethodPrefix)
|
||||
}, globals.PasswordAuthMethodPrefix, globals.OidcAuthMethodPrefix, globals.LdapAuthMethodPrefix)
|
||||
}
|
||||
|
||||
func validateDeleteRequest(req *pbs.DeleteAuthMethodRequest) error {
|
||||
@@ -1063,7 +1210,7 @@ func validateDeleteRequest(req *pbs.DeleteAuthMethodRequest) error {
|
||||
if req == nil {
|
||||
return errors.NewDeprecated(errors.InvalidParameter, op, "Missing request")
|
||||
}
|
||||
return handlers.ValidateDeleteRequest(handlers.NoopValidatorFn, req, globals.PasswordAuthMethodPrefix, globals.OidcAuthMethodPrefix)
|
||||
return handlers.ValidateDeleteRequest(handlers.NoopValidatorFn, req, globals.PasswordAuthMethodPrefix, globals.OidcAuthMethodPrefix, globals.LdapAuthMethodPrefix)
|
||||
}
|
||||
|
||||
func validateListRequest(req *pbs.ListAuthMethodsRequest) error {
|
||||
@@ -1127,7 +1274,7 @@ func validateAuthenticateRequest(req *pbs.AuthenticateRequest) error {
|
||||
} else {
|
||||
st := subtypes.SubtypeFromId(domain, req.GetAuthMethodId())
|
||||
switch st {
|
||||
case password.Subtype, oidc.Subtype:
|
||||
case password.Subtype, oidc.Subtype, ldap.Subtype:
|
||||
default:
|
||||
badFields[authMethodIdField] = "Unknown auth method type."
|
||||
}
|
||||
@@ -1267,6 +1414,14 @@ func transformAuthenticateRequestAttributes(msg proto.Message) error {
|
||||
default:
|
||||
return fmt.Errorf("%s: unknown command %q", op, authRequest.GetCommand())
|
||||
}
|
||||
case ldap.Subtype:
|
||||
newAttrs := &pbs.LdapLoginAttributes{}
|
||||
if err := handlers.StructToProto(attrs, newAttrs); err != nil {
|
||||
return err
|
||||
}
|
||||
authRequest.Attrs = &pbs.AuthenticateRequest_LdapLoginAttributes{
|
||||
LdapLoginAttributes: newAttrs,
|
||||
}
|
||||
default:
|
||||
return &subtypes.UnknownSubtypeIDError{
|
||||
ID: authRequest.GetAuthMethodId(),
|
||||
|
||||
@@ -5,6 +5,10 @@ package authmethods_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
@@ -12,6 +16,7 @@ import (
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/hashicorp/boundary/globals"
|
||||
"github.com/hashicorp/boundary/internal/auth/ldap"
|
||||
"github.com/hashicorp/boundary/internal/auth/oidc"
|
||||
"github.com/hashicorp/boundary/internal/auth/password"
|
||||
"github.com/hashicorp/boundary/internal/authtoken"
|
||||
@@ -27,7 +32,9 @@ import (
|
||||
"github.com/hashicorp/boundary/internal/types/scope"
|
||||
pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/authmethods"
|
||||
scopepb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/scopes"
|
||||
"golang.org/x/exp/slices"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/testing/protocmp"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
@@ -58,6 +65,13 @@ var (
|
||||
action.ChangeState.String(),
|
||||
action.Authenticate.String(),
|
||||
}
|
||||
ldapAuthorizedActions = []string{
|
||||
action.NoOp.String(),
|
||||
action.Read.String(),
|
||||
action.Update.String(),
|
||||
action.Delete.String(),
|
||||
action.Authenticate.String(),
|
||||
}
|
||||
)
|
||||
|
||||
var authorizedCollectionActions = map[string]*structpb.ListValue{
|
||||
@@ -87,6 +101,9 @@ func TestGet(t *testing.T) {
|
||||
oidcRepoFn := func() (*oidc.Repository, error) {
|
||||
return oidc.NewRepository(ctx, rw, rw, kmsCache)
|
||||
}
|
||||
ldapRepoFn := func() (*ldap.Repository, error) {
|
||||
return ldap.NewRepository(ctx, rw, rw, kmsCache)
|
||||
}
|
||||
pwRepoFn := func() (*password.Repository, error) {
|
||||
return password.NewRepository(rw, rw, kmsCache)
|
||||
}
|
||||
@@ -153,6 +170,32 @@ func TestGet(t *testing.T) {
|
||||
AuthorizedCollectionActions: authorizedCollectionActions,
|
||||
}
|
||||
|
||||
ldapAm := ldap.TestAuthMethod(t, conn, databaseWrapper, o.GetPublicId(), []string{"ldaps://ldap1"}, ldap.WithAccountAttributeMap(ctx, map[string]ldap.AccountToAttribute{
|
||||
"mail": ldap.ToEmailAttribute,
|
||||
}))
|
||||
wantLdap := &pb.AuthMethod{
|
||||
Id: ldapAm.GetPublicId(),
|
||||
ScopeId: ldapAm.GetScopeId(),
|
||||
CreatedTime: ldapAm.CreateTime.GetTimestamp(),
|
||||
UpdatedTime: ldapAm.UpdateTime.GetTimestamp(),
|
||||
Type: ldap.Subtype.String(),
|
||||
Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{
|
||||
LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{
|
||||
State: string(ldap.InactiveState),
|
||||
Urls: []string{"ldaps://ldap1"},
|
||||
AccountAttributeMaps: []string{"mail=email"},
|
||||
},
|
||||
},
|
||||
Version: 1,
|
||||
Scope: &scopepb.ScopeInfo{
|
||||
Id: o.GetPublicId(),
|
||||
Type: o.GetType(),
|
||||
ParentScopeId: scope.Global.String(),
|
||||
},
|
||||
AuthorizedActions: ldapAuthorizedActions,
|
||||
AuthorizedCollectionActions: authorizedCollectionActions,
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
scopeId string
|
||||
@@ -173,7 +216,13 @@ func TestGet(t *testing.T) {
|
||||
res: &pbs.GetAuthMethodResponse{Item: wantOidc},
|
||||
},
|
||||
{
|
||||
name: "Get a non existant AuthMethod",
|
||||
name: "Get an Existing LDAP AuthMethod",
|
||||
scopeId: o.GetPublicId(),
|
||||
req: &pbs.GetAuthMethodRequest{Id: ldapAm.GetPublicId()},
|
||||
res: &pbs.GetAuthMethodResponse{Item: wantLdap},
|
||||
},
|
||||
{
|
||||
name: "Get a non existent AuthMethod",
|
||||
scopeId: o.GetPublicId(),
|
||||
req: &pbs.GetAuthMethodRequest{Id: globals.PasswordAuthMethodPrefix + "_DoesntExis"},
|
||||
res: nil,
|
||||
@@ -198,7 +247,7 @@ func TestGet(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
|
||||
s, err := authmethods.NewService(kmsCache, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn)
|
||||
s, err := authmethods.NewService(kmsCache, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn)
|
||||
require.NoError(err, "Couldn't create new auth_method service.")
|
||||
|
||||
got, gErr := s.GetAuthMethod(requestauth.DisabledAuthTestContext(iamRepoFn, tc.scopeId), tc.req)
|
||||
@@ -228,6 +277,9 @@ func TestList(t *testing.T) {
|
||||
oidcRepoFn := func() (*oidc.Repository, error) {
|
||||
return oidc.NewRepository(ctx, rw, rw, kmsCache)
|
||||
}
|
||||
ldapRepoFn := func() (*ldap.Repository, error) {
|
||||
return ldap.NewRepository(ctx, rw, rw, kmsCache)
|
||||
}
|
||||
pwRepoFn := func() (*password.Repository, error) {
|
||||
return password.NewRepository(rw, rw, kmsCache)
|
||||
}
|
||||
@@ -293,6 +345,42 @@ func TestList(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
sorterFn := func(a *pb.AuthMethod, b *pb.AuthMethod) bool {
|
||||
switch {
|
||||
case a.GetId() > b.GetId():
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
cpSorted := func(ams []*pb.AuthMethod) []*pb.AuthMethod {
|
||||
cp := make([]*pb.AuthMethod, 0, len(ams))
|
||||
for _, a := range ams {
|
||||
cp = append(cp, proto.Clone(a).(*pb.AuthMethod))
|
||||
}
|
||||
slices.SortFunc(cp, sorterFn)
|
||||
return cp
|
||||
}
|
||||
|
||||
ldapAm := ldap.TestAuthMethod(t, conn, databaseWrapper, oWithAuthMethods.GetPublicId(), []string{"ldaps://ldap1"}, ldap.WithOperationalState(ctx, ldap.ActivePublicState))
|
||||
wantSomeAuthMethods = append(wantSomeAuthMethods, &pb.AuthMethod{
|
||||
Id: ldapAm.GetPublicId(),
|
||||
ScopeId: oWithAuthMethods.GetPublicId(),
|
||||
CreatedTime: ldapAm.GetCreateTime().GetTimestamp(),
|
||||
UpdatedTime: ldapAm.GetUpdateTime().GetTimestamp(),
|
||||
Scope: &scopepb.ScopeInfo{Id: oWithAuthMethods.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()},
|
||||
Version: 1,
|
||||
Type: ldap.Subtype.String(),
|
||||
Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{
|
||||
LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{
|
||||
State: string(ldap.ActivePublicState),
|
||||
Urls: []string{"ldaps://ldap1"},
|
||||
},
|
||||
},
|
||||
AuthorizedActions: ldapAuthorizedActions,
|
||||
AuthorizedCollectionActions: authorizedCollectionActions,
|
||||
})
|
||||
|
||||
var wantOtherAuthMethods []*pb.AuthMethod
|
||||
for _, aa := range password.TestAuthMethods(t, conn, oWithOtherAuthMethods.GetPublicId(), 3) {
|
||||
wantOtherAuthMethods = append(wantOtherAuthMethods, &pb.AuthMethod{
|
||||
@@ -323,12 +411,12 @@ func TestList(t *testing.T) {
|
||||
{
|
||||
name: "List Some Auth Methods",
|
||||
req: &pbs.ListAuthMethodsRequest{ScopeId: oWithAuthMethods.GetPublicId()},
|
||||
res: &pbs.ListAuthMethodsResponse{Items: wantSomeAuthMethods},
|
||||
res: &pbs.ListAuthMethodsResponse{Items: cpSorted(wantSomeAuthMethods)},
|
||||
},
|
||||
{
|
||||
name: "List Other Auth Methods",
|
||||
req: &pbs.ListAuthMethodsRequest{ScopeId: oWithOtherAuthMethods.GetPublicId()},
|
||||
res: &pbs.ListAuthMethodsResponse{Items: wantOtherAuthMethods},
|
||||
res: &pbs.ListAuthMethodsResponse{Items: cpSorted(wantOtherAuthMethods)},
|
||||
},
|
||||
{
|
||||
name: "List No Auth Methods",
|
||||
@@ -344,7 +432,9 @@ func TestList(t *testing.T) {
|
||||
name: "List All Auth Methods Recursively",
|
||||
req: &pbs.ListAuthMethodsRequest{ScopeId: "global", Recursive: true},
|
||||
res: &pbs.ListAuthMethodsResponse{
|
||||
Items: append(wantSomeAuthMethods, wantOtherAuthMethods...),
|
||||
Items: func() []*pb.AuthMethod {
|
||||
return cpSorted(append(wantSomeAuthMethods, wantOtherAuthMethods...))
|
||||
}(),
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -353,7 +443,7 @@ func TestList(t *testing.T) {
|
||||
ScopeId: "global", Recursive: true,
|
||||
Filter: fmt.Sprintf(`"/item/scope/id"==%q`, oWithAuthMethods.GetPublicId()),
|
||||
},
|
||||
res: &pbs.ListAuthMethodsResponse{Items: wantSomeAuthMethods},
|
||||
res: &pbs.ListAuthMethodsResponse{Items: cpSorted(wantSomeAuthMethods)},
|
||||
},
|
||||
{
|
||||
name: "Filter All Auth Methods",
|
||||
@@ -369,7 +459,7 @@ func TestList(t *testing.T) {
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
s, err := authmethods.NewService(kmsCache, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn)
|
||||
s, err := authmethods.NewService(kmsCache, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn)
|
||||
require.NoError(err, "Couldn't create new auth_method service.")
|
||||
|
||||
// First check with non-anonymous user
|
||||
@@ -385,7 +475,11 @@ func TestList(t *testing.T) {
|
||||
assert.NotEqual("secret", oidcAttrs.ClientSecretHmac)
|
||||
}
|
||||
}
|
||||
assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform(), protocmp.IgnoreFields(&pb.OidcAuthMethodAttributes{}, "client_secret_hmac")),
|
||||
|
||||
slices.SortFunc(got.Items, sorterFn)
|
||||
assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform(),
|
||||
protocmp.IgnoreFields(&pb.OidcAuthMethodAttributes{}, "client_secret_hmac"),
|
||||
protocmp.IgnoreFields(&pb.LdapAuthMethodAttributes{}, "bind_password_hmac", "client_certificate_key_hmac")),
|
||||
"ListAuthMethods() for scope %q got response %q, wanted %q", tc.req.GetScopeId(), got, tc.res)
|
||||
|
||||
// Now check with anonymous user
|
||||
@@ -414,6 +508,9 @@ func TestDelete(t *testing.T) {
|
||||
oidcRepoFn := func() (*oidc.Repository, error) {
|
||||
return oidc.NewRepository(ctx, rw, rw, kmsCache)
|
||||
}
|
||||
ldapRepoFn := func() (*ldap.Repository, error) {
|
||||
return ldap.NewRepository(ctx, rw, rw, kmsCache)
|
||||
}
|
||||
pwRepoFn := func() (*password.Repository, error) {
|
||||
return password.NewRepository(rw, rw, kmsCache)
|
||||
}
|
||||
@@ -430,7 +527,9 @@ func TestDelete(t *testing.T) {
|
||||
oidcam := oidc.TestAuthMethod(t, conn, databaseWrapper, o.GetPublicId(), oidc.InactiveState, "alice_rp", "my-dogs-name",
|
||||
oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://alice.com")[0]), oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://api.com")[0]))
|
||||
|
||||
s, err := authmethods.NewService(kmsCache, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn)
|
||||
ldapAm := ldap.TestAuthMethod(t, conn, databaseWrapper, o.GetPublicId(), []string{"ldaps://ldap1"})
|
||||
|
||||
s, err := authmethods.NewService(kmsCache, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn)
|
||||
require.NoError(t, err, "Error when getting new auth_method service.")
|
||||
|
||||
cases := []struct {
|
||||
@@ -453,6 +552,13 @@ func TestDelete(t *testing.T) {
|
||||
},
|
||||
res: &pbs.DeleteAuthMethodResponse{},
|
||||
},
|
||||
{
|
||||
name: "Delete an Existing LDAP AuthMethod",
|
||||
req: &pbs.DeleteAuthMethodRequest{
|
||||
Id: ldapAm.GetPublicId(),
|
||||
},
|
||||
res: &pbs.DeleteAuthMethodResponse{},
|
||||
},
|
||||
{
|
||||
name: "Delete bad auth_method id",
|
||||
req: &pbs.DeleteAuthMethodRequest{
|
||||
@@ -494,6 +600,9 @@ func TestDelete_twice(t *testing.T) {
|
||||
oidcRepoFn := func() (*oidc.Repository, error) {
|
||||
return oidc.NewRepository(ctx, rw, rw, kms)
|
||||
}
|
||||
ldapRepoFn := func() (*ldap.Repository, error) {
|
||||
return ldap.NewRepository(ctx, rw, rw, kms)
|
||||
}
|
||||
pwRepoFn := func() (*password.Repository, error) {
|
||||
return password.NewRepository(rw, rw, kms)
|
||||
}
|
||||
@@ -505,7 +614,7 @@ func TestDelete_twice(t *testing.T) {
|
||||
o, _ := iam.TestScopes(t, iamRepo)
|
||||
am := password.TestAuthMethods(t, conn, o.GetPublicId(), 1)[0]
|
||||
|
||||
s, err := authmethods.NewService(kms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn)
|
||||
s, err := authmethods.NewService(kms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn)
|
||||
require.NoError(err, "Error when getting new auth_method service.")
|
||||
|
||||
req := &pbs.DeleteAuthMethodRequest{
|
||||
@@ -523,18 +632,21 @@ func TestCreate(t *testing.T) {
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
rw := db.New(conn)
|
||||
wrapper := db.TestWrapper(t)
|
||||
kms := kms.TestKms(t, conn, wrapper)
|
||||
testKms := kms.TestKms(t, conn, wrapper)
|
||||
iamRepoFn := func() (*iam.Repository, error) {
|
||||
return iam.TestRepo(t, conn, wrapper), nil
|
||||
}
|
||||
pwRepoFn := func() (*password.Repository, error) {
|
||||
return password.NewRepository(rw, rw, kms)
|
||||
return password.NewRepository(rw, rw, testKms)
|
||||
}
|
||||
oidcRepoFn := func() (*oidc.Repository, error) {
|
||||
return oidc.NewRepository(ctx, rw, rw, kms)
|
||||
return oidc.NewRepository(ctx, rw, rw, testKms)
|
||||
}
|
||||
ldapRepoFn := func() (*ldap.Repository, error) {
|
||||
return ldap.NewRepository(ctx, rw, rw, testKms)
|
||||
}
|
||||
atRepoFn := func() (*authtoken.Repository, error) {
|
||||
return authtoken.NewRepository(rw, rw, kms)
|
||||
return authtoken.NewRepository(rw, rw, testKms)
|
||||
}
|
||||
iamRepo := iam.TestRepo(t, conn, wrapper)
|
||||
|
||||
@@ -542,12 +654,20 @@ func TestCreate(t *testing.T) {
|
||||
defaultAm := password.TestAuthMethods(t, conn, o.GetPublicId(), 1)[0]
|
||||
defaultCreated := defaultAm.GetCreateTime().GetTimestamp()
|
||||
|
||||
_, testEncodedCert := ldap.TestGenerateCA(t, "localhost")
|
||||
_, privKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
require.NoError(t, err)
|
||||
derEncodedKey, err := x509.MarshalPKCS8PrivateKey(privKey)
|
||||
require.NoError(t, err)
|
||||
testEncodedKey := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: derEncodedKey})
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
req *pbs.CreateAuthMethodRequest
|
||||
res *pbs.CreateAuthMethodResponse
|
||||
idPrefix string
|
||||
err error
|
||||
name string
|
||||
req *pbs.CreateAuthMethodRequest
|
||||
res *pbs.CreateAuthMethodResponse
|
||||
idPrefix string
|
||||
err error
|
||||
errContains string
|
||||
}{
|
||||
{
|
||||
name: "Create a valid Password AuthMethod",
|
||||
@@ -627,6 +747,74 @@ func TestCreate(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "create-a-valid-ldap-auth-method",
|
||||
req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{
|
||||
ScopeId: o.GetPublicId(),
|
||||
Type: ldap.Subtype.String(),
|
||||
Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{
|
||||
LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{
|
||||
StartTls: true,
|
||||
InsecureTls: true,
|
||||
DiscoverDn: true,
|
||||
AnonGroupSearch: true,
|
||||
UpnDomain: wrapperspb.String("upn_domain"),
|
||||
Urls: []string{"ldap://ldap1", "ldaps://ldap1"},
|
||||
BindDn: wrapperspb.String("bind-dn"),
|
||||
BindPassword: wrapperspb.String("bind-password"),
|
||||
UserDn: wrapperspb.String("user-dn"),
|
||||
UserAttr: wrapperspb.String("user-attr"),
|
||||
UserFilter: wrapperspb.String("user-filter"),
|
||||
EnableGroups: true,
|
||||
GroupDn: wrapperspb.String("group-dn"),
|
||||
GroupAttr: wrapperspb.String("group-attr"),
|
||||
GroupFilter: wrapperspb.String("group-filter"),
|
||||
Certificates: []string{testEncodedCert},
|
||||
ClientCertificate: wrapperspb.String(testEncodedCert),
|
||||
ClientCertificateKey: wrapperspb.String(string(testEncodedKey)),
|
||||
UseTokenGroups: true,
|
||||
AccountAttributeMaps: []string{"mail=email"},
|
||||
},
|
||||
},
|
||||
}},
|
||||
idPrefix: globals.LdapAuthMethodPrefix + "_",
|
||||
res: &pbs.CreateAuthMethodResponse{
|
||||
Uri: fmt.Sprintf("auth-methods/%s_", globals.LdapAuthMethodPrefix),
|
||||
Item: &pb.AuthMethod{
|
||||
ScopeId: o.GetPublicId(),
|
||||
CreatedTime: defaultAm.GetCreateTime().GetTimestamp(),
|
||||
UpdatedTime: defaultAm.GetUpdateTime().GetTimestamp(),
|
||||
Scope: &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: o.GetType(), ParentScopeId: scope.Global.String()},
|
||||
Version: 1,
|
||||
Type: ldap.Subtype.String(),
|
||||
Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{
|
||||
LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{
|
||||
State: string(ldap.InactiveState),
|
||||
StartTls: true,
|
||||
InsecureTls: true,
|
||||
DiscoverDn: true,
|
||||
AnonGroupSearch: true,
|
||||
UpnDomain: wrapperspb.String("upn_domain"),
|
||||
Urls: []string{"ldap://ldap1", "ldaps://ldap1"},
|
||||
BindDn: wrapperspb.String("bind-dn"),
|
||||
UserDn: wrapperspb.String("user-dn"),
|
||||
UserAttr: wrapperspb.String("user-attr"),
|
||||
UserFilter: wrapperspb.String("user-filter"),
|
||||
EnableGroups: true,
|
||||
GroupDn: wrapperspb.String("group-dn"),
|
||||
GroupAttr: wrapperspb.String("group-attr"),
|
||||
GroupFilter: wrapperspb.String("group-filter"),
|
||||
Certificates: []string{testEncodedCert},
|
||||
ClientCertificate: wrapperspb.String(testEncodedCert),
|
||||
UseTokenGroups: true,
|
||||
AccountAttributeMaps: []string{"mail=email"},
|
||||
},
|
||||
},
|
||||
AuthorizedActions: ldapAuthorizedActions,
|
||||
AuthorizedCollectionActions: authorizedCollectionActions,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Create a global Password AuthMethod",
|
||||
req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{
|
||||
@@ -698,6 +886,42 @@ func TestCreate(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "create-a-global-ldap-auth-method",
|
||||
req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{
|
||||
ScopeId: scope.Global.String(),
|
||||
Type: ldap.Subtype.String(),
|
||||
Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{
|
||||
LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{
|
||||
Urls: []string{"ldap://ldap1", "ldaps://ldap1"},
|
||||
EnableGroups: true,
|
||||
GroupDn: wrapperspb.String("group-dn"),
|
||||
},
|
||||
},
|
||||
}},
|
||||
idPrefix: globals.LdapAuthMethodPrefix + "_",
|
||||
res: &pbs.CreateAuthMethodResponse{
|
||||
Uri: fmt.Sprintf("auth-methods/%s_", globals.LdapAuthMethodPrefix),
|
||||
Item: &pb.AuthMethod{
|
||||
ScopeId: scope.Global.String(),
|
||||
CreatedTime: defaultAm.GetCreateTime().GetTimestamp(),
|
||||
UpdatedTime: defaultAm.GetUpdateTime().GetTimestamp(),
|
||||
Scope: &scopepb.ScopeInfo{Id: scope.Global.String(), Type: scope.Global.String(), Name: scope.Global.String(), Description: "Global Scope"},
|
||||
Version: 1,
|
||||
Type: ldap.Subtype.String(),
|
||||
Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{
|
||||
LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{
|
||||
State: string(ldap.InactiveState),
|
||||
Urls: []string{"ldap://ldap1", "ldaps://ldap1"},
|
||||
EnableGroups: true,
|
||||
GroupDn: wrapperspb.String("group-dn"),
|
||||
},
|
||||
},
|
||||
AuthorizedActions: ldapAuthorizedActions,
|
||||
AuthorizedCollectionActions: authorizedCollectionActions,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Can't specify Id",
|
||||
req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{
|
||||
@@ -955,18 +1179,186 @@ func TestCreate(t *testing.T) {
|
||||
}},
|
||||
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
|
||||
},
|
||||
{
|
||||
name: "ldap-auth-method-requires-urls",
|
||||
req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{
|
||||
ScopeId: o.GetPublicId(),
|
||||
Type: ldap.Subtype.String(),
|
||||
Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{
|
||||
LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{},
|
||||
},
|
||||
}},
|
||||
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
|
||||
errContains: "At least one URL is required",
|
||||
},
|
||||
{
|
||||
name: "ldap-auth-method-invalid-urls",
|
||||
req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{
|
||||
ScopeId: o.GetPublicId(),
|
||||
Type: ldap.Subtype.String(),
|
||||
Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{
|
||||
LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{
|
||||
Urls: []string{"ldap://ldap1", "not-ldap-scheme://ldap2"},
|
||||
},
|
||||
},
|
||||
}},
|
||||
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
|
||||
errContains: "is not either ldap or ldaps",
|
||||
},
|
||||
{
|
||||
name: "ldap-auth-method-invalid-cert",
|
||||
req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{
|
||||
ScopeId: o.GetPublicId(),
|
||||
Type: ldap.Subtype.String(),
|
||||
Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{
|
||||
LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{
|
||||
Urls: []string{"ldap://ldap1"},
|
||||
Certificates: []string{"invalid-cert"},
|
||||
},
|
||||
},
|
||||
}},
|
||||
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
|
||||
errContains: "failed to parse certificate: invalid PEM encoding",
|
||||
},
|
||||
{
|
||||
name: "ldap-auth-method-missing-bind-dn",
|
||||
req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{
|
||||
ScopeId: o.GetPublicId(),
|
||||
Type: ldap.Subtype.String(),
|
||||
Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{
|
||||
LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{
|
||||
Urls: []string{"ldap://ldap1"},
|
||||
BindPassword: wrapperspb.String("pass"),
|
||||
},
|
||||
},
|
||||
}},
|
||||
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
|
||||
errContains: "attributes.bind_password is missing required attributes.bind_dn field",
|
||||
},
|
||||
{
|
||||
name: "ldap-auth-method-missing-bind-password",
|
||||
req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{
|
||||
ScopeId: o.GetPublicId(),
|
||||
Type: ldap.Subtype.String(),
|
||||
Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{
|
||||
LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{
|
||||
Urls: []string{"ldap://ldap1"},
|
||||
BindDn: wrapperspb.String("bind-dn"),
|
||||
},
|
||||
},
|
||||
}},
|
||||
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
|
||||
errContains: "attributes.bind_dn is missing required attributes.bind_password field",
|
||||
},
|
||||
{
|
||||
name: "ldap-auth-method-invalid-client-cert",
|
||||
req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{
|
||||
ScopeId: o.GetPublicId(),
|
||||
Type: ldap.Subtype.String(),
|
||||
Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{
|
||||
LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{
|
||||
Urls: []string{"ldap://ldap1"},
|
||||
ClientCertificate: wrapperspb.String("invalid-cert"),
|
||||
ClientCertificateKey: wrapperspb.String(string(testEncodedKey)),
|
||||
},
|
||||
},
|
||||
}},
|
||||
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
|
||||
errContains: "failed to parse certificate: invalid PEM encoding",
|
||||
},
|
||||
{
|
||||
name: "ldap-auth-method-invalid-client-cert-key",
|
||||
req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{
|
||||
ScopeId: o.GetPublicId(),
|
||||
Type: ldap.Subtype.String(),
|
||||
Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{
|
||||
LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{
|
||||
Urls: []string{"ldap://ldap1"},
|
||||
ClientCertificate: wrapperspb.String(testEncodedCert),
|
||||
ClientCertificateKey: wrapperspb.String("invalid-key"),
|
||||
},
|
||||
},
|
||||
}},
|
||||
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
|
||||
errContains: "attributes.client_certificate_key is not encoded as a valid pem",
|
||||
},
|
||||
{
|
||||
name: "ldap-auth-method-client-cert-key-not-a-key",
|
||||
req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{
|
||||
ScopeId: o.GetPublicId(),
|
||||
Type: ldap.Subtype.String(),
|
||||
Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{
|
||||
LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{
|
||||
Urls: []string{"ldap://ldap1"},
|
||||
ClientCertificate: wrapperspb.String(testEncodedCert),
|
||||
ClientCertificateKey: wrapperspb.String(testEncodedCert),
|
||||
},
|
||||
},
|
||||
}},
|
||||
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
|
||||
errContains: "attributes.client_certificate_key is not a valid private key",
|
||||
},
|
||||
{
|
||||
name: "ldap-auth-method-missing-client-cert-key",
|
||||
req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{
|
||||
ScopeId: o.GetPublicId(),
|
||||
Type: ldap.Subtype.String(),
|
||||
Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{
|
||||
LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{
|
||||
Urls: []string{"ldap://ldap1"},
|
||||
ClientCertificate: wrapperspb.String(testEncodedCert),
|
||||
},
|
||||
},
|
||||
}},
|
||||
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
|
||||
errContains: "attributes.client_certificate is missing required attributes.client_certificate_key field",
|
||||
},
|
||||
{
|
||||
name: "ldap-auth-method-missing-client-cert",
|
||||
req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{
|
||||
ScopeId: o.GetPublicId(),
|
||||
Type: ldap.Subtype.String(),
|
||||
Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{
|
||||
LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{
|
||||
Urls: []string{"ldap://ldap1"},
|
||||
ClientCertificateKey: wrapperspb.String(string(testEncodedKey)),
|
||||
},
|
||||
},
|
||||
}},
|
||||
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
|
||||
errContains: "attributes.client_certificate_key is missing required attributes.client_certificate field",
|
||||
},
|
||||
{
|
||||
name: "ldap-auth-method-invalid-attribute-map",
|
||||
req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{
|
||||
ScopeId: o.GetPublicId(),
|
||||
Type: ldap.Subtype.String(),
|
||||
Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{
|
||||
LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{
|
||||
Urls: []string{"ldap://ldap1"},
|
||||
AccountAttributeMaps: []string{"invalid-map"},
|
||||
},
|
||||
},
|
||||
}},
|
||||
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
|
||||
errContains: "invalid attributes.account_attribute_maps (unable to parse)",
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
|
||||
s, err := authmethods.NewService(kms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn)
|
||||
s, err := authmethods.NewService(testKms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn)
|
||||
require.NoError(err, "Error when getting new auth_method service.")
|
||||
|
||||
conn.Debug(true)
|
||||
got, gErr := s.CreateAuthMethod(requestauth.DisabledAuthTestContext(iamRepoFn, tc.req.GetItem().GetScopeId()), tc.req)
|
||||
if tc.err != nil {
|
||||
require.Error(gErr)
|
||||
assert.True(errors.Is(gErr, tc.err), "CreateAuthMethod(%+v) got error %v, wanted %v", tc.req, gErr, tc.err)
|
||||
if tc.errContains != "" {
|
||||
assert.Contains(gErr.Error(), tc.errContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(gErr)
|
||||
@@ -1003,6 +1395,14 @@ func TestCreate(t *testing.T) {
|
||||
protocmp.IgnoreFields(&pb.OidcAuthMethodAttributes{}, "client_secret_hmac", "callback_url"),
|
||||
)
|
||||
}
|
||||
if ldapAttrs := got.Item.GetLdapAuthMethodsAttributes(); ldapAttrs != nil {
|
||||
assert.NotEqual(ldapAttrs.BindPassword, ldapAttrs.BindPasswordHmac)
|
||||
cmpOptions = append(
|
||||
cmpOptions,
|
||||
protocmp.SortRepeatedFields(&pb.LdapAuthMethodAttributes{}, "account_attribute_maps", "urls", "certificates"),
|
||||
protocmp.IgnoreFields(&pb.LdapAuthMethodAttributes{}, "bind_password_hmac", "client_certificate_key_hmac"),
|
||||
)
|
||||
}
|
||||
}
|
||||
assert.Empty(cmp.Diff(got, tc.res, cmpOptions...), "CreateAuthMethod(%q) got response %q, wanted %q", tc.req, got, tc.res)
|
||||
})
|
||||
|
||||
@@ -51,6 +51,29 @@ func TestTransformAuthenticateRequestAttributes(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ldap-attributes",
|
||||
input: &pbs.AuthenticateRequest{
|
||||
AuthMethodId: "amldap_test",
|
||||
Attrs: &pbs.AuthenticateRequest_Attributes{
|
||||
Attributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"login_name": structpb.NewStringValue("login-name"),
|
||||
"password": structpb.NewStringValue("password"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: &pbs.AuthenticateRequest{
|
||||
AuthMethodId: "amldap_test",
|
||||
Attrs: &pbs.AuthenticateRequest_LdapLoginAttributes{
|
||||
LdapLoginAttributes: &pbs.LdapLoginAttributes{
|
||||
LoginName: "login-name",
|
||||
Password: "password",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "oidc-start-attributes",
|
||||
input: &pbs.AuthenticateRequest{
|
||||
@@ -218,6 +241,20 @@ func TestTransformAuthenticateRequestAttributesErrors(t *testing.T) {
|
||||
},
|
||||
}))
|
||||
})
|
||||
t.Run("invalid-ldap-attributes", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
require.Error(t, transformAuthenticateRequestAttributes(&pbs.AuthenticateRequest{
|
||||
AuthMethodId: "amldap_test",
|
||||
Attrs: &pbs.AuthenticateRequest_Attributes{
|
||||
Attributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
"field1": structpb.NewBoolValue(true),
|
||||
"field2": structpb.NewStringValue("value2"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
})
|
||||
t.Run("invalid-oidc-start-attributes", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
require.Error(t, transformAuthenticateRequestAttributes(&pbs.AuthenticateRequest{
|
||||
|
||||
@@ -0,0 +1,366 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package authmethods
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/auth/ldap"
|
||||
ldapstore "github.com/hashicorp/boundary/internal/auth/ldap/store"
|
||||
"github.com/hashicorp/boundary/internal/daemon/controller/auth"
|
||||
"github.com/hashicorp/boundary/internal/daemon/controller/handlers"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services"
|
||||
"github.com/hashicorp/boundary/internal/types/action"
|
||||
pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/authmethods"
|
||||
"google.golang.org/grpc/codes"
|
||||
)
|
||||
|
||||
var ldapMaskManager handlers.MaskManager
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
if ldapMaskManager, err = handlers.NewMaskManager(handlers.MaskDestination{&ldapstore.AuthMethod{}}, handlers.MaskSource{&pb.AuthMethod{}, &pb.LdapAuthMethodAttributes{}}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
IdActions[ldap.Subtype] = action.ActionSet{
|
||||
action.NoOp,
|
||||
action.Read,
|
||||
action.Update,
|
||||
action.Delete,
|
||||
action.Authenticate,
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
urlsField = "attributes.urls"
|
||||
bindDnField = "attributes.bind_dn"
|
||||
bindPasswordField = "attributes.bind_password"
|
||||
clientCertificateField = "attributes.client_certificate"
|
||||
clientCertificateKeyField = "attributes.client_certificate_key"
|
||||
certificatesField = "attributes.certificates"
|
||||
accountAttributesMapField = "attributes.account_attribute_maps"
|
||||
)
|
||||
|
||||
func (s Service) authenticateLdap(ctx context.Context, req *pbs.AuthenticateRequest, authResults *auth.VerifyResults) (*pbs.AuthenticateResponse, error) {
|
||||
const op = "authmethod_service.(Service).authenticateLdap"
|
||||
if req == nil {
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "Nil request.")
|
||||
}
|
||||
if authResults == nil {
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "Nil auth results.")
|
||||
}
|
||||
reqAttrs := req.GetLdapLoginAttributes()
|
||||
|
||||
ldapRepo, err := s.ldapRepoFn()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
ldapFn := func() (ldap.Authenticator, error) {
|
||||
return ldapRepo, nil
|
||||
}
|
||||
|
||||
iamRepo, err := s.iamRepoFn()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
iamFn := func() (ldap.LookupUser, error) {
|
||||
return iamRepo, nil
|
||||
}
|
||||
|
||||
atRepo, err := s.atRepoFn()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
atFn := func() (ldap.AuthTokenCreator, error) {
|
||||
return atRepo, nil
|
||||
}
|
||||
|
||||
rawTk, err := ldap.Authenticate(ctx, ldapFn, iamFn, atFn, req.GetAuthMethodId(), reqAttrs.GetLoginName(), reqAttrs.GetPassword())
|
||||
if err != nil {
|
||||
// let's not send back too much info about the error
|
||||
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Unauthenticated, "Unable to authenticate.")
|
||||
}
|
||||
tk, err := s.ConvertInternalAuthTokenToApiAuthToken(
|
||||
ctx,
|
||||
rawTk,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
|
||||
return s.convertToAuthenticateResponse(ctx, req, authResults, tk)
|
||||
}
|
||||
|
||||
// createLdapInRepo creates an ldap auth method in a repo and returns the result.
|
||||
// This method should never return a nil AuthMethod without returning an error.
|
||||
func (s Service) createLdapInRepo(ctx context.Context, scopeId string, item *pb.AuthMethod) (*ldap.AuthMethod, error) {
|
||||
u, err := toStorageLdapAuthMethod(ctx, scopeId, item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
repo, err := s.ldapRepoFn()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out, err := repo.CreateAuthMethod(ctx, u)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create auth method: %w", err)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s Service) updateLdapInRepo(ctx context.Context, scopeId, id string, mask []string, item *pb.AuthMethod) (*ldap.AuthMethod, error) {
|
||||
u, err := toStorageLdapAuthMethod(ctx, scopeId, item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
version := item.GetVersion()
|
||||
u.PublicId = id
|
||||
|
||||
dbMask := ldapMaskManager.Translate(mask)
|
||||
if len(dbMask) == 0 {
|
||||
return nil, handlers.InvalidArgumentErrorf("No valid fields included in the update mask.", map[string]string{"update_mask": "No valid fields provided in the update mask."})
|
||||
}
|
||||
|
||||
repo, err := s.ldapRepoFn()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out, rowsUpdated, err := repo.UpdateAuthMethod(ctx, u, version, dbMask)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to update auth method: %w", err)
|
||||
}
|
||||
if rowsUpdated == 0 {
|
||||
return nil, handlers.NotFoundErrorf("AuthMethod %q doesn't exist or incorrect version provided or no changes were made to the existing AuthMethod.", id)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func toStorageLdapAuthMethod(ctx context.Context, scopeId string, in *pb.AuthMethod) (out *ldap.AuthMethod, err error) {
|
||||
const op = "authmethod_service.toStorageLdapAuthMethod"
|
||||
if in == nil {
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "nil auth method.")
|
||||
}
|
||||
attrs := in.GetLdapAuthMethodsAttributes()
|
||||
|
||||
var opts []ldap.Option
|
||||
if in.GetName() != nil {
|
||||
opts = append(opts, ldap.WithName(ctx, in.GetName().GetValue()))
|
||||
}
|
||||
if in.GetDescription() != nil {
|
||||
opts = append(opts, ldap.WithDescription(ctx, in.GetDescription().GetValue()))
|
||||
}
|
||||
var urls []*url.URL
|
||||
if attrs != nil {
|
||||
if attrs.GetState() != "" {
|
||||
opts = append(opts, ldap.WithOperationalState(ctx, ldap.AuthMethodState(attrs.GetState())))
|
||||
}
|
||||
if attrs.StartTls {
|
||||
opts = append(opts, ldap.WithStartTLS(ctx))
|
||||
}
|
||||
if attrs.InsecureTls {
|
||||
opts = append(opts, ldap.WithInsecureTLS(ctx))
|
||||
}
|
||||
if attrs.DiscoverDn {
|
||||
opts = append(opts, ldap.WithDiscoverDn(ctx))
|
||||
}
|
||||
if attrs.AnonGroupSearch {
|
||||
opts = append(opts, ldap.WithAnonGroupSearch(ctx))
|
||||
}
|
||||
if attrs.UpnDomain.GetValue() != "" {
|
||||
opts = append(opts, ldap.WithUpnDomain(ctx, attrs.UpnDomain.GetValue()))
|
||||
}
|
||||
if attrs.UserDn.GetValue() != "" {
|
||||
opts = append(opts, ldap.WithUserDn(ctx, attrs.UserDn.GetValue()))
|
||||
}
|
||||
if attrs.UserAttr.GetValue() != "" {
|
||||
opts = append(opts, ldap.WithUserAttr(ctx, attrs.UserAttr.GetValue()))
|
||||
}
|
||||
if attrs.UserFilter.GetValue() != "" {
|
||||
opts = append(opts, ldap.WithUserFilter(ctx, attrs.UserFilter.GetValue()))
|
||||
}
|
||||
if attrs.EnableGroups {
|
||||
opts = append(opts, ldap.WithEnableGroups(ctx))
|
||||
}
|
||||
if attrs.GroupDn.GetValue() != "" {
|
||||
opts = append(opts, ldap.WithGroupDn(ctx, attrs.GroupDn.GetValue()))
|
||||
}
|
||||
if attrs.GroupAttr.GetValue() != "" {
|
||||
opts = append(opts, ldap.WithGroupAttr(ctx, attrs.GroupAttr.GetValue()))
|
||||
}
|
||||
if attrs.GroupFilter.GetValue() != "" {
|
||||
opts = append(opts, ldap.WithGroupFilter(ctx, attrs.GroupFilter.GetValue()))
|
||||
}
|
||||
if len(attrs.Certificates) > 0 {
|
||||
certs, err := ldap.ParseCertificates(ctx, attrs.Certificates...)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
opts = append(opts, ldap.WithCertificates(ctx, certs...))
|
||||
}
|
||||
if attrs.ClientCertificate != nil || attrs.ClientCertificateKey != nil {
|
||||
keyBlk, _ := pem.Decode([]byte(attrs.ClientCertificateKey.GetValue()))
|
||||
if keyBlk == nil {
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unable to parse %s PEM", clientCertificateKeyField))
|
||||
}
|
||||
certBlk, _ := pem.Decode([]byte(attrs.ClientCertificate.GetValue()))
|
||||
if certBlk == nil {
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unable to parse %s PEM", clientCertificateField))
|
||||
}
|
||||
cc, err := x509.ParseCertificate(certBlk.Bytes)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to parse %s ASN.1 DER", clientCertificateField))
|
||||
}
|
||||
opts = append(opts, ldap.WithClientCertificate(ctx, keyBlk.Bytes, cc))
|
||||
}
|
||||
if attrs.BindDn.GetValue() != "" || attrs.BindPassword.GetValue() != "" {
|
||||
opts = append(opts, ldap.WithBindCredential(ctx, attrs.BindDn.GetValue(), attrs.BindPassword.GetValue()))
|
||||
}
|
||||
if attrs.UseTokenGroups {
|
||||
opts = append(opts, ldap.WithUseTokenGroups(ctx))
|
||||
}
|
||||
if len(attrs.AccountAttributeMaps) > 0 {
|
||||
attribMaps, err := ldap.ParseAccountAttributeMaps(ctx, attrs.AccountAttributeMaps...)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to parse %s", accountAttributesMapField))
|
||||
}
|
||||
fromToMap := map[string]ldap.AccountToAttribute{}
|
||||
for _, m := range attribMaps {
|
||||
fromToMap[m.From] = ldap.AccountToAttribute(m.To)
|
||||
}
|
||||
opts = append(opts, ldap.WithAccountAttributeMap(ctx, fromToMap))
|
||||
}
|
||||
|
||||
if len(attrs.GetUrls()) > 0 {
|
||||
urls = make([]*url.URL, 0, len(attrs.GetUrls()))
|
||||
for _, urlStr := range attrs.GetUrls() {
|
||||
u, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to parse %q into a url", urlStr))
|
||||
}
|
||||
urls = append(urls, u)
|
||||
}
|
||||
opts = append(opts, ldap.WithUrls(ctx, urls...))
|
||||
}
|
||||
}
|
||||
u, err := ldap.NewAuthMethod(ctx, scopeId, opts...)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Match(errors.T(errors.InvalidParameter), err):
|
||||
return nil, handlers.ApiErrorWithCodeAndMessage(codes.InvalidArgument, err.Error())
|
||||
default:
|
||||
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to build auth method: %v.", err)
|
||||
}
|
||||
}
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// validateLdapAttributes implements a handlers.CustomValidatorFunc(...) to be
|
||||
// used when validating requests with ldap attributes.
|
||||
func validateLdapAttributes(ctx context.Context, attrs *pb.LdapAuthMethodAttributes, badFields map[string]string) {
|
||||
if attrs == nil {
|
||||
// LDAP attributes are required when creating an LDAP auth method.
|
||||
// badFields[attributesField] = "Attributes are required for creating an LDAP auth method."
|
||||
return
|
||||
}
|
||||
if len(attrs.GetUrls()) > 0 {
|
||||
badUrlMsgs := []string{}
|
||||
for _, rawUrl := range attrs.GetUrls() {
|
||||
u, err := url.Parse(rawUrl)
|
||||
if err != nil {
|
||||
badUrlMsgs = append(badUrlMsgs, fmt.Sprintf("%q is not a valid url", rawUrl))
|
||||
continue
|
||||
}
|
||||
if u.Scheme != "ldap" && u.Scheme != "ldaps" {
|
||||
badUrlMsgs = append(badUrlMsgs, fmt.Sprintf("%s scheme in url %q is not either ldap or ldaps", u.Scheme, u.String()))
|
||||
}
|
||||
}
|
||||
if len(badUrlMsgs) > 0 {
|
||||
badFields[urlsField] = strings.Join(badUrlMsgs, " / ")
|
||||
}
|
||||
}
|
||||
if len(attrs.GetCertificates()) > 0 {
|
||||
if _, err := ldap.ParseCertificates(ctx, attrs.GetCertificates()...); err != nil {
|
||||
badFields[certificatesField] = fmt.Sprintf("invalid %s: %s", certificatesField, err.Error())
|
||||
}
|
||||
}
|
||||
if attrs.GetBindDn().GetValue() != "" && attrs.GetBindPassword().GetValue() == "" {
|
||||
badFields[bindPasswordField] = fmt.Sprintf("%s is missing required %s field", bindDnField, bindPasswordField)
|
||||
}
|
||||
if attrs.GetBindPassword().GetValue() != "" && attrs.GetBindDn().GetValue() == "" {
|
||||
badFields[bindDnField] = fmt.Sprintf("%s is missing required %s field", bindPasswordField, bindDnField)
|
||||
}
|
||||
if attrs.GetClientCertificate().GetValue() != "" && attrs.GetClientCertificateKey().GetValue() == "" {
|
||||
badFields[clientCertificateKeyField] = fmt.Sprintf("%s is missing required %s field", clientCertificateField, clientCertificateKeyField)
|
||||
}
|
||||
if attrs.GetClientCertificateKey().GetValue() != "" && attrs.GetClientCertificate().GetValue() == "" {
|
||||
badFields[clientCertificateField] = fmt.Sprintf("%s is missing required %s field", clientCertificateKeyField, clientCertificateField)
|
||||
}
|
||||
if attrs.GetClientCertificate().GetValue() != "" {
|
||||
if _, err := ldap.ParseCertificates(ctx, attrs.GetClientCertificate().GetValue()); err != nil {
|
||||
badFields[clientCertificateField] = fmt.Sprintf("invalid %s: %s", clientCertificateField, err.Error())
|
||||
}
|
||||
}
|
||||
if attrs.GetClientCertificateKey().GetValue() != "" {
|
||||
blk, _ := pem.Decode([]byte(attrs.GetClientCertificateKey().GetValue()))
|
||||
if blk == nil || blk.Bytes == nil {
|
||||
badFields[clientCertificateKeyField] = fmt.Sprintf("%s is not encoded as a valid pem", clientCertificateKeyField)
|
||||
} else {
|
||||
if _, err := x509.ParsePKCS8PrivateKey(blk.Bytes); err != nil {
|
||||
badFields[clientCertificateKeyField] = fmt.Sprintf("%s is not a valid private key", clientCertificateKeyField)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(attrs.AccountAttributeMaps) > 0 {
|
||||
if _, err := ldap.ParseAccountAttributeMaps(ctx, attrs.AccountAttributeMaps...); err != nil {
|
||||
badFields[accountAttributesMapField] = fmt.Sprintf("invalid %s (unable to parse)", accountAttributesMapField)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func validateAuthenticateLdapRequest(req *pbs.AuthenticateRequest) error {
|
||||
badFields := make(map[string]string)
|
||||
|
||||
attrs := req.GetLdapLoginAttributes()
|
||||
switch {
|
||||
case attrs == nil:
|
||||
badFields["attributes"] = "This is a required field."
|
||||
default:
|
||||
if attrs.LoginName == "" {
|
||||
badFields["attributes.login_name"] = "This is a required field."
|
||||
}
|
||||
if attrs.Password == "" {
|
||||
badFields["attributes.password"] = "This is a required field."
|
||||
}
|
||||
if req.GetCommand() == "" {
|
||||
// TODO: Eventually, require a command. For now, fall back to "login" for backwards compat.
|
||||
req.Command = loginCommand
|
||||
}
|
||||
if req.Command != loginCommand {
|
||||
badFields[commandField] = "Invalid command for this auth method type."
|
||||
}
|
||||
tokenType := req.GetType()
|
||||
if tokenType == "" {
|
||||
// Fall back to deprecated field if type is not set
|
||||
tokenType = req.GetTokenType() //nolint:all
|
||||
}
|
||||
tType := strings.ToLower(strings.TrimSpace(tokenType))
|
||||
if tType != "" && tType != "token" && tType != "cookie" {
|
||||
badFields[tokenTypeField] = `The only accepted types are "token" and "cookie".`
|
||||
}
|
||||
}
|
||||
|
||||
if len(badFields) > 0 {
|
||||
return handlers.InvalidArgumentErrorf("Invalid fields provided in request.", badFields)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,983 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package authmethods_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/hashicorp/boundary/globals"
|
||||
"github.com/hashicorp/boundary/internal/auth/ldap"
|
||||
"github.com/hashicorp/boundary/internal/auth/oidc"
|
||||
"github.com/hashicorp/boundary/internal/auth/password"
|
||||
"github.com/hashicorp/boundary/internal/authtoken"
|
||||
"github.com/hashicorp/boundary/internal/daemon/controller/auth"
|
||||
"github.com/hashicorp/boundary/internal/daemon/controller/handlers"
|
||||
"github.com/hashicorp/boundary/internal/daemon/controller/handlers/authmethods"
|
||||
"github.com/hashicorp/boundary/internal/db"
|
||||
"github.com/hashicorp/boundary/internal/errors"
|
||||
pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services"
|
||||
"github.com/hashicorp/boundary/internal/iam"
|
||||
"github.com/hashicorp/boundary/internal/kms"
|
||||
"github.com/hashicorp/boundary/internal/types/scope"
|
||||
pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/authmethods"
|
||||
scopepb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/scopes"
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/jimlambrt/gldap"
|
||||
"github.com/jimlambrt/gldap/testdirectory"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/genproto/protobuf/field_mask"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/protobuf/testing/protocmp"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
"google.golang.org/protobuf/types/known/wrapperspb"
|
||||
)
|
||||
|
||||
func Test_UpdateLdap(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.TODO()
|
||||
conn, _ := db.TestSetup(t, "postgres")
|
||||
rw := db.New(conn)
|
||||
wrapper := db.TestWrapper(t)
|
||||
kms := kms.TestKms(t, conn, wrapper)
|
||||
iamRepoFn := func() (*iam.Repository, error) {
|
||||
return iam.TestRepo(t, conn, wrapper), nil
|
||||
}
|
||||
oidcRepoFn := func() (*oidc.Repository, error) {
|
||||
return oidc.NewRepository(ctx, rw, rw, kms)
|
||||
}
|
||||
ldapRepoFn := func() (*ldap.Repository, error) {
|
||||
return ldap.NewRepository(ctx, rw, rw, kms)
|
||||
}
|
||||
pwRepoFn := func() (*password.Repository, error) {
|
||||
return password.NewRepository(rw, rw, kms)
|
||||
}
|
||||
atRepoFn := func() (*authtoken.Repository, error) {
|
||||
return authtoken.NewRepository(rw, rw, kms)
|
||||
}
|
||||
iamRepo := iam.TestRepo(t, conn, wrapper)
|
||||
|
||||
o, _ := iam.TestScopes(t, iamRepo)
|
||||
tested, err := authmethods.NewService(kms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn)
|
||||
require.NoError(t, err, "Error when getting new auth_method service.")
|
||||
|
||||
defaultScopeInfo := &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: o.GetType(), ParentScopeId: scope.Global.String()}
|
||||
|
||||
defaultAttributes := &pb.AuthMethod_LdapAuthMethodsAttributes{
|
||||
LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{
|
||||
Urls: []string{"ldaps://ldap1"},
|
||||
State: "active-private",
|
||||
},
|
||||
}
|
||||
defaultReadAttributes := &pb.AuthMethod_LdapAuthMethodsAttributes{
|
||||
LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{
|
||||
Urls: []string{"ldaps://ldap1"},
|
||||
State: "active-private",
|
||||
},
|
||||
}
|
||||
|
||||
freshAuthMethod := func(t *testing.T) (*pb.AuthMethod, func()) {
|
||||
ctx := auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId())
|
||||
am, err := tested.CreateAuthMethod(ctx, &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{
|
||||
ScopeId: o.GetPublicId(),
|
||||
Name: wrapperspb.String("default"),
|
||||
Description: wrapperspb.String("default"),
|
||||
Type: ldap.Subtype.String(),
|
||||
Attrs: defaultAttributes,
|
||||
}})
|
||||
require.NoError(t, err)
|
||||
|
||||
clean := func() {
|
||||
_, err := tested.DeleteAuthMethod(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()),
|
||||
&pbs.DeleteAuthMethodRequest{Id: am.GetItem().GetId()})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
return am.GetItem(), clean
|
||||
}
|
||||
|
||||
_, testEncodedCert := ldap.TestGenerateCA(t, "localhost")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
req *pbs.UpdateAuthMethodRequest
|
||||
res *pbs.UpdateAuthMethodResponse
|
||||
err error
|
||||
errContains string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "update-an-existing-auth-method",
|
||||
req: &pbs.UpdateAuthMethodRequest{
|
||||
UpdateMask: &field_mask.FieldMask{
|
||||
Paths: []string{"name", "description"},
|
||||
},
|
||||
Item: &pb.AuthMethod{
|
||||
Name: &wrapperspb.StringValue{Value: "new"},
|
||||
Description: &wrapperspb.StringValue{Value: "desc"},
|
||||
Type: ldap.Subtype.String(),
|
||||
},
|
||||
},
|
||||
res: &pbs.UpdateAuthMethodResponse{
|
||||
Item: &pb.AuthMethod{
|
||||
ScopeId: o.GetPublicId(),
|
||||
Name: &wrapperspb.StringValue{Value: "new"},
|
||||
Description: &wrapperspb.StringValue{Value: "desc"},
|
||||
Version: 2,
|
||||
Type: ldap.Subtype.String(),
|
||||
Attrs: defaultReadAttributes,
|
||||
Scope: defaultScopeInfo,
|
||||
AuthorizedActions: ldapAuthorizedActions,
|
||||
AuthorizedCollectionActions: authorizedCollectionActions,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multi-paths-in-single-string",
|
||||
req: &pbs.UpdateAuthMethodRequest{
|
||||
UpdateMask: &field_mask.FieldMask{
|
||||
Paths: []string{"name,description,type"},
|
||||
},
|
||||
Item: &pb.AuthMethod{
|
||||
Name: &wrapperspb.StringValue{Value: "new"},
|
||||
Description: &wrapperspb.StringValue{Value: "desc"},
|
||||
Type: ldap.Subtype.String(),
|
||||
},
|
||||
},
|
||||
res: &pbs.UpdateAuthMethodResponse{
|
||||
Item: &pb.AuthMethod{
|
||||
ScopeId: o.GetPublicId(),
|
||||
Version: 2,
|
||||
Name: &wrapperspb.StringValue{Value: "new"},
|
||||
Description: &wrapperspb.StringValue{Value: "desc"},
|
||||
Type: ldap.Subtype.String(),
|
||||
Attrs: defaultReadAttributes,
|
||||
Scope: defaultScopeInfo,
|
||||
AuthorizedActions: ldapAuthorizedActions,
|
||||
AuthorizedCollectionActions: authorizedCollectionActions,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no-update-mask",
|
||||
req: &pbs.UpdateAuthMethodRequest{
|
||||
Item: &pb.AuthMethod{
|
||||
Name: &wrapperspb.StringValue{Value: "updated name"},
|
||||
Description: &wrapperspb.StringValue{Value: "updated desc"},
|
||||
},
|
||||
},
|
||||
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
|
||||
errContains: "UpdateMask not provided but is required to update this resource",
|
||||
},
|
||||
{
|
||||
name: "missing-paths-in-mask",
|
||||
req: &pbs.UpdateAuthMethodRequest{
|
||||
UpdateMask: &field_mask.FieldMask{Paths: []string{}},
|
||||
Item: &pb.AuthMethod{
|
||||
Name: &wrapperspb.StringValue{Value: "updated name"},
|
||||
Description: &wrapperspb.StringValue{Value: "updated desc"},
|
||||
},
|
||||
},
|
||||
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
|
||||
errContains: "No valid fields included in the update mask",
|
||||
},
|
||||
{
|
||||
name: "non-existent-paths-in-mask",
|
||||
req: &pbs.UpdateAuthMethodRequest{
|
||||
UpdateMask: &field_mask.FieldMask{Paths: []string{"nonexistant_field"}},
|
||||
Item: &pb.AuthMethod{
|
||||
Name: &wrapperspb.StringValue{Value: "updated name"},
|
||||
Description: &wrapperspb.StringValue{Value: "updated desc"},
|
||||
},
|
||||
},
|
||||
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
|
||||
errContains: "No valid fields included in the update mask",
|
||||
},
|
||||
{
|
||||
name: "cannot-change-type",
|
||||
req: &pbs.UpdateAuthMethodRequest{
|
||||
UpdateMask: &field_mask.FieldMask{Paths: []string{"name", "type"}},
|
||||
Item: &pb.AuthMethod{
|
||||
Name: &wrapperspb.StringValue{Value: "updated name"},
|
||||
Type: password.Subtype.String(),
|
||||
},
|
||||
},
|
||||
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
|
||||
errContains: "Cannot modify the resource type",
|
||||
},
|
||||
{
|
||||
name: "cannot-change-is-primary",
|
||||
req: &pbs.UpdateAuthMethodRequest{
|
||||
UpdateMask: &field_mask.FieldMask{Paths: []string{"is_primary"}},
|
||||
Item: &pb.AuthMethod{
|
||||
IsPrimary: true,
|
||||
},
|
||||
},
|
||||
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
|
||||
errContains: "This field is read only",
|
||||
},
|
||||
{
|
||||
name: "unset-name",
|
||||
req: &pbs.UpdateAuthMethodRequest{
|
||||
UpdateMask: &field_mask.FieldMask{
|
||||
Paths: []string{"name"},
|
||||
},
|
||||
Item: &pb.AuthMethod{
|
||||
Description: &wrapperspb.StringValue{Value: "ignored"},
|
||||
},
|
||||
},
|
||||
res: &pbs.UpdateAuthMethodResponse{
|
||||
Item: &pb.AuthMethod{
|
||||
ScopeId: o.GetPublicId(),
|
||||
Version: 2,
|
||||
Description: &wrapperspb.StringValue{Value: "default"},
|
||||
Type: ldap.Subtype.String(),
|
||||
Attrs: defaultReadAttributes,
|
||||
Scope: defaultScopeInfo,
|
||||
AuthorizedActions: ldapAuthorizedActions,
|
||||
AuthorizedCollectionActions: authorizedCollectionActions,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unset-description",
|
||||
req: &pbs.UpdateAuthMethodRequest{
|
||||
UpdateMask: &field_mask.FieldMask{
|
||||
Paths: []string{"description"},
|
||||
},
|
||||
Item: &pb.AuthMethod{
|
||||
Name: &wrapperspb.StringValue{Value: "ignored"},
|
||||
},
|
||||
},
|
||||
res: &pbs.UpdateAuthMethodResponse{
|
||||
Item: &pb.AuthMethod{
|
||||
ScopeId: o.GetPublicId(),
|
||||
Version: 2,
|
||||
Name: &wrapperspb.StringValue{Value: "default"},
|
||||
Type: ldap.Subtype.String(),
|
||||
Attrs: defaultReadAttributes,
|
||||
Scope: defaultScopeInfo,
|
||||
AuthorizedActions: ldapAuthorizedActions,
|
||||
AuthorizedCollectionActions: authorizedCollectionActions,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "update-only-state",
|
||||
req: &pbs.UpdateAuthMethodRequest{
|
||||
UpdateMask: &field_mask.FieldMask{
|
||||
Paths: []string{"attributes.state"},
|
||||
},
|
||||
Item: &pb.AuthMethod{
|
||||
Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{
|
||||
LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{
|
||||
State: "active-public",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
res: &pbs.UpdateAuthMethodResponse{
|
||||
Item: &pb.AuthMethod{
|
||||
ScopeId: o.GetPublicId(),
|
||||
Version: 2,
|
||||
Name: &wrapperspb.StringValue{Value: "default"},
|
||||
Description: &wrapperspb.StringValue{Value: "default"},
|
||||
Type: ldap.Subtype.String(),
|
||||
Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{
|
||||
LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{
|
||||
Urls: []string{"ldaps://ldap1"},
|
||||
State: "active-public",
|
||||
},
|
||||
},
|
||||
Scope: defaultScopeInfo,
|
||||
AuthorizedActions: ldapAuthorizedActions,
|
||||
AuthorizedCollectionActions: authorizedCollectionActions,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "update-only-name",
|
||||
req: &pbs.UpdateAuthMethodRequest{
|
||||
UpdateMask: &field_mask.FieldMask{
|
||||
Paths: []string{"name"},
|
||||
},
|
||||
Item: &pb.AuthMethod{
|
||||
Name: &wrapperspb.StringValue{Value: "updated"},
|
||||
Description: &wrapperspb.StringValue{Value: "ignored"},
|
||||
},
|
||||
},
|
||||
res: &pbs.UpdateAuthMethodResponse{
|
||||
Item: &pb.AuthMethod{
|
||||
ScopeId: o.GetPublicId(),
|
||||
Version: 2,
|
||||
Name: &wrapperspb.StringValue{Value: "updated"},
|
||||
Description: &wrapperspb.StringValue{Value: "default"},
|
||||
Type: ldap.Subtype.String(),
|
||||
Attrs: defaultReadAttributes,
|
||||
Scope: defaultScopeInfo,
|
||||
AuthorizedActions: ldapAuthorizedActions,
|
||||
AuthorizedCollectionActions: authorizedCollectionActions,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "update-only-description",
|
||||
req: &pbs.UpdateAuthMethodRequest{
|
||||
UpdateMask: &field_mask.FieldMask{
|
||||
Paths: []string{"description"},
|
||||
},
|
||||
Item: &pb.AuthMethod{
|
||||
Name: &wrapperspb.StringValue{Value: "ignored"},
|
||||
Description: &wrapperspb.StringValue{Value: "notignored"},
|
||||
},
|
||||
},
|
||||
res: &pbs.UpdateAuthMethodResponse{
|
||||
Item: &pb.AuthMethod{
|
||||
ScopeId: o.GetPublicId(),
|
||||
Version: 2,
|
||||
Name: &wrapperspb.StringValue{Value: "default"},
|
||||
Description: &wrapperspb.StringValue{Value: "notignored"},
|
||||
Type: ldap.Subtype.String(),
|
||||
Attrs: defaultReadAttributes,
|
||||
Scope: defaultScopeInfo,
|
||||
AuthorizedActions: ldapAuthorizedActions,
|
||||
AuthorizedCollectionActions: authorizedCollectionActions,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "update-a-non-existent-auth-method",
|
||||
req: &pbs.UpdateAuthMethodRequest{
|
||||
Id: globals.LdapAuthMethodPrefix + "_DoesNotExist",
|
||||
UpdateMask: &field_mask.FieldMask{
|
||||
Paths: []string{"description"},
|
||||
},
|
||||
Item: &pb.AuthMethod{
|
||||
Name: &wrapperspb.StringValue{Value: "new"},
|
||||
Description: &wrapperspb.StringValue{Value: "desc"},
|
||||
},
|
||||
},
|
||||
err: handlers.ApiErrorWithCode(codes.NotFound),
|
||||
errContains: "Resource not found",
|
||||
},
|
||||
{
|
||||
name: "cannot-change-id",
|
||||
req: &pbs.UpdateAuthMethodRequest{
|
||||
UpdateMask: &field_mask.FieldMask{
|
||||
Paths: []string{"id"},
|
||||
},
|
||||
Item: &pb.AuthMethod{
|
||||
Id: globals.LdapAuthMethodPrefix + "_something",
|
||||
Name: &wrapperspb.StringValue{Value: "new"},
|
||||
Description: &wrapperspb.StringValue{Value: "new desc"},
|
||||
},
|
||||
},
|
||||
res: nil,
|
||||
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
|
||||
errContains: "This is a read only field and cannot be specified in an update request",
|
||||
},
|
||||
{
|
||||
name: "cannot-specify-created-time",
|
||||
req: &pbs.UpdateAuthMethodRequest{
|
||||
UpdateMask: &field_mask.FieldMask{
|
||||
Paths: []string{"created_time"},
|
||||
},
|
||||
Item: &pb.AuthMethod{
|
||||
CreatedTime: timestamppb.Now(),
|
||||
},
|
||||
},
|
||||
res: nil,
|
||||
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
|
||||
errContains: "This is a read only field and cannot be specified in an update request",
|
||||
},
|
||||
{
|
||||
name: "cannot-specify-updated-time",
|
||||
req: &pbs.UpdateAuthMethodRequest{
|
||||
UpdateMask: &field_mask.FieldMask{
|
||||
Paths: []string{"updated_time"},
|
||||
},
|
||||
Item: &pb.AuthMethod{
|
||||
UpdatedTime: timestamppb.Now(),
|
||||
},
|
||||
},
|
||||
res: nil,
|
||||
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
|
||||
errContains: "This is a read only field and cannot be specified in an update request",
|
||||
},
|
||||
{
|
||||
name: "cannot-specify-type",
|
||||
req: &pbs.UpdateAuthMethodRequest{
|
||||
UpdateMask: &field_mask.FieldMask{
|
||||
Paths: []string{"type"},
|
||||
},
|
||||
Item: &pb.AuthMethod{
|
||||
Type: ldap.Subtype.String(),
|
||||
},
|
||||
},
|
||||
res: nil,
|
||||
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
|
||||
errContains: "No valid fields included in the update mask",
|
||||
},
|
||||
{
|
||||
name: "cannot-change-scope",
|
||||
req: &pbs.UpdateAuthMethodRequest{
|
||||
UpdateMask: &field_mask.FieldMask{
|
||||
Paths: []string{"scope"},
|
||||
},
|
||||
Item: &pb.AuthMethod{
|
||||
ScopeId: "something-new",
|
||||
Name: &wrapperspb.StringValue{Value: "new"},
|
||||
Description: &wrapperspb.StringValue{Value: "new desc"},
|
||||
},
|
||||
},
|
||||
res: nil,
|
||||
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
|
||||
errContains: "No valid fields included in the update mask",
|
||||
},
|
||||
{
|
||||
name: "invalid-certs",
|
||||
req: &pbs.UpdateAuthMethodRequest{
|
||||
UpdateMask: &field_mask.FieldMask{
|
||||
Paths: []string{"attributes.certificates"},
|
||||
},
|
||||
Item: &pb.AuthMethod{
|
||||
ScopeId: "something-new",
|
||||
Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{
|
||||
LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{
|
||||
Certificates: []string{ldap.TestInvalidPem},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
res: nil,
|
||||
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
|
||||
errContains: "invalid attributes.certificates",
|
||||
},
|
||||
{
|
||||
name: "update-certs",
|
||||
req: &pbs.UpdateAuthMethodRequest{
|
||||
UpdateMask: &field_mask.FieldMask{
|
||||
Paths: []string{"attributes.certificates"},
|
||||
},
|
||||
Item: &pb.AuthMethod{
|
||||
Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{
|
||||
LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{
|
||||
Certificates: []string{testEncodedCert},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
res: &pbs.UpdateAuthMethodResponse{
|
||||
Item: &pb.AuthMethod{
|
||||
ScopeId: o.GetPublicId(),
|
||||
Version: 2,
|
||||
Name: &wrapperspb.StringValue{Value: "default"},
|
||||
Description: &wrapperspb.StringValue{Value: "default"},
|
||||
Type: ldap.Subtype.String(),
|
||||
Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{
|
||||
LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{
|
||||
Urls: []string{"ldaps://ldap1"},
|
||||
State: "active-private",
|
||||
Certificates: []string{testEncodedCert},
|
||||
},
|
||||
},
|
||||
Scope: defaultScopeInfo,
|
||||
AuthorizedActions: ldapAuthorizedActions,
|
||||
AuthorizedCollectionActions: authorizedCollectionActions,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "update-urls",
|
||||
req: &pbs.UpdateAuthMethodRequest{
|
||||
UpdateMask: &field_mask.FieldMask{
|
||||
Paths: []string{"attributes.urls"},
|
||||
},
|
||||
Item: &pb.AuthMethod{
|
||||
Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{
|
||||
LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{
|
||||
Urls: []string{"ldaps://ldap2", "ldaps://ldap3"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
res: &pbs.UpdateAuthMethodResponse{
|
||||
Item: &pb.AuthMethod{
|
||||
ScopeId: o.GetPublicId(),
|
||||
Version: 2,
|
||||
Name: &wrapperspb.StringValue{Value: "default"},
|
||||
Description: &wrapperspb.StringValue{Value: "default"},
|
||||
Type: ldap.Subtype.String(),
|
||||
Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{
|
||||
LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{
|
||||
Urls: []string{"ldaps://ldap2", "ldaps://ldap3"},
|
||||
State: "active-private",
|
||||
},
|
||||
},
|
||||
Scope: defaultScopeInfo,
|
||||
AuthorizedActions: ldapAuthorizedActions,
|
||||
AuthorizedCollectionActions: authorizedCollectionActions,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "update-urls-err",
|
||||
req: &pbs.UpdateAuthMethodRequest{
|
||||
UpdateMask: &field_mask.FieldMask{
|
||||
Paths: []string{"attributes.urls"},
|
||||
},
|
||||
Item: &pb.AuthMethod{
|
||||
Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{
|
||||
LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{
|
||||
Urls: []string{" ldaps://ldap2"}, // invalid url (space at start)
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
res: nil,
|
||||
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
|
||||
errContains: "is not a valid url",
|
||||
},
|
||||
{
|
||||
name: "update-user-search-config",
|
||||
req: &pbs.UpdateAuthMethodRequest{
|
||||
UpdateMask: &field_mask.FieldMask{
|
||||
Paths: []string{"attributes.user_dn", "attributes.user_attr", "attributes.user_filter"},
|
||||
},
|
||||
Item: &pb.AuthMethod{
|
||||
Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{
|
||||
LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{
|
||||
UserDn: wrapperspb.String("user-dn"),
|
||||
UserAttr: wrapperspb.String("user-attr"),
|
||||
UserFilter: wrapperspb.String("user-filter"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
res: &pbs.UpdateAuthMethodResponse{
|
||||
Item: &pb.AuthMethod{
|
||||
ScopeId: o.GetPublicId(),
|
||||
Version: 2,
|
||||
Name: &wrapperspb.StringValue{Value: "default"},
|
||||
Description: &wrapperspb.StringValue{Value: "default"},
|
||||
Type: ldap.Subtype.String(),
|
||||
Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{
|
||||
LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{
|
||||
Urls: []string{"ldaps://ldap1"},
|
||||
State: "active-private",
|
||||
UserDn: wrapperspb.String("user-dn"),
|
||||
UserAttr: wrapperspb.String("user-attr"),
|
||||
UserFilter: wrapperspb.String("user-filter"),
|
||||
},
|
||||
},
|
||||
Scope: defaultScopeInfo,
|
||||
AuthorizedActions: ldapAuthorizedActions,
|
||||
AuthorizedCollectionActions: authorizedCollectionActions,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "use-token-groups",
|
||||
req: &pbs.UpdateAuthMethodRequest{
|
||||
UpdateMask: &field_mask.FieldMask{
|
||||
Paths: []string{"attributes.use_token_groups"},
|
||||
},
|
||||
Item: &pb.AuthMethod{
|
||||
ScopeId: "something-new",
|
||||
Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{
|
||||
LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{
|
||||
UseTokenGroups: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
res: &pbs.UpdateAuthMethodResponse{
|
||||
Item: &pb.AuthMethod{
|
||||
ScopeId: o.GetPublicId(),
|
||||
Version: 2,
|
||||
Name: &wrapperspb.StringValue{Value: "default"},
|
||||
Description: &wrapperspb.StringValue{Value: "default"},
|
||||
Type: ldap.Subtype.String(),
|
||||
Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{
|
||||
LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{
|
||||
Urls: []string{"ldaps://ldap1"},
|
||||
State: "active-private",
|
||||
UseTokenGroups: true,
|
||||
},
|
||||
},
|
||||
Scope: defaultScopeInfo,
|
||||
AuthorizedActions: ldapAuthorizedActions,
|
||||
AuthorizedCollectionActions: authorizedCollectionActions,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "enable-groups-err",
|
||||
req: &pbs.UpdateAuthMethodRequest{
|
||||
UpdateMask: &field_mask.FieldMask{
|
||||
Paths: []string{"attributes.enable_groups"},
|
||||
},
|
||||
Item: &pb.AuthMethod{
|
||||
ScopeId: "something-new",
|
||||
Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{
|
||||
LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{
|
||||
EnableGroups: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
res: nil,
|
||||
wantErr: true,
|
||||
errContains: "have a configured group_dn when enable_groups = true and use_token_groups = false",
|
||||
},
|
||||
{
|
||||
name: "update-group-search-config",
|
||||
req: &pbs.UpdateAuthMethodRequest{
|
||||
UpdateMask: &field_mask.FieldMask{
|
||||
Paths: []string{"attributes.group_dn", "attributes.group_attr", "attributes.group_filter"},
|
||||
},
|
||||
Item: &pb.AuthMethod{
|
||||
Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{
|
||||
LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{
|
||||
GroupDn: wrapperspb.String("group-dn"),
|
||||
GroupAttr: wrapperspb.String("group-attr"),
|
||||
GroupFilter: wrapperspb.String("group-filter"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
res: &pbs.UpdateAuthMethodResponse{
|
||||
Item: &pb.AuthMethod{
|
||||
ScopeId: o.GetPublicId(),
|
||||
Version: 2,
|
||||
Name: &wrapperspb.StringValue{Value: "default"},
|
||||
Description: &wrapperspb.StringValue{Value: "default"},
|
||||
Type: ldap.Subtype.String(),
|
||||
Attrs: &pb.AuthMethod_LdapAuthMethodsAttributes{
|
||||
LdapAuthMethodsAttributes: &pb.LdapAuthMethodAttributes{
|
||||
Urls: []string{"ldaps://ldap1"},
|
||||
State: "active-private",
|
||||
GroupDn: wrapperspb.String("group-dn"),
|
||||
GroupAttr: wrapperspb.String("group-attr"),
|
||||
GroupFilter: wrapperspb.String("group-filter"),
|
||||
},
|
||||
},
|
||||
Scope: defaultScopeInfo,
|
||||
AuthorizedActions: ldapAuthorizedActions,
|
||||
AuthorizedCollectionActions: authorizedCollectionActions,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no-change",
|
||||
req: &pbs.UpdateAuthMethodRequest{
|
||||
UpdateMask: &field_mask.FieldMask{
|
||||
Paths: []string{"attributes.certificates"},
|
||||
},
|
||||
Item: &pb.AuthMethod{
|
||||
Name: &wrapperspb.StringValue{Value: "default"},
|
||||
},
|
||||
},
|
||||
err: handlers.ApiErrorWithCode(codes.NotFound),
|
||||
errContains: "no changes were made to the existing AuthMethod",
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
am, cleanup := freshAuthMethod(t)
|
||||
defer cleanup()
|
||||
|
||||
tc.req.Item.Version = am.GetVersion()
|
||||
|
||||
if tc.req.GetId() == "" {
|
||||
tc.req.Id = am.GetId()
|
||||
}
|
||||
|
||||
if tc.res != nil && tc.res.Item != nil {
|
||||
tc.res.Item.Id = am.GetId()
|
||||
tc.res.Item.CreatedTime = am.GetCreatedTime()
|
||||
}
|
||||
got, gErr := tested.UpdateAuthMethod(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), tc.req)
|
||||
// TODO: When handlers move to domain errors remove wantErr and rely errors.Match here.
|
||||
if tc.err != nil || tc.wantErr {
|
||||
require.Error(gErr)
|
||||
if tc.err != nil {
|
||||
assert.True(errors.Is(gErr, tc.err), "UpdateAuthMethod(%+v) got error %v, wanted %v", tc.req, gErr, tc.err)
|
||||
}
|
||||
if tc.errContains != "" {
|
||||
assert.Contains(gErr.Error(), tc.errContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(gErr)
|
||||
if tc.res == nil {
|
||||
require.Nil(got)
|
||||
}
|
||||
cmpOptions := []cmp.Option{protocmp.Transform()}
|
||||
if got != nil {
|
||||
assert.NotNilf(tc.res, "Expected UpdateAuthMethod response to be nil, but was %v", got)
|
||||
gotUpdateTime := got.GetItem().GetUpdatedTime().AsTime()
|
||||
created := am.GetCreatedTime().AsTime()
|
||||
|
||||
// Verify it is a auth_method updated after it was created
|
||||
assert.True(gotUpdateTime.After(created), "Updated auth_method should have been updated after it's creation. Was updated %v, which is after %v", gotUpdateTime, created)
|
||||
|
||||
// Ignore all values which are hard to compare against.
|
||||
cmpOptions = append(
|
||||
cmpOptions,
|
||||
protocmp.IgnoreFields(&pb.AuthMethod{}, "updated_time"),
|
||||
protocmp.SortRepeatedFields(&pb.LdapAuthMethodAttributes{}, "account_attribute_maps", "urls", "certificates"),
|
||||
protocmp.IgnoreFields(&pb.LdapAuthMethodAttributes{}, "bind_password_hmac", "client_certificate_key_hmac"),
|
||||
)
|
||||
assert.NotEqual("bind_password", got.Item.GetLdapAuthMethodsAttributes().GetBindPasswordHmac())
|
||||
assert.NotEqual("client_certificate_key", got.Item.GetLdapAuthMethodsAttributes().GetClientCertificateKeyHmac())
|
||||
}
|
||||
assert.Empty(cmp.Diff(got, tc.res, cmpOptions...), "UpdateAuthMethod(%q) got response %q, wanted %q", tc.req, got, tc.res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthenticate_Ldap(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCtx := context.Background()
|
||||
testConn, _ := db.TestSetup(t, "postgres")
|
||||
testRw := db.New(testConn)
|
||||
testRootWrapper := db.TestWrapper(t)
|
||||
testKms := kms.TestKms(t, testConn, testRootWrapper)
|
||||
o, _ := iam.TestScopes(t, iam.TestRepo(t, testConn, testRootWrapper))
|
||||
|
||||
iamRepoFn := func() (*iam.Repository, error) {
|
||||
return iam.TestRepo(t, testConn, testRootWrapper), nil
|
||||
}
|
||||
oidcRepoFn := func() (*oidc.Repository, error) {
|
||||
return oidc.NewRepository(testCtx, testRw, testRw, testKms)
|
||||
}
|
||||
ldapRepoFn := func() (*ldap.Repository, error) {
|
||||
return ldap.NewRepository(testCtx, testRw, testRw, testKms)
|
||||
}
|
||||
pwRepoFn := func() (*password.Repository, error) {
|
||||
return password.NewRepository(testRw, testRw, testKms)
|
||||
}
|
||||
atRepoFn := func() (*authtoken.Repository, error) {
|
||||
return authtoken.NewRepository(testRw, testRw, testKms)
|
||||
}
|
||||
|
||||
orgDbWrapper, err := testKms.GetWrapper(testCtx, o.GetPublicId(), kms.KeyPurposeDatabase)
|
||||
require.NoError(t, err)
|
||||
logger := hclog.New(&hclog.LoggerOptions{
|
||||
Name: "test-logger",
|
||||
Level: hclog.Error,
|
||||
})
|
||||
td := testdirectory.Start(t,
|
||||
testdirectory.WithDefaults(t, &testdirectory.Defaults{AllowAnonymousBind: true}),
|
||||
testdirectory.WithLogger(t, logger),
|
||||
)
|
||||
groups := []*gldap.Entry{
|
||||
testdirectory.NewGroup(t, "admin", []string{"alice"}),
|
||||
testdirectory.NewGroup(t, "users", []string{"alice"}),
|
||||
}
|
||||
users := testdirectory.NewUsers(t, []string{"alice"}, testdirectory.WithMembersOf(t, "admin", "users"))
|
||||
users2 := testdirectory.NewUsers(t, []string{"bob"})
|
||||
td.SetUsers(append(users, users2...)...)
|
||||
td.SetGroups(groups...)
|
||||
|
||||
tdCerts, err := ldap.ParseCertificates(testCtx, td.Cert())
|
||||
require.NoError(t, err)
|
||||
|
||||
testAm := ldap.TestAuthMethod(t, testConn, orgDbWrapper, o.PublicId,
|
||||
[]string{fmt.Sprintf("ldaps://127.0.0.1:%d", td.Port())},
|
||||
ldap.WithCertificates(testCtx, tdCerts...),
|
||||
ldap.WithDiscoverDn(testCtx),
|
||||
ldap.WithEnableGroups(testCtx),
|
||||
ldap.WithUserDn(testCtx, testdirectory.DefaultUserDN),
|
||||
ldap.WithGroupDn(testCtx, testdirectory.DefaultGroupDN),
|
||||
)
|
||||
|
||||
iam.TestSetPrimaryAuthMethod(t, iam.TestRepo(t, testConn, testRootWrapper), o, testAm.PublicId)
|
||||
|
||||
testManagedGrp := ldap.TestManagedGroup(t, testConn, testAm, []string{"cn=admin,ou=groups,dc=example,dc=org"})
|
||||
|
||||
const (
|
||||
testLoginName = "alice"
|
||||
testPassword = "password"
|
||||
testLoginName2 = "bob"
|
||||
)
|
||||
|
||||
testAcct := ldap.TestAccount(t, testConn, testAm, testLoginName)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
acctId string
|
||||
request *pbs.AuthenticateRequest
|
||||
wantType string
|
||||
wantGroups []string
|
||||
wantErr error
|
||||
wantErrContains string
|
||||
}{
|
||||
{
|
||||
name: "basic-with-groups",
|
||||
acctId: testAcct.PublicId,
|
||||
request: &pbs.AuthenticateRequest{
|
||||
AuthMethodId: testAm.GetPublicId(),
|
||||
TokenType: "token",
|
||||
Attrs: &pbs.AuthenticateRequest_LdapLoginAttributes{
|
||||
LdapLoginAttributes: &pbs.LdapLoginAttributes{
|
||||
LoginName: testLoginName,
|
||||
Password: testPassword,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantGroups: []string{testManagedGrp.PublicId},
|
||||
wantType: "token",
|
||||
},
|
||||
{
|
||||
name: "basic-without-groups",
|
||||
request: &pbs.AuthenticateRequest{
|
||||
AuthMethodId: testAm.GetPublicId(),
|
||||
TokenType: "token",
|
||||
Attrs: &pbs.AuthenticateRequest_LdapLoginAttributes{
|
||||
LdapLoginAttributes: &pbs.LdapLoginAttributes{
|
||||
LoginName: testLoginName2,
|
||||
Password: testPassword,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantType: "token",
|
||||
},
|
||||
{
|
||||
name: "cookie-type",
|
||||
acctId: testAcct.PublicId,
|
||||
request: &pbs.AuthenticateRequest{
|
||||
AuthMethodId: testAm.GetPublicId(),
|
||||
TokenType: "cookie",
|
||||
Attrs: &pbs.AuthenticateRequest_LdapLoginAttributes{
|
||||
LdapLoginAttributes: &pbs.LdapLoginAttributes{
|
||||
LoginName: testLoginName,
|
||||
Password: testPassword,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantType: "cookie",
|
||||
},
|
||||
{
|
||||
name: "no-token-type",
|
||||
acctId: testAcct.PublicId,
|
||||
request: &pbs.AuthenticateRequest{
|
||||
AuthMethodId: testAm.GetPublicId(),
|
||||
Attrs: &pbs.AuthenticateRequest_LdapLoginAttributes{
|
||||
LdapLoginAttributes: &pbs.LdapLoginAttributes{
|
||||
LoginName: testLoginName,
|
||||
Password: testPassword,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "bad-token-type",
|
||||
request: &pbs.AuthenticateRequest{
|
||||
AuthMethodId: testAm.GetPublicId(),
|
||||
TokenType: "email",
|
||||
Attrs: &pbs.AuthenticateRequest_LdapLoginAttributes{
|
||||
LdapLoginAttributes: &pbs.LdapLoginAttributes{
|
||||
LoginName: testLoginName,
|
||||
Password: testPassword,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: handlers.ApiErrorWithCode(codes.InvalidArgument),
|
||||
},
|
||||
{
|
||||
name: "no-authmethod",
|
||||
request: &pbs.AuthenticateRequest{
|
||||
Attrs: &pbs.AuthenticateRequest_LdapLoginAttributes{
|
||||
LdapLoginAttributes: &pbs.LdapLoginAttributes{
|
||||
LoginName: testLoginName,
|
||||
Password: testPassword,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: handlers.ApiErrorWithCode(codes.InvalidArgument),
|
||||
},
|
||||
{
|
||||
name: "wrong-password",
|
||||
request: &pbs.AuthenticateRequest{
|
||||
AuthMethodId: testAm.GetPublicId(),
|
||||
TokenType: "token",
|
||||
Attrs: &pbs.AuthenticateRequest_LdapLoginAttributes{
|
||||
LdapLoginAttributes: &pbs.LdapLoginAttributes{
|
||||
LoginName: testLoginName,
|
||||
Password: "wrong",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: handlers.ApiErrorWithCode(codes.Unauthenticated),
|
||||
},
|
||||
{
|
||||
name: "wrong-login-name",
|
||||
request: &pbs.AuthenticateRequest{
|
||||
AuthMethodId: testAm.GetPublicId(),
|
||||
TokenType: "token",
|
||||
Attrs: &pbs.AuthenticateRequest_LdapLoginAttributes{
|
||||
LdapLoginAttributes: &pbs.LdapLoginAttributes{
|
||||
LoginName: "wrong",
|
||||
Password: testPassword,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: handlers.ApiErrorWithCode(codes.Unauthenticated),
|
||||
},
|
||||
{
|
||||
name: "no-attributes",
|
||||
request: &pbs.AuthenticateRequest{
|
||||
AuthMethodId: testAm.GetPublicId(),
|
||||
TokenType: "token",
|
||||
Attrs: &pbs.AuthenticateRequest_LdapLoginAttributes{},
|
||||
},
|
||||
wantErr: handlers.ApiErrorWithCode(codes.InvalidArgument),
|
||||
wantErrContains: `Details: {{name: "attributes", desc: "This is a required field."}}`,
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
s, err := authmethods.NewService(testKms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn)
|
||||
require.NoError(err)
|
||||
|
||||
resp, err := s.Authenticate(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), tc.request)
|
||||
if tc.wantErr != nil {
|
||||
assert.Error(err)
|
||||
assert.Truef(errors.Is(err, tc.wantErr), "Got %#v, wanted %#v", err, tc.wantErr)
|
||||
if tc.wantErrContains != "" {
|
||||
assert.Contains(err.Error(), tc.wantErrContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
require.NoError(err)
|
||||
|
||||
aToken := resp.GetAuthTokenResponse()
|
||||
assert.NotEmpty(aToken.GetId())
|
||||
assert.NotEmpty(aToken.GetToken())
|
||||
assert.True(strings.HasPrefix(aToken.GetToken(), aToken.GetId()))
|
||||
assert.Equal(testAm.GetPublicId(), aToken.GetAuthMethodId())
|
||||
assert.Equal(aToken.GetCreatedTime(), aToken.GetUpdatedTime())
|
||||
assert.Equal(aToken.GetCreatedTime(), aToken.GetApproximateLastUsedTime())
|
||||
assert.Equal(testAm.GetPublicId(), aToken.GetAuthMethodId())
|
||||
assert.Equal(tc.wantType, resp.GetType())
|
||||
|
||||
// support testing for pre-provisioned accounts
|
||||
if tc.acctId != "" {
|
||||
assert.Equal(tc.acctId, aToken.GetAccountId())
|
||||
}
|
||||
|
||||
names := ldap.TestGetAcctManagedGroups(t, testConn, aToken.GetAccountId())
|
||||
if len(tc.wantGroups) > 0 {
|
||||
assert.Equal(tc.wantGroups, names)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/hashicorp/boundary/globals"
|
||||
authpb "github.com/hashicorp/boundary/internal/gen/controller/auth"
|
||||
|
||||
"github.com/hashicorp/boundary/internal/auth/ldap"
|
||||
"github.com/hashicorp/boundary/internal/auth/oidc"
|
||||
"github.com/hashicorp/boundary/internal/auth/password"
|
||||
"github.com/hashicorp/boundary/internal/authtoken"
|
||||
@@ -58,6 +59,7 @@ type setup struct {
|
||||
iamRepo *iam.Repository
|
||||
iamRepoFn common.IamRepoFactory
|
||||
oidcRepoFn common.OidcAuthRepoFactory
|
||||
ldapRepoFn common.LdapAuthRepoFactory
|
||||
pwRepoFn common.PasswordAuthRepoFactory
|
||||
atRepoFn common.AuthTokenRepoFactory
|
||||
org *iam.Scope
|
||||
@@ -92,6 +94,9 @@ func getSetup(t *testing.T) setup {
|
||||
ret.oidcRepoFn = func() (*oidc.Repository, error) {
|
||||
return oidc.NewRepository(ctx, ret.rw, ret.rw, ret.kmsCache)
|
||||
}
|
||||
ret.ldapRepoFn = func() (*ldap.Repository, error) {
|
||||
return ldap.NewRepository(ctx, ret.rw, ret.rw, ret.kmsCache)
|
||||
}
|
||||
ret.pwRepoFn = func() (*password.Repository, error) {
|
||||
return password.NewRepository(ret.rw, ret.rw, ret.kmsCache)
|
||||
}
|
||||
@@ -103,7 +108,7 @@ func getSetup(t *testing.T) setup {
|
||||
ret.databaseWrapper, err = ret.kmsCache.GetWrapper(ret.ctx, ret.org.PublicId, kms.KeyPurposeDatabase)
|
||||
require.NoError(err)
|
||||
|
||||
ret.authMethodService, err = authmethods.NewService(ret.kmsCache, ret.pwRepoFn, ret.oidcRepoFn, ret.iamRepoFn, ret.atRepoFn)
|
||||
ret.authMethodService, err = authmethods.NewService(ret.kmsCache, ret.pwRepoFn, ret.oidcRepoFn, ret.iamRepoFn, ret.atRepoFn, ret.ldapRepoFn)
|
||||
require.NoError(err)
|
||||
|
||||
ret.testProvider = capoidc.StartTestProvider(t)
|
||||
@@ -149,6 +154,9 @@ func TestList_FilterNonPublic(t *testing.T) {
|
||||
oidcRepoFn := func() (*oidc.Repository, error) {
|
||||
return oidc.NewRepository(ctx, rw, rw, kmsCache)
|
||||
}
|
||||
ldapRepoFn := func() (*ldap.Repository, error) {
|
||||
return ldap.NewRepository(ctx, rw, rw, kmsCache)
|
||||
}
|
||||
pwRepoFn := func() (*password.Repository, error) {
|
||||
return password.NewRepository(rw, rw, kmsCache)
|
||||
}
|
||||
@@ -185,7 +193,7 @@ func TestList_FilterNonPublic(t *testing.T) {
|
||||
oidc.WithIssuer(oidc.TestConvertToUrls(t, fmt.Sprintf("https://alice%d.com", i))[0]), oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://api.com")[0]))
|
||||
}
|
||||
|
||||
s, err := authmethods.NewService(kmsCache, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn)
|
||||
s, err := authmethods.NewService(kmsCache, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn)
|
||||
require.NoError(t, err, "Couldn't create new auth_method service.")
|
||||
|
||||
req := &pbs.ListAuthMethodsRequest{
|
||||
@@ -251,6 +259,9 @@ func TestUpdate_OIDC(t *testing.T) {
|
||||
oidcRepoFn := func() (*oidc.Repository, error) {
|
||||
return oidc.NewRepository(ctx, rw, rw, kms)
|
||||
}
|
||||
ldapRepoFn := func() (*ldap.Repository, error) {
|
||||
return ldap.NewRepository(ctx, rw, rw, kms)
|
||||
}
|
||||
pwRepoFn := func() (*password.Repository, error) {
|
||||
return password.NewRepository(rw, rw, kms)
|
||||
}
|
||||
@@ -260,7 +271,7 @@ func TestUpdate_OIDC(t *testing.T) {
|
||||
iamRepo := iam.TestRepo(t, conn, wrapper)
|
||||
|
||||
o, _ := iam.TestScopes(t, iamRepo)
|
||||
tested, err := authmethods.NewService(kms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn)
|
||||
tested, err := authmethods.NewService(kms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn)
|
||||
require.NoError(t, err, "Error when getting new auth_method service.")
|
||||
|
||||
defaultScopeInfo := &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: o.GetType(), ParentScopeId: scope.Global.String()}
|
||||
@@ -1059,6 +1070,9 @@ func TestUpdate_OIDCDryRun(t *testing.T) {
|
||||
oidcRepoFn := func() (*oidc.Repository, error) {
|
||||
return oidc.NewRepository(ctx, rw, rw, kmsCache)
|
||||
}
|
||||
ldapRepoFn := func() (*ldap.Repository, error) {
|
||||
return ldap.NewRepository(ctx, rw, rw, kmsCache)
|
||||
}
|
||||
pwRepoFn := func() (*password.Repository, error) {
|
||||
return password.NewRepository(rw, rw, kmsCache)
|
||||
}
|
||||
@@ -1118,7 +1132,7 @@ func TestUpdate_OIDCDryRun(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
tested, err := authmethods.NewService(kmsCache, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn)
|
||||
tested, err := authmethods.NewService(kmsCache, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn)
|
||||
require.NoError(t, err, "Error when getting new auth_method service.")
|
||||
cases := []struct {
|
||||
name string
|
||||
@@ -1235,6 +1249,9 @@ func TestChangeState_OIDC(t *testing.T) {
|
||||
oidcRepoFn := func() (*oidc.Repository, error) {
|
||||
return oidc.NewRepository(ctx, rw, rw, kmsCache)
|
||||
}
|
||||
ldapRepoFn := func() (*ldap.Repository, error) {
|
||||
return ldap.NewRepository(ctx, rw, rw, kmsCache)
|
||||
}
|
||||
pwRepoFn := func() (*password.Repository, error) {
|
||||
return password.NewRepository(rw, rw, kmsCache)
|
||||
}
|
||||
@@ -1264,7 +1281,7 @@ func TestChangeState_OIDC(t *testing.T) {
|
||||
mismatchedAM := oidc.TestAuthMethod(t, conn, databaseWrapper, o.PublicId, "inactive", "different_client_id", oidc.ClientSecret(tpClientSecret),
|
||||
oidc.WithIssuer(oidc.TestConvertToUrls(t, tp.Addr())[0]), oidc.WithSigningAlgs(oidc.EdDSA), oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://example.callback:58")[0]), oidc.WithCertificates(tpCert...))
|
||||
|
||||
s, err := authmethods.NewService(kmsCache, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn)
|
||||
s, err := authmethods.NewService(kmsCache, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn)
|
||||
require.NoError(t, err, "Error when getting new auth_method service.")
|
||||
|
||||
wantTemplate := &pb.AuthMethod{
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/hashicorp/boundary/globals"
|
||||
"github.com/hashicorp/boundary/internal/auth/ldap"
|
||||
"github.com/hashicorp/boundary/internal/auth/oidc"
|
||||
"github.com/hashicorp/boundary/internal/auth/password"
|
||||
"github.com/hashicorp/boundary/internal/authtoken"
|
||||
@@ -45,6 +46,9 @@ func TestUpdate_Password(t *testing.T) {
|
||||
oidcRepoFn := func() (*oidc.Repository, error) {
|
||||
return oidc.NewRepository(ctx, rw, rw, kms)
|
||||
}
|
||||
ldapRepoFn := func() (*ldap.Repository, error) {
|
||||
return ldap.NewRepository(ctx, rw, rw, kms)
|
||||
}
|
||||
pwRepoFn := func() (*password.Repository, error) {
|
||||
return password.NewRepository(rw, rw, kms)
|
||||
}
|
||||
@@ -54,7 +58,7 @@ func TestUpdate_Password(t *testing.T) {
|
||||
iamRepo := iam.TestRepo(t, conn, wrapper)
|
||||
|
||||
o, _ := iam.TestScopes(t, iamRepo)
|
||||
tested, err := authmethods.NewService(kms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn)
|
||||
tested, err := authmethods.NewService(kms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn)
|
||||
require.NoError(t, err, "Error when getting new auth_method service.")
|
||||
|
||||
defaultScopeInfo := &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: o.GetType(), ParentScopeId: scope.Global.String()}
|
||||
@@ -482,6 +486,9 @@ func TestAuthenticate_Password(t *testing.T) {
|
||||
oidcRepoFn := func() (*oidc.Repository, error) {
|
||||
return oidc.NewRepository(ctx, rw, rw, kms)
|
||||
}
|
||||
ldapRepoFn := func() (*ldap.Repository, error) {
|
||||
return ldap.NewRepository(ctx, rw, rw, kms)
|
||||
}
|
||||
pwRepoFn := func() (*password.Repository, error) {
|
||||
return password.NewRepository(rw, rw, kms)
|
||||
}
|
||||
@@ -617,7 +624,7 @@ func TestAuthenticate_Password(t *testing.T) {
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
s, err := authmethods.NewService(kms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn)
|
||||
s, err := authmethods.NewService(kms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn)
|
||||
require.NoError(err)
|
||||
|
||||
resp, err := s.Authenticate(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), tc.request)
|
||||
@@ -660,6 +667,9 @@ func TestAuthenticate_AuthAccountConnectedToIamUser_Password(t *testing.T) {
|
||||
oidcRepoFn := func() (*oidc.Repository, error) {
|
||||
return oidc.NewRepository(ctx, rw, rw, kms)
|
||||
}
|
||||
ldapRepoFn := func() (*ldap.Repository, error) {
|
||||
return ldap.NewRepository(ctx, rw, rw, kms)
|
||||
}
|
||||
pwRepoFn := func() (*password.Repository, error) {
|
||||
return password.NewRepository(rw, rw, kms)
|
||||
}
|
||||
@@ -683,7 +693,7 @@ func TestAuthenticate_AuthAccountConnectedToIamUser_Password(t *testing.T) {
|
||||
iamUser, err := iamRepo.LookupUserWithLogin(context.Background(), acct.GetPublicId())
|
||||
require.NoError(err)
|
||||
|
||||
s, err := authmethods.NewService(kms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn)
|
||||
s, err := authmethods.NewService(kms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn, ldapRepoFn)
|
||||
require.NoError(err)
|
||||
resp, err := s.Authenticate(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), &pbs.AuthenticateRequest{
|
||||
AuthMethodId: am.GetPublicId(),
|
||||
|
||||
@@ -5,10 +5,13 @@ package managed_groups
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/hashicorp/boundary/globals"
|
||||
"github.com/hashicorp/boundary/internal/auth"
|
||||
"github.com/hashicorp/boundary/internal/auth/ldap"
|
||||
ldapstore "github.com/hashicorp/boundary/internal/auth/ldap/store"
|
||||
"github.com/hashicorp/boundary/internal/auth/oidc"
|
||||
oidcstore "github.com/hashicorp/boundary/internal/auth/oidc/store"
|
||||
requestauth "github.com/hashicorp/boundary/internal/daemon/controller/auth"
|
||||
@@ -29,13 +32,15 @@ import (
|
||||
|
||||
const (
|
||||
// oidc field names
|
||||
attrFilterField = "attributes.filter"
|
||||
attrFilterField = "attributes.filter"
|
||||
attrGroupNamesField = "attributes.group_names"
|
||||
|
||||
domain = "auth"
|
||||
)
|
||||
|
||||
var (
|
||||
oidcMaskManager handlers.MaskManager
|
||||
ldapMaskManager handlers.MaskManager
|
||||
|
||||
// IdActions contains the set of actions that can be performed on
|
||||
// individual resources
|
||||
@@ -46,6 +51,12 @@ var (
|
||||
action.Update,
|
||||
action.Delete,
|
||||
},
|
||||
ldap.Subtype: {
|
||||
action.NoOp,
|
||||
action.Read,
|
||||
action.Update,
|
||||
action.Delete,
|
||||
},
|
||||
}
|
||||
|
||||
// CollectionActions contains the set of actions that can be performed on
|
||||
@@ -61,6 +72,9 @@ func init() {
|
||||
if oidcMaskManager, err = handlers.NewMaskManager(handlers.MaskDestination{&oidcstore.ManagedGroup{}}, handlers.MaskSource{&pb.ManagedGroup{}, &pb.OidcManagedGroupAttributes{}}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if ldapMaskManager, err = handlers.NewMaskManager(handlers.MaskDestination{&ldapstore.ManagedGroup{}}, handlers.MaskSource{&pb.ManagedGroup{}, &pb.LdapManagedGroupAttributes{}}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Service handles request as described by the pbs.ManagedGroupServiceServer interface.
|
||||
@@ -68,17 +82,21 @@ type Service struct {
|
||||
pbs.UnsafeManagedGroupServiceServer
|
||||
|
||||
oidcRepoFn common.OidcAuthRepoFactory
|
||||
ldapRepoFn common.LdapAuthRepoFactory
|
||||
}
|
||||
|
||||
var _ pbs.ManagedGroupServiceServer = (*Service)(nil)
|
||||
|
||||
// NewService returns a managed group service which handles managed group related requests to boundary.
|
||||
func NewService(oidcRepo common.OidcAuthRepoFactory) (Service, error) {
|
||||
func NewService(ctx context.Context, oidcRepo common.OidcAuthRepoFactory, ldapRepo common.LdapAuthRepoFactory) (Service, error) {
|
||||
const op = "managed_groups.NewService"
|
||||
if oidcRepo == nil {
|
||||
return Service{}, errors.NewDeprecated(errors.InvalidParameter, op, "missing oidc repository provided")
|
||||
switch {
|
||||
case oidcRepo == nil:
|
||||
return Service{}, errors.New(ctx, errors.InvalidParameter, op, "missing oidc repository provided")
|
||||
case ldapRepo == nil:
|
||||
return Service{}, errors.New(ctx, errors.InvalidParameter, op, "missing ldap repository provided")
|
||||
}
|
||||
return Service{oidcRepoFn: oidcRepo}, nil
|
||||
return Service{oidcRepoFn: oidcRepo, ldapRepoFn: ldapRepo}, nil
|
||||
}
|
||||
|
||||
// ListManagedGroups implements the interface pbs.ManagedGroupsServiceServer.
|
||||
@@ -307,6 +325,29 @@ func (s Service) getFromRepo(ctx context.Context, id string) (auth.ManagedGroup,
|
||||
}
|
||||
}
|
||||
out = mg
|
||||
case ldap.Subtype:
|
||||
repo, err := s.ldapRepoFn()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
mg, err := repo.LookupManagedGroup(ctx, id)
|
||||
if err != nil {
|
||||
if errors.IsNotFoundError(err) {
|
||||
return nil, nil, handlers.NotFoundErrorf("LDAP ManagedGroup %q doesn't exist.", id)
|
||||
}
|
||||
return nil, nil, err
|
||||
}
|
||||
ids, err := repo.ListManagedGroupMembershipsByGroup(ctx, mg.GetPublicId())
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if len(ids) > 0 {
|
||||
memberIds = make([]string, len(ids))
|
||||
for i, v := range ids {
|
||||
memberIds[i] = v.MemberId
|
||||
}
|
||||
}
|
||||
out = mg
|
||||
default:
|
||||
return nil, nil, handlers.NotFoundErrorf("Unrecognized id.")
|
||||
}
|
||||
@@ -328,7 +369,7 @@ func (s Service) createOidcInRepo(ctx context.Context, am auth.AuthMethod, item
|
||||
attrs := item.GetOidcManagedGroupAttributes()
|
||||
mg, err := oidc.NewManagedGroup(ctx, am.GetPublicId(), attrs.GetFilter(), opts...)
|
||||
if err != nil {
|
||||
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to build user for creation: %v.", err)
|
||||
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to build managed group for creation: %v.", err)
|
||||
}
|
||||
repo, err := s.oidcRepoFn()
|
||||
if err != nil {
|
||||
@@ -345,6 +386,38 @@ func (s Service) createOidcInRepo(ctx context.Context, am auth.AuthMethod, item
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s Service) createLdapInRepo(ctx context.Context, am auth.AuthMethod, item *pb.ManagedGroup) (*ldap.ManagedGroup, error) {
|
||||
const op = "managed_groups.(Service).createLdapInRepo"
|
||||
if item == nil {
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "missing item")
|
||||
}
|
||||
var opts []ldap.Option
|
||||
if item.GetName() != nil {
|
||||
opts = append(opts, ldap.WithName(ctx, item.GetName().GetValue()))
|
||||
}
|
||||
if item.GetDescription() != nil {
|
||||
opts = append(opts, ldap.WithDescription(ctx, item.GetDescription().GetValue()))
|
||||
}
|
||||
attrs := item.GetLdapManagedGroupAttributes()
|
||||
mg, err := ldap.NewManagedGroup(ctx, am.GetPublicId(), attrs.GetGroupNames(), opts...)
|
||||
if err != nil {
|
||||
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to build managed group for creation: %v.", err)
|
||||
}
|
||||
repo, err := s.ldapRepoFn()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out, err := repo.CreateManagedGroup(ctx, am.GetScopeId(), mg)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to create managed group"))
|
||||
}
|
||||
if out == nil {
|
||||
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create managed group but no error returned from repository.")
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s Service) createInRepo(ctx context.Context, am auth.AuthMethod, item *pb.ManagedGroup) (auth.ManagedGroup, error) {
|
||||
const op = "managed_groups.(Service).createInRepo"
|
||||
if item == nil {
|
||||
@@ -361,6 +434,15 @@ func (s Service) createInRepo(ctx context.Context, am auth.AuthMethod, item *pb.
|
||||
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create managed group but no error returned from repository.")
|
||||
}
|
||||
out = am
|
||||
case ldap.Subtype:
|
||||
am, err := s.createLdapInRepo(ctx, am, item)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
if am == nil {
|
||||
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create ldap managed group but no error returned from repository.")
|
||||
}
|
||||
out = am
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -401,6 +483,46 @@ func (s Service) updateOidcInRepo(ctx context.Context, scopeId, amId, id string,
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s Service) updateLdapInRepo(ctx context.Context, scopeId, amId, id string, mask []string, item *pb.ManagedGroup) (*ldap.ManagedGroup, error) {
|
||||
const op = "managed_groups.(Service).updateLdapInRepo"
|
||||
if item == nil {
|
||||
return nil, errors.New(ctx, errors.InvalidParameter, op, "nil managed group.")
|
||||
}
|
||||
mg := ldap.AllocManagedGroup()
|
||||
mg.PublicId = id
|
||||
if item.GetName() != nil {
|
||||
mg.Name = item.GetName().GetValue()
|
||||
}
|
||||
if item.GetDescription() != nil {
|
||||
mg.Description = item.GetDescription().GetValue()
|
||||
}
|
||||
// Set this regardless; it'll only take effect if the masks contain the value
|
||||
encodedGroupNames, err := json.Marshal(item.GetLdapManagedGroupAttributes().GetGroupNames())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to encode group names"))
|
||||
}
|
||||
mg.GroupNames = string(encodedGroupNames)
|
||||
|
||||
version := item.GetVersion()
|
||||
|
||||
dbMask := ldapMaskManager.Translate(mask)
|
||||
if len(dbMask) == 0 {
|
||||
return nil, handlers.InvalidArgumentErrorf("No valid fields included in the update mask.", map[string]string{"update_mask": "No valid fields provided in the update mask."})
|
||||
}
|
||||
repo, err := s.ldapRepoFn()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
out, rowsUpdated, err := repo.UpdateManagedGroup(ctx, scopeId, mg, version, dbMask)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to update managed group"))
|
||||
}
|
||||
if rowsUpdated == 0 {
|
||||
return nil, handlers.NotFoundErrorf("Managed Group %q doesn't exist or incorrect version provided.", id)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s Service) updateInRepo(ctx context.Context, scopeId, authMethodId string, req *pbs.UpdateManagedGroupRequest) (auth.ManagedGroup, error) {
|
||||
const op = "managed_groups.(Service).updateInRepo"
|
||||
var out auth.ManagedGroup
|
||||
@@ -414,6 +536,15 @@ func (s Service) updateInRepo(ctx context.Context, scopeId, authMethodId string,
|
||||
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to update managed group but no error returned from repository.")
|
||||
}
|
||||
out = mg
|
||||
case ldap.Subtype:
|
||||
mg, err := s.updateLdapInRepo(ctx, scopeId, authMethodId, req.GetId(), req.GetUpdateMask().GetPaths(), req.GetItem())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
if mg == nil {
|
||||
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to update managed group but no error returned from repository.")
|
||||
}
|
||||
out = mg
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -429,6 +560,12 @@ func (s Service) deleteFromRepo(ctx context.Context, scopeId, id string) (bool,
|
||||
return false, iErr
|
||||
}
|
||||
rows, err = repo.DeleteManagedGroup(ctx, scopeId, id)
|
||||
case ldap.Subtype:
|
||||
repo, iErr := s.ldapRepoFn()
|
||||
if iErr != nil {
|
||||
return false, iErr
|
||||
}
|
||||
rows, err = repo.DeleteManagedGroup(ctx, scopeId, id)
|
||||
}
|
||||
if err != nil {
|
||||
if errors.IsNotFoundError(err) {
|
||||
@@ -456,6 +593,18 @@ func (s Service) listFromRepo(ctx context.Context, authMethodId string) ([]auth.
|
||||
for _, a := range oidcl {
|
||||
outUl = append(outUl, a)
|
||||
}
|
||||
case ldap.Subtype:
|
||||
ldapRepo, err := s.ldapRepoFn()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
oidcl, err := ldapRepo.ListManagedGroups(ctx, authMethodId)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(ctx, err, op)
|
||||
}
|
||||
for _, a := range oidcl {
|
||||
outUl = append(outUl, a)
|
||||
}
|
||||
}
|
||||
return outUl, nil
|
||||
}
|
||||
@@ -468,6 +617,11 @@ func (s Service) parentAndAuthResult(ctx context.Context, id string, a action.Ty
|
||||
res.Error = err
|
||||
return nil, res
|
||||
}
|
||||
ldapRepo, err := s.ldapRepoFn()
|
||||
if err != nil {
|
||||
res.Error = err
|
||||
return nil, res
|
||||
}
|
||||
|
||||
var parentId string
|
||||
opts := []requestauth.Option{requestauth.WithType(resource.ManagedGroup), requestauth.WithAction(a)}
|
||||
@@ -477,16 +631,27 @@ func (s Service) parentAndAuthResult(ctx context.Context, id string, a action.Ty
|
||||
default:
|
||||
switch subtypes.SubtypeFromId(domain, id) {
|
||||
case oidc.Subtype:
|
||||
acct, err := oidcRepo.LookupManagedGroup(ctx, id)
|
||||
grp, err := oidcRepo.LookupManagedGroup(ctx, id)
|
||||
if err != nil {
|
||||
res.Error = err
|
||||
return nil, res
|
||||
}
|
||||
if acct == nil {
|
||||
if grp == nil {
|
||||
res.Error = handlers.NotFoundError()
|
||||
return nil, res
|
||||
}
|
||||
parentId = acct.GetAuthMethodId()
|
||||
parentId = grp.GetAuthMethodId()
|
||||
case ldap.Subtype:
|
||||
grp, err := ldapRepo.LookupManagedGroup(ctx, id)
|
||||
if err != nil {
|
||||
res.Error = err
|
||||
return nil, res
|
||||
}
|
||||
if grp == nil {
|
||||
res.Error = handlers.NotFoundError()
|
||||
return nil, res
|
||||
}
|
||||
parentId = grp.GetAuthMethodId()
|
||||
default:
|
||||
res.Error = errors.New(ctx, errors.InvalidPublicId, op, "unrecognized managed group subtype")
|
||||
return nil, res
|
||||
@@ -508,6 +673,18 @@ func (s Service) parentAndAuthResult(ctx context.Context, id string, a action.Ty
|
||||
}
|
||||
authMeth = am
|
||||
opts = append(opts, requestauth.WithScopeId(am.GetScopeId()))
|
||||
case ldap.Subtype:
|
||||
am, err := ldapRepo.LookupAuthMethod(ctx, parentId)
|
||||
if err != nil {
|
||||
res.Error = err
|
||||
return nil, res
|
||||
}
|
||||
if am == nil {
|
||||
res.Error = handlers.NotFoundError()
|
||||
return nil, res
|
||||
}
|
||||
authMeth = am
|
||||
opts = append(opts, requestauth.WithScopeId(am.GetScopeId()))
|
||||
default:
|
||||
res.Error = errors.New(ctx, errors.InvalidPublicId, op, "unrecognized auth method subtype")
|
||||
return nil, res
|
||||
@@ -568,6 +745,25 @@ func toProto(ctx context.Context, in auth.ManagedGroup, opt ...handlers.Option)
|
||||
out.Attrs = &pb.ManagedGroup_OidcManagedGroupAttributes{
|
||||
OidcManagedGroupAttributes: attrs,
|
||||
}
|
||||
case *ldap.ManagedGroup:
|
||||
if outputFields.Has(globals.TypeField) {
|
||||
out.Type = ldap.Subtype.String()
|
||||
}
|
||||
if !outputFields.Has(globals.AttributesField) {
|
||||
break
|
||||
}
|
||||
|
||||
var grpNames []string
|
||||
if err := json.Unmarshal([]byte(i.GetGroupNames()), &grpNames); err != nil {
|
||||
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "unable to unmarshal group names")
|
||||
}
|
||||
|
||||
attrs := &pb.LdapManagedGroupAttributes{
|
||||
GroupNames: grpNames,
|
||||
}
|
||||
out.Attrs = &pb.ManagedGroup_LdapManagedGroupAttributes{
|
||||
LdapManagedGroupAttributes: attrs,
|
||||
}
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
@@ -582,7 +778,7 @@ func validateGetRequest(req *pbs.GetManagedGroupRequest) error {
|
||||
if req == nil {
|
||||
return errors.NewDeprecated(errors.InvalidParameter, op, "nil request")
|
||||
}
|
||||
return handlers.ValidateGetRequest(handlers.NoopValidatorFn, req, globals.OidcManagedGroupPrefix)
|
||||
return handlers.ValidateGetRequest(handlers.NoopValidatorFn, req, globals.OidcManagedGroupPrefix, globals.LdapManagedGroupPrefix)
|
||||
}
|
||||
|
||||
func validateCreateRequest(req *pbs.CreateManagedGroupRequest) error {
|
||||
@@ -612,6 +808,18 @@ func validateCreateRequest(req *pbs.CreateManagedGroupRequest) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
case ldap.Subtype:
|
||||
if req.GetItem().GetType() != "" && req.GetItem().GetType() != ldap.Subtype.String() {
|
||||
badFields[globals.TypeField] = "Doesn't match the parent resource's type."
|
||||
}
|
||||
attrs := req.GetItem().GetLdapManagedGroupAttributes()
|
||||
if attrs == nil {
|
||||
badFields[globals.AttributesField] = "Attribute fields is required."
|
||||
} else {
|
||||
if len(attrs.GroupNames) == 0 {
|
||||
badFields[attrGroupNamesField] = "This field is required."
|
||||
}
|
||||
}
|
||||
default:
|
||||
badFields[globals.AuthMethodIdField] = "Unknown auth method type from ID."
|
||||
}
|
||||
@@ -646,11 +854,21 @@ func validateUpdateRequest(req *pbs.UpdateManagedGroupRequest) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
case ldap.Subtype:
|
||||
if req.GetItem().GetType() != "" && req.GetItem().GetType() != ldap.Subtype.String() {
|
||||
badFields[globals.TypeField] = "Cannot modify the resource type."
|
||||
}
|
||||
attrs := req.GetItem().GetLdapManagedGroupAttributes()
|
||||
if handlers.MaskContains(req.GetUpdateMask().GetPaths(), attrGroupNamesField) {
|
||||
if len(attrs.GroupNames) == 0 {
|
||||
badFields[attrFilterField] = "Field cannot be empty."
|
||||
}
|
||||
}
|
||||
default:
|
||||
badFields[globals.IdField] = "Unrecognized resource type."
|
||||
}
|
||||
return badFields
|
||||
}, globals.OidcManagedGroupPrefix)
|
||||
}, globals.OidcManagedGroupPrefix, globals.LdapManagedGroupPrefix)
|
||||
}
|
||||
|
||||
func validateDeleteRequest(req *pbs.DeleteManagedGroupRequest) error {
|
||||
@@ -658,7 +876,7 @@ func validateDeleteRequest(req *pbs.DeleteManagedGroupRequest) error {
|
||||
if req == nil {
|
||||
return errors.NewDeprecated(errors.InvalidParameter, op, "nil request")
|
||||
}
|
||||
return handlers.ValidateDeleteRequest(handlers.NoopValidatorFn, req, globals.OidcManagedGroupPrefix)
|
||||
return handlers.ValidateDeleteRequest(handlers.NoopValidatorFn, req, globals.OidcManagedGroupPrefix, globals.LdapManagedGroupPrefix)
|
||||
}
|
||||
|
||||
func validateListRequest(req *pbs.ListManagedGroupsRequest) error {
|
||||
@@ -667,7 +885,7 @@ func validateListRequest(req *pbs.ListManagedGroupsRequest) error {
|
||||
return errors.NewDeprecated(errors.InvalidParameter, op, "nil request")
|
||||
}
|
||||
badFields := map[string]string{}
|
||||
if !handlers.ValidId(handlers.Id(req.GetAuthMethodId()), globals.OidcAuthMethodPrefix) {
|
||||
if !handlers.ValidId(handlers.Id(req.GetAuthMethodId()), globals.OidcAuthMethodPrefix, globals.LdapAuthMethodPrefix) {
|
||||
badFields[globals.AuthMethodIdField] = "Invalid formatted identifier."
|
||||
}
|
||||
if _, err := handlers.NewFilter(req.GetFilter()); err != nil {
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -9,6 +9,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/boundary/globals"
|
||||
"github.com/hashicorp/boundary/internal/auth/ldap"
|
||||
"github.com/hashicorp/boundary/internal/auth/oidc"
|
||||
"github.com/hashicorp/boundary/internal/auth/password"
|
||||
pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services"
|
||||
@@ -78,6 +79,48 @@ func TestValidateCreateRequest(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mismatched ldap authmethod pw type",
|
||||
item: &pb.ManagedGroup{
|
||||
Type: "oidc",
|
||||
AuthMethodId: globals.LdapAuthMethodPrefix + "_1234567890",
|
||||
},
|
||||
errContains: fieldError(globals.TypeField, "Doesn't match the parent resource's type."),
|
||||
},
|
||||
{
|
||||
name: "missing ldap attributes",
|
||||
item: &pb.ManagedGroup{
|
||||
Type: ldap.Subtype.String(),
|
||||
AuthMethodId: globals.LdapAuthMethodPrefix + "_1234567890",
|
||||
Attrs: nil,
|
||||
},
|
||||
errContains: fieldError(globals.AttributesField, "Attribute fields is required."),
|
||||
},
|
||||
{
|
||||
name: "bad ldap attributes",
|
||||
item: &pb.ManagedGroup{
|
||||
Type: ldap.Subtype.String(),
|
||||
AuthMethodId: globals.LdapAuthMethodPrefix + "_1234567890",
|
||||
Attrs: &pb.ManagedGroup_LdapManagedGroupAttributes{
|
||||
LdapManagedGroupAttributes: &pb.LdapManagedGroupAttributes{
|
||||
GroupNames: []string{},
|
||||
},
|
||||
},
|
||||
},
|
||||
errContains: "name: \"attributes.group_names\", desc: \"This field is required.",
|
||||
},
|
||||
{
|
||||
name: "no ldap errors",
|
||||
item: &pb.ManagedGroup{
|
||||
Type: ldap.Subtype.String(),
|
||||
AuthMethodId: globals.LdapAuthMethodPrefix + "_1234567890",
|
||||
Attrs: &pb.ManagedGroup_LdapManagedGroupAttributes{
|
||||
LdapManagedGroupAttributes: &pb.LdapManagedGroupAttributes{
|
||||
GroupNames: []string{"admin"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
tc := tc // capture range variable
|
||||
@@ -113,7 +156,7 @@ func TestValidateUpdateRequest(t *testing.T) {
|
||||
errContains: fieldError(globals.TypeField, "Cannot modify the resource type."),
|
||||
},
|
||||
{
|
||||
name: "no error",
|
||||
name: "oidc no error",
|
||||
req: &pbs.UpdateManagedGroupRequest{
|
||||
Id: globals.OidcManagedGroupPrefix + "_1234567890",
|
||||
UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{}},
|
||||
@@ -127,6 +170,31 @@ func TestValidateUpdateRequest(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ldap to password change type",
|
||||
req: &pbs.UpdateManagedGroupRequest{
|
||||
Id: globals.LdapManagedGroupPrefix + "_1234567890",
|
||||
Item: &pb.ManagedGroup{
|
||||
Type: password.Subtype.String(),
|
||||
},
|
||||
},
|
||||
errContains: fieldError(globals.TypeField, "Cannot modify the resource type."),
|
||||
},
|
||||
{
|
||||
name: "ldap no error",
|
||||
req: &pbs.UpdateManagedGroupRequest{
|
||||
Id: globals.LdapManagedGroupPrefix + "_1234567890",
|
||||
UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{}},
|
||||
Item: &pb.ManagedGroup{
|
||||
Version: 1,
|
||||
Attrs: &pb.ManagedGroup_LdapManagedGroupAttributes{
|
||||
LdapManagedGroupAttributes: &pb.LdapManagedGroupAttributes{
|
||||
GroupNames: []string{"admin"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
tc := tc // capture range variable
|
||||
|
||||
@@ -572,7 +572,7 @@ func (s Service) updateInRepo(ctx context.Context, parentScope *pb.ScopeInfo, sc
|
||||
opts = append(opts, iam.WithName(scopeName))
|
||||
}
|
||||
if primaryAuthMethodId := item.GetPrimaryAuthMethodId(); primaryAuthMethodId != nil {
|
||||
if !handlers.ValidId(handlers.Id(primaryAuthMethodId.GetValue()), globals.PasswordAuthMethodPrefix, globals.OidcAuthMethodPrefix) {
|
||||
if !handlers.ValidId(handlers.Id(primaryAuthMethodId.GetValue()), globals.PasswordAuthMethodPrefix, globals.OidcAuthMethodPrefix, globals.LdapAuthMethodPrefix) {
|
||||
return nil, handlers.InvalidArgumentErrorf("Error in provided request.", map[string]string{"primary_auth_method_id": "Improperly formatted identifier"})
|
||||
}
|
||||
scopePrimaryAuthMethodId = primaryAuthMethodId.GetValue()
|
||||
@@ -929,7 +929,7 @@ func validateUpdateRequest(req *pbs.UpdateScopeRequest) error {
|
||||
if item.GetUpdatedTime() != nil {
|
||||
badFields["updated_time"] = "This is a read only field and cannot be specified in an update request."
|
||||
}
|
||||
if item.GetPrimaryAuthMethodId().GetValue() != "" && !handlers.ValidId(handlers.Id(item.GetPrimaryAuthMethodId().GetValue()), globals.PasswordAuthMethodPrefix, globals.OidcAuthMethodPrefix) {
|
||||
if item.GetPrimaryAuthMethodId().GetValue() != "" && !handlers.ValidId(handlers.Id(item.GetPrimaryAuthMethodId().GetValue()), globals.PasswordAuthMethodPrefix, globals.OidcAuthMethodPrefix, globals.LdapAuthMethodPrefix) {
|
||||
badFields["primary_auth_method_id"] = "Improperly formatted identifier."
|
||||
}
|
||||
if len(badFields) > 0 {
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/hashicorp/boundary/globals"
|
||||
"github.com/hashicorp/boundary/internal/auth/ldap"
|
||||
"github.com/hashicorp/boundary/internal/auth/oidc"
|
||||
"github.com/hashicorp/boundary/internal/auth/password"
|
||||
"github.com/hashicorp/boundary/internal/authtoken"
|
||||
@@ -2438,6 +2439,9 @@ func TestAuthorizeSession(t *testing.T) {
|
||||
oidcAuthRepoFn := func() (*oidc.Repository, error) {
|
||||
return oidc.NewRepository(ctx, rw, rw, kms)
|
||||
}
|
||||
ldapAuthRepoFn := func() (*ldap.Repository, error) {
|
||||
return ldap.NewRepository(ctx, rw, rw, kms)
|
||||
}
|
||||
|
||||
plg := host.TestPlugin(t, conn, "test")
|
||||
plgm := map[string]plgpb.HostPluginServiceClient{
|
||||
@@ -2485,6 +2489,7 @@ func TestAuthorizeSession(t *testing.T) {
|
||||
serversRepoFn,
|
||||
passwordAuthRepoFn,
|
||||
oidcAuthRepoFn,
|
||||
ldapAuthRepoFn,
|
||||
kms,
|
||||
&authpb.RequestInfo{
|
||||
Token: at.GetToken(),
|
||||
|
||||
@@ -53,6 +53,7 @@ func requestCtxInterceptor(
|
||||
serversRepoFn common.ServersRepoFactory,
|
||||
passwordAuthRepoFn common.PasswordAuthRepoFactory,
|
||||
oidcAuthRepoFn common.OidcAuthRepoFactory,
|
||||
ldapAuthRepoFn common.LdapAuthRepoFactory,
|
||||
kms *kms.Kms,
|
||||
ticket string,
|
||||
eventer *event.Eventer,
|
||||
@@ -110,7 +111,7 @@ func requestCtxInterceptor(
|
||||
return nil, errors.New(interceptorCtx, errors.Internal, op, "Invalid context (bad ticket)")
|
||||
}
|
||||
|
||||
interceptorCtx = auth.NewVerifierContextWithAccounts(interceptorCtx, iamRepoFn, authTokenRepoFn, serversRepoFn, passwordAuthRepoFn, oidcAuthRepoFn, kms, &requestInfo)
|
||||
interceptorCtx = auth.NewVerifierContextWithAccounts(interceptorCtx, iamRepoFn, authTokenRepoFn, serversRepoFn, passwordAuthRepoFn, oidcAuthRepoFn, ldapAuthRepoFn, kms, &requestInfo)
|
||||
|
||||
// Add general request information to the context. The information from
|
||||
// the auth verifier context is pretty specifically curated to
|
||||
|
||||
@@ -314,7 +314,7 @@ func Test_requestCtxInterceptor(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert, require := assert.New(t), require.New(t)
|
||||
interceptor, err := requestCtxInterceptor(factoryCtx, tt.iamRepoFn, tt.authTokenRepoFn, tt.serversRepoFn, nil, nil, tt.kms, tt.ticket, tt.eventer)
|
||||
interceptor, err := requestCtxInterceptor(factoryCtx, tt.iamRepoFn, tt.authTokenRepoFn, tt.serversRepoFn, nil, nil, nil, tt.kms, tt.ticket, tt.eventer)
|
||||
if tt.wantFactoryErr {
|
||||
require.Error(err)
|
||||
assert.Nil(interceptor)
|
||||
|
||||
@@ -46,7 +46,7 @@ func closeListener(_ context.Context, l net.Listener, _ any) error {
|
||||
func (c *Controller) startListeners() error {
|
||||
servers := make([]func(), 0, len(c.conf.Listeners))
|
||||
|
||||
grpcServer, gwTicket, err := newGrpcServer(c.baseContext, c.IamRepoFn, c.AuthTokenRepoFn, c.ServersRepoFn, c.PasswordAuthRepoFn, c.OidcRepoFn, c.kms, c.conf.Eventer)
|
||||
grpcServer, gwTicket, err := newGrpcServer(c.baseContext, c.IamRepoFn, c.AuthTokenRepoFn, c.ServersRepoFn, c.PasswordAuthRepoFn, c.OidcRepoFn, c.LdapRepoFn, c.kms, c.conf.Eventer)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create new grpc server: %w", err)
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ begin;
|
||||
alter column auth_account_email type wh_dim_text
|
||||
;
|
||||
|
||||
-- Replaced in 64/01_wh_user_dimension_ldap.up.sql
|
||||
drop view whx_user_dimension_source;
|
||||
create view whx_user_dimension_source as
|
||||
select -- id is the first column in the target view
|
||||
|
||||
@@ -0,0 +1,644 @@
|
||||
-- Copyright (c) HashiCorp, Inc.
|
||||
-- SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
begin;
|
||||
|
||||
create table auth_ldap_method_state_enm (
|
||||
name text primary key
|
||||
constraint name_only_predefined_ldap_method_states_allowed
|
||||
check (name in ('inactive', 'active-private', 'active-public'))
|
||||
);
|
||||
comment on table auth_ldap_method_state_enm is
|
||||
'auth_ldap_method_state_enm entries enumerate the valid auth ldap method states';
|
||||
|
||||
-- populate the values of auth_ldap_method_state_enm
|
||||
insert into auth_ldap_method_state_enm(name)
|
||||
values
|
||||
('inactive'),
|
||||
('active-private'),
|
||||
('active-public');
|
||||
|
||||
create table auth_ldap_method (
|
||||
public_id wt_public_id primary key,
|
||||
scope_id wt_scope_id not null,
|
||||
name wt_name,
|
||||
description wt_description,
|
||||
create_time wt_timestamp,
|
||||
update_time wt_timestamp,
|
||||
version wt_version,
|
||||
state text not null
|
||||
constraint auth_ldap_method_state_enm_fkey
|
||||
references auth_ldap_method_state_enm(name)
|
||||
on delete restrict
|
||||
on update cascade,
|
||||
start_tls bool not null default false,
|
||||
insecure_tls bool not null default false,
|
||||
discover_dn bool not null default false,
|
||||
anon_group_search bool not null default false,
|
||||
upn_domain text
|
||||
constraint upn_domain_too_short
|
||||
check (length(trim(upn_domain)) > 0)
|
||||
constraint upn_domain_too_long
|
||||
check (length(trim(upn_domain)) < 253),
|
||||
enable_groups bool not null default false,
|
||||
use_token_groups bool not null default false,
|
||||
constraint auth_method_fkey
|
||||
foreign key (scope_id, public_id)
|
||||
references auth_method (scope_id, public_id)
|
||||
on delete cascade
|
||||
on update cascade,
|
||||
constraint auth_ldap_method_scope_id_name_uq
|
||||
unique(scope_id, name),
|
||||
constraint auth_ldap_method_scope_id_public_id_uq
|
||||
unique(scope_id, public_id)
|
||||
);
|
||||
comment on table auth_ldap_method is
|
||||
'auth_ldap_method entries are the current ldap auth methods configured for '
|
||||
'existing scopes';
|
||||
|
||||
-- auth_ldap_method column triggers
|
||||
create trigger insert_auth_method_subtype before insert on auth_ldap_method
|
||||
for each row execute procedure insert_auth_method_subtype();
|
||||
|
||||
create trigger delete_auth_method_subtype after delete on auth_ldap_method
|
||||
for each row execute procedure delete_auth_method_subtype();
|
||||
|
||||
create trigger update_auth_method_subtype before update on auth_ldap_method
|
||||
for each row execute procedure update_auth_method_subtype();
|
||||
|
||||
create trigger update_time_column before update on auth_ldap_method
|
||||
for each row execute procedure update_time_column();
|
||||
|
||||
create trigger immutable_columns before update on auth_ldap_method
|
||||
for each row execute procedure immutable_columns('public_id', 'scope_id', 'create_time');
|
||||
|
||||
create trigger default_create_time_column before insert on auth_ldap_method
|
||||
for each row execute procedure default_create_time();
|
||||
|
||||
create trigger update_version_column after update on auth_ldap_method
|
||||
for each row execute procedure update_version_column();
|
||||
|
||||
-- auth_ldap_url entries are LDAP URLs that specify an LDAP servers to connect
|
||||
-- to. Examples: ldap://ldap.myorg.com, ldaps://ldap.myorg.com:636. There must
|
||||
-- be at least one and if there's more than one URL configured for an auth
|
||||
-- method, the directories will be tried in connection_priority order if there
|
||||
-- are errors during the connection process. The URL scheme must be either ldap
|
||||
-- or ldaps. The port is optional.If no port is specified, then a default of 389
|
||||
-- is used for ldap and a default of 689 is used for ldaps. (see rfc4516 for
|
||||
-- more information about LDAP URLs)
|
||||
--
|
||||
-- Updates will be implemented as a delete + insert with the auth_ldap_method
|
||||
-- being used as the root aggregate for auth_ldap_url updates.
|
||||
create table auth_ldap_url (
|
||||
create_time wt_timestamp,
|
||||
ldap_method_id wt_public_id not null
|
||||
constraint auth_ldap_method_fkey
|
||||
references auth_ldap_method(public_id)
|
||||
on delete cascade
|
||||
on update cascade,
|
||||
url text not null
|
||||
constraint url_too_short
|
||||
check (length(trim(url)) > 3)
|
||||
constraint url_too_long
|
||||
check (length(trim(url)) < 4000),
|
||||
constraint url_invalid_protocol
|
||||
check (url ~ 'ldaps?:\/\/*'),
|
||||
connection_priority int not null
|
||||
constraint connection_priority_less_than_one
|
||||
check(connection_priority >= 1),
|
||||
primary key(ldap_method_id, connection_priority)
|
||||
);
|
||||
comment on table auth_ldap_url is
|
||||
'auth_ldap_url entries specify a connection URL an LDAP';
|
||||
|
||||
create function auth_ldap_url_parent_children() returns trigger
|
||||
as $$
|
||||
declare
|
||||
n integer;
|
||||
begin
|
||||
if tg_op = 'INSERT' or tg_op = 'UPDATE' then
|
||||
select into n count(*) from auth_ldap_url where ldap_method_id = new.ldap_method_id;
|
||||
if n < 1 then
|
||||
raise exception 'During % of auth_ldap_url: auth_ldap_method id=% must have at least one url, not %',tg_op,new.ldap_method_id,n;
|
||||
end if;
|
||||
end if;
|
||||
if tg_op = 'UPDATE' then
|
||||
select into n count(*) from auth_ldap_url where ldap_method_id = old.ldap_method_id;
|
||||
if n < 1 then
|
||||
raise exception 'During % of %: auth_ldap_method id=% must have at least one url, not %',tg_op,tg_table_name,old.ldap_method_id,n;
|
||||
end if;
|
||||
end if;
|
||||
|
||||
return null;
|
||||
end;
|
||||
$$ language plpgsql;
|
||||
comment on function auth_ldap_url_parent_children() is
|
||||
'function used on auth_ldap_url after insert/update initially deferred to ensure each '
|
||||
'auth_ldap_method has at least one auth_ldap_url. Unfortunately, it cannot be used on '
|
||||
'delete since that would make it impossible to delete an ldap auth method, because you '
|
||||
'would not be able to remove all of its urls';
|
||||
|
||||
create constraint trigger auth_ldap_url_children_per_parent_tg
|
||||
after insert or update or delete on auth_ldap_url deferrable initially deferred
|
||||
for each row execute procedure auth_ldap_url_parent_children();
|
||||
|
||||
create function auth_ldap_method_children() returns trigger
|
||||
as $$
|
||||
declare
|
||||
n integer;
|
||||
begin
|
||||
if tg_op = 'INSERT' then
|
||||
select into n count(*) from auth_ldap_url where ldap_method_id = new.public_id;
|
||||
if n < 1 then
|
||||
raise exception 'During % of auth_ldap_method public_id=% must have at least one url, not %',tg_op,new.public_id,n;
|
||||
end if;
|
||||
-- No need for an UPDATE or DELETE check, as regular referential integrity constraints
|
||||
-- and the trigger on `child' will do the job.
|
||||
|
||||
return null;
|
||||
end if;
|
||||
end;
|
||||
$$ language plpgsql;
|
||||
comment on function auth_ldap_method_children() is
|
||||
'function used on auth_ldap_method after insert initially deferred to ensure each '
|
||||
'auth_ldap_method has at least one auth_ldap_url';
|
||||
|
||||
create constraint trigger auth_ldap_method_children_tg
|
||||
after insert on auth_ldap_method deferrable initially deferred
|
||||
for each row execute procedure auth_ldap_method_children();
|
||||
|
||||
-- auth_ldap_user_entry_search entries specify the required parameters to find a
|
||||
-- user entry before attempting to authenticate the user.
|
||||
--
|
||||
-- Updates will be implemented as a delete + insert with the auth_ldap_method
|
||||
-- being used as the root aggregate for auth_ldap_user_entry_search updates.
|
||||
create table auth_ldap_user_entry_search (
|
||||
create_time wt_timestamp,
|
||||
ldap_method_id wt_public_id primary key
|
||||
constraint auth_ldap_method_fkey
|
||||
references auth_ldap_method(public_id)
|
||||
on delete cascade
|
||||
on update cascade,
|
||||
user_dn text
|
||||
constraint user_dn_too_short
|
||||
check (length(trim(user_dn)) > 0)
|
||||
constraint user_dn_too_long
|
||||
check (length(trim(user_dn)) < 1025),
|
||||
user_attr text
|
||||
constraint user_attr_too_short
|
||||
check (length(trim(user_attr)) > 0)
|
||||
constraint user_attr_too_long
|
||||
check (length(trim(user_attr)) < 1025),
|
||||
user_filter text
|
||||
constraint user_filter_too_short
|
||||
check (length(trim(user_filter)) > 0)
|
||||
constraint user_filter_too_long
|
||||
check (length(trim(user_filter)) < 2049),
|
||||
constraint all_fields_are_not_null
|
||||
check (
|
||||
not(user_dn, user_attr, user_filter) is null
|
||||
)
|
||||
);
|
||||
comment on table auth_ldap_user_entry_search is
|
||||
'auth_ldap_user_entry_search entries specify the required parameters to find '
|
||||
'a user entry before attempting to authenticate the user';
|
||||
|
||||
-- auth_ldap_group_entry_search entries specify the required parameters to find
|
||||
-- the groups a user is a member of
|
||||
--
|
||||
-- Updates will be implemented as a delete + insert with the auth_ldap_method
|
||||
-- being used as the root aggregate for auth_ldap_user_entry_search updates.
|
||||
create table auth_ldap_group_entry_search (
|
||||
create_time wt_timestamp,
|
||||
ldap_method_id wt_public_id primary key
|
||||
constraint auth_ldap_method_fkey
|
||||
references auth_ldap_method(public_id)
|
||||
on delete cascade
|
||||
on update cascade,
|
||||
group_dn text not null -- required
|
||||
constraint group_dn_too_short
|
||||
check (length(trim(group_dn)) > 0)
|
||||
constraint group_dn_too_long
|
||||
check (length(trim(group_dn)) < 1025),
|
||||
group_attr text
|
||||
constraint group_attr_too_short
|
||||
check (length(trim(group_attr)) > 0)
|
||||
constraint group_attr_too_long
|
||||
check (length(trim(group_attr)) < 1025),
|
||||
group_filter text
|
||||
constraint group_filter_too_short
|
||||
check (length(trim(group_filter)) > 0)
|
||||
constraint group_filter_too_long
|
||||
check (length(trim(group_filter)) < 2049)
|
||||
);
|
||||
comment on table auth_ldap_group_entry_search is
|
||||
'auth_ldap_group_entry_search entries specify the required parameters to find '
|
||||
'the groups a user is a member of';
|
||||
|
||||
create function auth_ldap_method_group_search() returns trigger
|
||||
as $$
|
||||
declare
|
||||
n integer;
|
||||
begin
|
||||
if new.enable_groups = true and new.use_token_groups = false then
|
||||
select into n count(*) from auth_ldap_group_entry_search where ldap_method_id = new.public_id;
|
||||
if n < 1 then
|
||||
raise exception 'During % of auth_ldap_method public_id=% must have a configured group_dn when enable_groups = true and use_token_groups = false',tg_op,new.public_id;
|
||||
end if;
|
||||
end if;
|
||||
return null;
|
||||
end;
|
||||
$$ language plpgsql;
|
||||
comment on function auth_ldap_method_children() is
|
||||
'function used on auth_ldap_method after insert/update initially deferred to ensure each '
|
||||
'groups search is properly configured when enable_groups is true and use_token_groups is false';
|
||||
|
||||
create constraint trigger auth_ldap_method_group_search
|
||||
after insert or update on auth_ldap_method deferrable initially deferred
|
||||
for each row execute procedure auth_ldap_method_group_search();
|
||||
|
||||
-- auth_ldap_certificate entries are optional PEM encoded x509 certificates.
|
||||
-- Each entry is a single certificate. An ldap auth method may have 0 or more
|
||||
-- of these optional x509s. If an auth method has any cert entries, they are
|
||||
-- used as trust anchors when connecting to the auth method's ldap provider
|
||||
-- (instead of the host system's cert chain).
|
||||
create table auth_ldap_certificate (
|
||||
create_time wt_timestamp,
|
||||
ldap_method_id wt_public_id not null
|
||||
constraint auth_ldap_method_fkey
|
||||
references auth_ldap_method(public_id)
|
||||
on delete cascade
|
||||
on update cascade,
|
||||
certificate bytea not null
|
||||
constraint certificate_must_not_be_empty
|
||||
check(length(certificate) > 0),
|
||||
primary key(ldap_method_id, certificate)
|
||||
);
|
||||
comment on table auth_ldap_certificate is
|
||||
'auth_ldap_certificate entries are optional PEM encoded x509 certificates. '
|
||||
'Each entry is a single certificate. An ldap auth method may have 0 or more '
|
||||
'of these optional x509s. If an auth method has any cert entries, they are '
|
||||
'used as trust anchors when connecting to the auth methods ldap provider '
|
||||
'(instead of the host system cert chain)';
|
||||
|
||||
create table auth_ldap_client_certificate (
|
||||
create_time wt_timestamp,
|
||||
ldap_method_id wt_public_id primary key
|
||||
constraint auth_ldap_method_fkey
|
||||
references auth_ldap_method (public_id)
|
||||
on delete cascade
|
||||
on update cascade,
|
||||
certificate bytea not null -- PEM encoded certificate
|
||||
constraint certificate_must_not_be_empty
|
||||
check(length(certificate) > 0),
|
||||
certificate_key bytea not null -- encrypted PEM encoded private key for certificate
|
||||
constraint certificate_key_must_not_be_empty
|
||||
check(length(certificate_key) > 0),
|
||||
certificate_key_hmac bytea not null
|
||||
constraint certificate_key_hmac_must_not_be_empty
|
||||
check(length(certificate_key_hmac) > 0),
|
||||
key_id text not null
|
||||
constraint kms_data_key_version_fkey
|
||||
references kms_data_key_version (private_id)
|
||||
on delete restrict
|
||||
on update cascade
|
||||
);
|
||||
comment on table auth_ldap_client_certificate is
|
||||
'auth_ldap_client_certificate entries contains a client certificate that a '
|
||||
'auth_ldap_method uses for mTLS when connecting to an LDAP server. '
|
||||
'An auth_ldap_method can have 0 or 1 client certificates.';
|
||||
|
||||
create table auth_ldap_bind_credential (
|
||||
create_time wt_timestamp,
|
||||
ldap_method_id wt_public_id primary key
|
||||
constraint auth_ldap_method_fkey
|
||||
references auth_ldap_method (public_id)
|
||||
on delete cascade
|
||||
on update cascade,
|
||||
dn text not null
|
||||
constraint dn_too_short
|
||||
check (length(trim(dn)) > 0)
|
||||
constraint dn_too_long
|
||||
check (length(trim(dn)) < 2049),
|
||||
password bytea not null
|
||||
constraint password_not_empty
|
||||
check(length(password) > 0), -- encrypted password0
|
||||
password_hmac bytea not null
|
||||
constraint password_hmac_not_empty
|
||||
check(length(password_hmac) > 0),
|
||||
key_id text not null
|
||||
constraint kms_data_key_version_fkey
|
||||
references kms_data_key_version (private_id)
|
||||
on delete restrict
|
||||
on update cascade
|
||||
);
|
||||
comment on table auth_ldap_bind_credential is
|
||||
'auth_ldap_bind_credential entries allow Boundary to bind (aka authenticate) using '
|
||||
'the provided credentials when searching for the user entry used to authenticate.';
|
||||
|
||||
-- auth_ldap_account_attribute_map entries are the optional attribute maps from custom
|
||||
-- attributes to the standard attribute of fullname and email. There can be 0 or more
|
||||
-- for each parent ldap auth method.
|
||||
create table auth_ldap_account_attribute_map (
|
||||
create_time wt_timestamp,
|
||||
ldap_method_id wt_public_id
|
||||
constraint auth_ldap_method_fkey
|
||||
references auth_ldap_method(public_id)
|
||||
on delete cascade
|
||||
on update cascade,
|
||||
from_attribute text not null
|
||||
constraint from_attribute_must_not_be_empty
|
||||
check(length(trim(from_attribute)) > 0)
|
||||
constraint from_attribute_must_be_less_than_1024_chars
|
||||
check(length(trim(from_attribute)) < 1024),
|
||||
to_attribute text not null
|
||||
constraint to_attribute_valid_values
|
||||
check (lower(to_attribute) in ('fullname', 'email')), -- intentionally case-sensitive matching
|
||||
primary key(ldap_method_id, to_attribute)
|
||||
);
|
||||
comment on table auth_ldap_account_attribute_map is
|
||||
'auth_ldap_account_attribute_map entries are the optional attribute maps from custom attributes to '
|
||||
'the standard attributes of sub, name and email. There can be 0 or more for each parent ldap auth method.';
|
||||
|
||||
create trigger default_create_time_column before insert on auth_ldap_account_attribute_map
|
||||
for each row execute procedure default_create_time();
|
||||
|
||||
create trigger immutable_columns before update on auth_ldap_account_attribute_map
|
||||
for each row execute procedure immutable_columns('ldap_method_id', 'from_attribute', 'to_attribute', 'create_time');
|
||||
|
||||
create table auth_ldap_account (
|
||||
public_id wt_public_id primary key,
|
||||
auth_method_id wt_public_id not null,
|
||||
-- NOTE(mgaffney): The scope_id type is not wt_scope_id because the domain
|
||||
-- check is executed before the insert trigger which retrieves the scope_id
|
||||
-- causing an insert to fail.
|
||||
scope_id text not null,
|
||||
name wt_name,
|
||||
description wt_description,
|
||||
create_time wt_timestamp,
|
||||
update_time wt_timestamp,
|
||||
version wt_version,
|
||||
login_name text not null
|
||||
constraint login_name_must_be_lowercase
|
||||
check(lower(trim(login_name)) = login_name)
|
||||
constraint login_name_must_not_be_empty
|
||||
check(length(trim(login_name)) > 0),
|
||||
email wt_email,
|
||||
full_name wt_full_name,
|
||||
dn text -- will be null until the first successful authentication
|
||||
constraint dn_must_not_be_empty
|
||||
check(length(trim(dn)) > 0),
|
||||
member_of_groups jsonb -- will be null until the first successful authentication
|
||||
constraint member_of_groups_must_not_be_empty
|
||||
check(length(trim(member_of_groups::text)) > 0),
|
||||
constraint auth_ldap_method_fkey
|
||||
foreign key (scope_id, auth_method_id)
|
||||
references auth_ldap_method (scope_id, public_id)
|
||||
on delete cascade
|
||||
on update cascade,
|
||||
constraint auth_account_fkey
|
||||
foreign key (scope_id, auth_method_id, public_id)
|
||||
references auth_account (scope_id, auth_method_id, public_id)
|
||||
on delete cascade
|
||||
on update cascade,
|
||||
constraint auth_ldap_account_auth_method_id_name_uq
|
||||
unique(auth_method_id, name),
|
||||
constraint auth_ldap_account_auth_method_id_login_name_uq
|
||||
unique(auth_method_id, login_name),
|
||||
constraint auth_ldap_account_auth_method_id_dn_uq
|
||||
unique(auth_method_id, dn),
|
||||
constraint auth_ldap_account_auth_method_id_public_id_uq
|
||||
unique(auth_method_id, public_id)
|
||||
);
|
||||
comment on table auth_ldap_account is
|
||||
'auth_ldap_account entries are subtypes of auth_account and represent an ldap account.';
|
||||
|
||||
-- insert_auth_ldap_account_subtype is intended as a before insert
|
||||
-- trigger on auth_ldap_account. Its purpose is to insert a base
|
||||
-- auth_account for new ldap accounts. It's a bit different than the
|
||||
-- standard trigger for this, because it will have conflicting PKs
|
||||
-- and we just want to "do nothing" on those conflicts, deferring the
|
||||
-- raising on an error to insert into the auth_ldap_account table.
|
||||
-- this is all necessary because of we're using predictable public ids
|
||||
-- for ldap accounts.
|
||||
create or replace function insert_auth_ldap_account_subtype() returns trigger
|
||||
as $$
|
||||
begin
|
||||
select auth_method.scope_id
|
||||
into new.scope_id
|
||||
from auth_method
|
||||
where auth_method.public_id = new.auth_method_id;
|
||||
|
||||
insert into auth_account
|
||||
(public_id, auth_method_id, scope_id)
|
||||
values
|
||||
(new.public_id, new.auth_method_id, new.scope_id)
|
||||
on conflict do nothing;
|
||||
|
||||
return new;
|
||||
end;
|
||||
$$ language plpgsql;
|
||||
|
||||
create trigger insert_auth_ldap_account_subtype before insert on auth_ldap_account
|
||||
for each row execute procedure insert_auth_ldap_account_subtype();
|
||||
|
||||
create trigger delete_auth_account_subtype after delete on auth_ldap_account
|
||||
for each row execute procedure delete_auth_account_subtype();
|
||||
|
||||
create trigger update_time_column before update on auth_ldap_account
|
||||
for each row execute procedure update_time_column();
|
||||
|
||||
create trigger immutable_columns before update on auth_ldap_account
|
||||
for each row execute procedure immutable_columns('public_id', 'auth_method_id', 'scope_id', 'create_time', 'login_name');
|
||||
|
||||
create trigger default_create_time_column before insert on auth_ldap_account
|
||||
for each row execute procedure default_create_time();
|
||||
|
||||
create trigger update_version_column after update on auth_ldap_account
|
||||
for each row execute procedure update_version_column();
|
||||
|
||||
|
||||
insert into oplog_ticket (name, version)
|
||||
values
|
||||
('auth_ldap_method', 1), -- auth_ldap_method is the root aggregate itself and all of its value objects.
|
||||
('auth_ldap_account', 1),
|
||||
('auth_ldap_managed_group', 1);
|
||||
|
||||
|
||||
-- ldap_auth_method_with_value_obj is useful for reading an ldap auth method
|
||||
-- with its associated value objects (urls, certs, search config, etc). The use
|
||||
-- of the postgres string_agg(...) to aggregate the url and cert value objects
|
||||
-- into a column works because we are only pulling in one column from the
|
||||
-- associated tables and that value is part of the primary key and unique. This
|
||||
-- view will make things like recursive listing of ldap auth methods fairly
|
||||
-- straightforward to implement for the ldap repo. The view also includes an
|
||||
-- is_primary_auth_method bool
|
||||
create view ldap_auth_method_with_value_obj as
|
||||
select
|
||||
case when s.primary_auth_method_id is not null then
|
||||
true
|
||||
else false end
|
||||
as is_primary_auth_method,
|
||||
am.public_id,
|
||||
am.scope_id,
|
||||
am.name,
|
||||
am.description,
|
||||
am.create_time,
|
||||
am.update_time,
|
||||
am.version,
|
||||
am.state,
|
||||
am.start_tls,
|
||||
am.insecure_tls,
|
||||
am.discover_dn,
|
||||
am.anon_group_search,
|
||||
am.upn_domain,
|
||||
am.enable_groups,
|
||||
am.use_token_groups,
|
||||
-- the string_agg(..) column will be null if there are no associated value objects
|
||||
string_agg(distinct url.url, '|') as urls,
|
||||
string_agg(distinct cert.certificate, '|') as certs,
|
||||
string_agg(distinct concat_ws('=', aam.from_attribute, aam.to_attribute), '|') as account_attribute_map,
|
||||
|
||||
-- the rest of the fields are zero to one relationships that are stored in
|
||||
-- related tables. Since we're outer joining with these tables, we need to
|
||||
-- either add them to the group by, use an aggregating func, or handle
|
||||
-- multiple rows returning for each auth method. I've chosen to just use
|
||||
-- string_agg(...)
|
||||
string_agg(distinct uc.user_dn, '|') as user_dn,
|
||||
string_agg(distinct uc.user_attr, '|') as user_attr,
|
||||
string_agg(distinct uc.user_filter, '|') as user_filter,
|
||||
string_agg(distinct gc.group_dn, '|') as group_dn,
|
||||
string_agg(distinct gc.group_attr, '|') as group_attr,
|
||||
string_agg(distinct gc.group_filter, '|') as group_filter,
|
||||
string_agg(distinct cc.certificate_key, '|') as client_certificate_key,
|
||||
string_agg(distinct cc.certificate_key_hmac, '|') as client_certificate_key_hmac,
|
||||
string_agg(distinct cc.key_id, '|') as client_certificate_key_id,
|
||||
string_agg(distinct cc.certificate, '|') as client_certificate_cert,
|
||||
string_agg(distinct bc.dn, '|') as bind_dn,
|
||||
string_agg(distinct bc.password, '|') as bind_password,
|
||||
string_agg(distinct bc.password_hmac, '|') as bind_password_hmac,
|
||||
string_agg(distinct bc.key_id, '|') as bind_password_key_id
|
||||
from
|
||||
auth_ldap_method am
|
||||
left outer join iam_scope s on am.public_id = s.primary_auth_method_id
|
||||
left outer join auth_ldap_url url on am.public_id = url.ldap_method_id
|
||||
left outer join auth_ldap_certificate cert on am.public_id = cert.ldap_method_id
|
||||
left outer join auth_ldap_account_attribute_map aam on am.public_id = aam.ldap_method_id
|
||||
left outer join auth_ldap_user_entry_search uc on am.public_id = uc.ldap_method_id
|
||||
left outer join auth_ldap_group_entry_search gc on am.public_id = gc.ldap_method_id
|
||||
left outer join auth_ldap_client_certificate cc on am.public_id = cc.ldap_method_id
|
||||
left outer join auth_ldap_bind_credential bc on am.public_id = bc.ldap_method_id
|
||||
group by am.public_id, is_primary_auth_method; -- there can be only one public_id + is_primary_auth_method, so group by isn't a problem.
|
||||
comment on view ldap_auth_method_with_value_obj is
|
||||
'ldap auth method with its associated value objects (urls, certs, search config, etc)';
|
||||
|
||||
create table auth_ldap_managed_group (
|
||||
public_id wt_public_id primary key,
|
||||
auth_method_id wt_public_id not null,
|
||||
name wt_name,
|
||||
description wt_description,
|
||||
create_time wt_timestamp,
|
||||
update_time wt_timestamp,
|
||||
version wt_version,
|
||||
group_names jsonb not null
|
||||
constraint group_names_must_not_be_empty
|
||||
check(length(trim(group_names::text)) > 0),
|
||||
constraint auth_ldap_method_fkey
|
||||
foreign key (auth_method_id) -- fk1
|
||||
references auth_ldap_method (public_id)
|
||||
on delete cascade
|
||||
on update cascade,
|
||||
-- Ensure it relates to an abstract managed group
|
||||
constraint auth_managed_group_fkey
|
||||
foreign key (auth_method_id, public_id) -- fk2
|
||||
references auth_managed_group (auth_method_id, public_id)
|
||||
on delete cascade
|
||||
on update cascade,
|
||||
constraint auth_ldap_managed_group_auth_method_id_name_uq
|
||||
unique(auth_method_id, name)
|
||||
);
|
||||
comment on table auth_ldap_managed_group is
|
||||
'auth_ldap_managed_group entries are subtypes of auth_managed_group and represent an ldap managed group.';
|
||||
|
||||
-- Define the immutable fields of auth_ldap_managed_group
|
||||
create trigger immutable_columns before update on auth_ldap_managed_group
|
||||
for each row execute procedure immutable_columns('public_id', 'auth_method_id', 'create_time');
|
||||
|
||||
-- Populate create time on insert
|
||||
create trigger default_create_time_column before insert on auth_ldap_managed_group
|
||||
for each row execute procedure default_create_time();
|
||||
|
||||
-- Generate update time on update
|
||||
create trigger update_time_column before update on auth_ldap_managed_group
|
||||
for each row execute procedure update_time_column();
|
||||
|
||||
-- Update version when something changes
|
||||
create trigger update_version_column after update on auth_ldap_managed_group
|
||||
for each row execute procedure update_version_column();
|
||||
|
||||
-- Add into the base table when inserting into the concrete table
|
||||
create trigger insert_managed_group_subtype before insert on auth_ldap_managed_group
|
||||
for each row execute procedure insert_managed_group_subtype();
|
||||
|
||||
-- Ensure that deletions in the ldap subtype result in deletions to the base
|
||||
-- table.
|
||||
create trigger delete_managed_group_subtype after delete on auth_ldap_managed_group
|
||||
for each row execute procedure delete_managed_group_subtype();
|
||||
|
||||
|
||||
-- auth_ldap_managed_group_member_account uses CTEs to expand and "normalize" the jsonb column
|
||||
-- containing groups in both the accounts and managed groups, then it joins these
|
||||
-- "normalized" expressions into a join table of mangagd group member account entries
|
||||
create view auth_ldap_managed_group_member_account as
|
||||
with
|
||||
account(id, group_name) as (
|
||||
select
|
||||
a.public_id, ag.group_name
|
||||
from
|
||||
auth_ldap_account a
|
||||
left join jsonb_array_elements(a.member_of_groups) as ag(group_name) on true
|
||||
),
|
||||
groups (create_time, id, group_name) as (
|
||||
select
|
||||
g.create_time,
|
||||
g.public_id,
|
||||
mg.group_name
|
||||
from
|
||||
auth_ldap_managed_group g
|
||||
left join jsonb_array_elements(g.group_names) as mg(group_name) on true
|
||||
)
|
||||
select distinct
|
||||
groups.create_time,
|
||||
account.id as member_id,
|
||||
groups.id as managed_group_id
|
||||
from account, groups
|
||||
where account.group_name = groups.group_name;
|
||||
comment on view auth_ldap_managed_group_member_account is
|
||||
'auth_ldap_managed_group_member_account is the join view for '
|
||||
'managed ldap groups and accounts';
|
||||
|
||||
|
||||
-- recreate view defined in postgres/9/03_oidc_managed_group_member.up.sql
|
||||
-- so the new view can include both oidc and ldap managed groups
|
||||
drop view auth_managed_group_member_account;
|
||||
|
||||
-- create view with both oidc and ldap managed groups; we can replace this view
|
||||
-- to union with other subtype tables as needed in the future.
|
||||
create view auth_managed_group_member_account as
|
||||
select
|
||||
oidc.create_time,
|
||||
oidc.managed_group_id,
|
||||
oidc.member_id
|
||||
from
|
||||
auth_oidc_managed_group_member_account oidc
|
||||
union
|
||||
select
|
||||
ldap.create_time,
|
||||
ldap.managed_group_id,
|
||||
ldap.member_id
|
||||
from
|
||||
auth_ldap_managed_group_member_account ldap;
|
||||
comment on view auth_managed_group_member_account is
|
||||
'';
|
||||
|
||||
commit;
|
||||
Loaded 100 of 121 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user