github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/pkg/ccl/gssapiccl/gssapi.go (about)

     1  // Copyright 2018 The Cockroach Authors.
     2  //
     3  // Licensed as a CockroachDB Enterprise file under the Cockroach Community
     4  // License (the "License"); you may not use this file except in compliance with
     5  // the License. You may obtain a copy of the License at
     6  //
     7  //     https://github.com/cockroachdb/cockroach/blob/master/licenses/CCL.txt
     8  
     9  // We use a non-standard build tag here because we want to only build on
    10  // linux-gnu targets (i.e., not musl). Since go doesn't have a builtin way
    11  // to do that, we have to set this in the top-level Makefile.
    12  
    13  // +build gss
    14  
    15  package gssapiccl
    16  
    17  import (
    18  	"context"
    19  	"crypto/tls"
    20  	"strings"
    21  	"unsafe"
    22  
    23  	"github.com/cockroachdb/cockroach/pkg/ccl/utilccl"
    24  	"github.com/cockroachdb/cockroach/pkg/clusterversion"
    25  	"github.com/cockroachdb/cockroach/pkg/security"
    26  	"github.com/cockroachdb/cockroach/pkg/sql"
    27  	"github.com/cockroachdb/cockroach/pkg/sql/pgwire"
    28  	"github.com/cockroachdb/cockroach/pkg/sql/pgwire/hba"
    29  	"github.com/cockroachdb/errors"
    30  )
    31  
    32  // #cgo LDFLAGS: -lgssapi_krb5 -lcom_err -lkrb5 -lkrb5support -ldl -lk5crypto -lresolv
    33  //
    34  // #include <gssapi/gssapi.h>
    35  // #include <stdlib.h>
    36  import "C"
    37  
    38  const (
    39  	authTypeGSS         int32 = 7
    40  	authTypeGSSContinue int32 = 8
    41  )
    42  
    43  // authGSS performs GSS authentication. See:
    44  // https://github.com/postgres/postgres/blob/0f9cdd7dca694d487ab663d463b308919f591c02/src/backend/libpq/auth.c#L1090
    45  func authGSS(
    46  	ctx context.Context,
    47  	c pgwire.AuthConn,
    48  	tlsState tls.ConnectionState,
    49  	_ pgwire.PasswordRetrievalFn,
    50  	_ pgwire.PasswordValidUntilFn,
    51  	execCfg *sql.ExecutorConfig,
    52  	entry *hba.Entry,
    53  ) (security.UserAuthHook, error) {
    54  	return func(requestedUser string, clientConnection bool) (func(), error) {
    55  		var (
    56  			majStat, minStat, lminS, gflags C.OM_uint32
    57  			gbuf                            C.gss_buffer_desc
    58  			contextHandle                   C.gss_ctx_id_t  = C.GSS_C_NO_CONTEXT
    59  			acceptorCredHandle              C.gss_cred_id_t = C.GSS_C_NO_CREDENTIAL
    60  			srcName                         C.gss_name_t
    61  			outputToken                     C.gss_buffer_desc
    62  
    63  			token []byte
    64  			err   error
    65  		)
    66  
    67  		if err = c.SendAuthRequest(authTypeGSS, nil); err != nil {
    68  			return nil, err
    69  		}
    70  
    71  		// This cleanup function must be called at the
    72  		// "completion of a communications session", not
    73  		// merely at the end of an authentication init. See
    74  		// https://tools.ietf.org/html/rfc2744.html, section
    75  		// `1. Introduction`, stage `d`:
    76  		//
    77  		//   At the completion of a communications session (which
    78  		//   may extend across several transport connections),
    79  		//   each application calls a GSS-API routine to delete
    80  		//   the security context.
    81  		//
    82  		// See https://github.com/postgres/postgres/blob/f4d59369d2ddf0ad7850112752ec42fd115825d4/src/backend/libpq/pqcomm.c#L269
    83  		connClose := func() {
    84  			C.gss_delete_sec_context(&lminS, &contextHandle, C.GSS_C_NO_BUFFER)
    85  		}
    86  
    87  		for {
    88  			token, err = c.GetPwdData()
    89  			if err != nil {
    90  				return connClose, err
    91  			}
    92  
    93  			gbuf.length = C.ulong(len(token))
    94  			gbuf.value = C.CBytes([]byte(token))
    95  
    96  			majStat = C.gss_accept_sec_context(
    97  				&minStat,
    98  				&contextHandle,
    99  				acceptorCredHandle,
   100  				&gbuf,
   101  				C.GSS_C_NO_CHANNEL_BINDINGS,
   102  				&srcName,
   103  				nil,
   104  				&outputToken,
   105  				&gflags,
   106  				nil,
   107  				nil,
   108  			)
   109  			C.free(unsafe.Pointer(gbuf.value))
   110  
   111  			if outputToken.length != 0 {
   112  				outputBytes := C.GoBytes(outputToken.value, C.int(outputToken.length))
   113  				C.gss_release_buffer(&lminS, &outputToken)
   114  				if err = c.SendAuthRequest(authTypeGSSContinue, outputBytes); err != nil {
   115  					return connClose, err
   116  				}
   117  			}
   118  			if majStat != C.GSS_S_COMPLETE && majStat != C.GSS_S_CONTINUE_NEEDED {
   119  				return connClose, gssError("accepting GSS security context failed", majStat, minStat)
   120  			}
   121  			if majStat != C.GSS_S_CONTINUE_NEEDED {
   122  				break
   123  			}
   124  		}
   125  
   126  		majStat = C.gss_display_name(&minStat, srcName, &gbuf, nil)
   127  		if majStat != C.GSS_S_COMPLETE {
   128  			return connClose, gssError("retrieving GSS user name failed", majStat, minStat)
   129  		}
   130  		gssUser := C.GoStringN((*C.char)(gbuf.value), C.int(gbuf.length))
   131  		C.gss_release_buffer(&lminS, &gbuf)
   132  
   133  		realms := entry.GetOptions("krb_realm")
   134  
   135  		if idx := strings.IndexByte(gssUser, '@'); idx >= 0 {
   136  			if len(realms) > 0 {
   137  				realm := gssUser[idx+1:]
   138  				matched := false
   139  				for _, krbRealm := range realms {
   140  					if realm == krbRealm {
   141  						matched = true
   142  						break
   143  					}
   144  				}
   145  				if !matched {
   146  					return connClose, errors.Errorf("GSSAPI realm (%s) didn't match any configured realm", realm)
   147  				}
   148  			}
   149  			if entry.GetOption("include_realm") != "1" {
   150  				gssUser = gssUser[:idx]
   151  			}
   152  		} else if len(realms) > 0 {
   153  			return connClose, errors.New("GSSAPI did not return realm but realm matching was requested")
   154  		}
   155  
   156  		if !strings.EqualFold(gssUser, requestedUser) {
   157  			return connClose, errors.Errorf("requested user is %s, but GSSAPI auth is for %s", requestedUser, gssUser)
   158  		}
   159  
   160  		// Do the license check last so that administrators are able to test whether
   161  		// their GSS configuration is correct. That is, the presence of this error
   162  		// message means they have a correctly functioning GSS/Kerberos setup,
   163  		// but now need to enable enterprise features.
   164  		return connClose, utilccl.CheckEnterpriseEnabled(execCfg.Settings, execCfg.ClusterID(), execCfg.Organization(), "GSS authentication")
   165  	}, nil
   166  }
   167  
   168  func gssError(msg string, majStat, minStat C.OM_uint32) error {
   169  	var (
   170  		gmsg          C.gss_buffer_desc
   171  		lminS, msgCtx C.OM_uint32
   172  	)
   173  
   174  	msgCtx = 0
   175  	C.gss_display_status(&lminS, majStat, C.GSS_C_GSS_CODE, C.GSS_C_NO_OID, &msgCtx, &gmsg)
   176  	msgMajor := C.GoString((*C.char)(gmsg.value))
   177  	C.gss_release_buffer(&lminS, &gmsg)
   178  
   179  	msgCtx = 0
   180  	C.gss_display_status(&lminS, minStat, C.GSS_C_MECH_CODE, C.GSS_C_NO_OID, &msgCtx, &gmsg)
   181  	msgMinor := C.GoString((*C.char)(gmsg.value))
   182  	C.gss_release_buffer(&lminS, &gmsg)
   183  
   184  	return errors.Errorf("%s: %s: %s", msg, msgMajor, msgMinor)
   185  }
   186  
   187  func checkEntry(entry hba.Entry) error {
   188  	hasInclude0 := false
   189  	for _, op := range entry.Options {
   190  		switch op[0] {
   191  		case "include_realm":
   192  			if op[1] == "0" {
   193  				hasInclude0 = true
   194  			} else {
   195  				return errors.Errorf("include_realm must be set to 0: %s", op[1])
   196  			}
   197  		case "krb_realm":
   198  		default:
   199  			return errors.Errorf("unsupported option %s", op[0])
   200  		}
   201  	}
   202  	if !hasInclude0 {
   203  		return errors.New(`missing "include_realm=0" option in GSS entry`)
   204  	}
   205  	return nil
   206  }
   207  
   208  func init() {
   209  	pgwire.RegisterAuthMethod("gss", authGSS, clusterversion.Version19_1, hba.ConnHostSSL, checkEntry)
   210  }