mongodb/mongo-ruby-kerberos

View on GitHub
ext/mongo_kerberos/mongo_kerberos_native.c

Summary

Maintainability
Test Coverage
// Copyright (C) 2014-2020 MongoDB, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include <ruby.h>
#include <sasl/sasl.h>
#include <sasl/saslutil.h>

static VALUE gssapi_error_cls() {
  VALUE mongo = rb_const_get(rb_cObject, rb_intern("Mongo"));
  VALUE gssapi = rb_const_get(mongo, rb_intern("GssapiNative"));
  return rb_const_get(gssapi, rb_intern("Error"));
}

static void raise_gssapi_error(const char *msg, int result) {
  rb_raise(gssapi_error_cls(), "%s (code %d: %s)", msg, result, sasl_errstring(result, NULL, NULL));
}

static void mongo_sasl_conn_free(void* data) {
  sasl_conn_t *conn = (sasl_conn_t*) data;
  if (conn) {
    sasl_dispose(&conn);
    /* We do not set connection to NULL in the Ruby object. */
    /* This is probably fine because this method is supposed to be called */
    /* when the Ruby object is being garbage collected. */
    /* Plus, we don't have the Ruby object reference here to do anything */
    /* with it. */
  }
}

static sasl_conn_t* mongo_sasl_context(VALUE self) {
  sasl_conn_t* conn;
  VALUE context;
  context = rb_iv_get(self, "@context");
  Data_Get_Struct(context, sasl_conn_t, conn);
  return conn;
}

static VALUE a_init(VALUE self, VALUE user_name, VALUE host_name, VALUE service_name, VALUE canonicalize_host_name)
{
  rb_iv_set(self, "@valid", Qtrue);
  rb_iv_set(self, "@user_name", user_name);
  rb_iv_set(self, "@host_name", host_name);
  rb_iv_set(self, "@service_name", service_name);
  rb_iv_set(self, "@canonicalize_host_name", canonicalize_host_name);

  return self;
}

static VALUE valid(VALUE self) {
  return rb_iv_get(self, "@valid");
}

int is_sasl_failure(int result)
{
  if (result < 0) {
    return 1;
  }

  return 0;
}

static int sasl_interact(VALUE self, int id, const char **result, unsigned *len) {
  switch (id) {
    case SASL_CB_AUTHNAME:
    case SASL_CB_USER:
    {
      VALUE user_name;
      user_name = rb_iv_get(self, "@user_name");
      *result = RSTRING_PTR(user_name);
      if (len) {
        *len = (int)RSTRING_LEN(user_name);
      }
      return SASL_OK;
    }
  }

  return SASL_FAIL;
}

static VALUE initialize_challenge(VALUE self) {
  int result;
  char encoded_payload[4096];
  const char *raw_payload;
  unsigned int raw_payload_len, encoded_payload_len;
  const char *mechanism_list = "GSSAPI";
  const char *mechanism_selected;
  VALUE context;
  sasl_conn_t *conn;
  sasl_callback_t client_interact [] = {
    { SASL_CB_AUTHNAME, (int (*)(void))sasl_interact, (void*)self },
    { SASL_CB_USER, (int (*)(void))sasl_interact, (void*)self },
    { SASL_CB_LIST_END, NULL, NULL }
  };

  const char *servicename = RSTRING_PTR(rb_iv_get(self, "@service_name"));
  const char *hostname = RSTRING_PTR(rb_iv_get(self, "@host_name"));

  result = sasl_client_new(servicename, hostname, NULL, NULL, client_interact, 0, &conn);

  if (result != SASL_OK) {
    raise_gssapi_error("sasl_client_new failed", result);
  }

  context = Data_Wrap_Struct(rb_cObject, NULL, mongo_sasl_conn_free, conn);
  /* I'm guessing ruby raises on out of memory condition rather than */
  /* returns NULL, hence no error checking is needed here? */
  
  /* from now on context owns conn */
  /* since mongo_sasl_conn_free cleans up conn, we should NOT call */
  /* sasl_dispose any more in this function. */
  rb_iv_set(self, "@context", context);
  RB_GC_GUARD(context);

  result = sasl_client_start(conn, mechanism_list, NULL, &raw_payload, &raw_payload_len, &mechanism_selected);
  if (is_sasl_failure(result)) {
    raise_gssapi_error("sasl_client_start failed", result);
  }
  if (strcmp(mechanism_selected, "GSSAPI") != 0) {
    rb_raise(gssapi_error_cls(), "sasl_client_start selected an unexpected mechanism: %s", mechanism_selected);
  }

  if (result != SASL_CONTINUE) {
    raise_gssapi_error("sasl_client_start did not return SASL_CONTINUE", result);
  }

  /* cyrus-sasl considers `outmax` (fourth argument) to include the null */
  /* terminator, but this is not documented. Be defensive and exclude it. */
  result = sasl_encode64(raw_payload, raw_payload_len, encoded_payload, sizeof(encoded_payload)-1, &encoded_payload_len);
  if (is_sasl_failure(result)) {
    raise_gssapi_error("sasl_encode64 failed to encode the payload", result);
  }
  if (encoded_payload_len >= sizeof(encoded_payload)) {
    /* Should never happen */
    rb_raise(gssapi_error_cls(), "sasl_encode64 claimed to write %u bytes when up to %lu bytes were allowed", encoded_payload_len, sizeof(encoded_payload));
  }

  encoded_payload[encoded_payload_len] = 0;
  return rb_str_new(encoded_payload, encoded_payload_len);
}

static VALUE evaluate_challenge(VALUE self, VALUE rb_payload) {
  char base_payload[4096], payload[4096];
  const char *step_payload, *out;
  unsigned int step_payload_len, payload_len, base_payload_len, outlen;
  int result;
  sasl_conn_t *conn = mongo_sasl_context(self);

  StringValue(rb_payload);
  step_payload = RSTRING_PTR(rb_payload);
  step_payload_len = (int)RSTRING_LEN(rb_payload);

  result = sasl_decode64(step_payload, step_payload_len, base_payload, sizeof(base_payload)-1, &base_payload_len);
  if (is_sasl_failure(result)) {
    raise_gssapi_error("sasl_decode64 failed to decode the payload", result);
  }

  result = sasl_client_step(conn, base_payload, base_payload_len, NULL, &out, &outlen);
  if (is_sasl_failure(result)) {
    raise_gssapi_error("sasl_client_step failed", result);
  }

  result = sasl_encode64(out, outlen, payload, sizeof(payload)-1, &payload_len);
  if (is_sasl_failure(result)) {
    raise_gssapi_error("sasl_encode64 failed to encode the payload", result);
  }

  return rb_str_new(payload, payload_len);
}

VALUE c_GSSAPI_authenticator;

void Init_mongo_kerberos_native() {
  VALUE mongo, auth;
  int result;
  
  result = sasl_client_init(NULL);
  if (result != SASL_OK) {
    rb_raise(rb_eLoadError, "Failed to initialize libsasl2: sasl_client_init failed (code %d: %s)", result, sasl_errstring(result, NULL, NULL));
  }
  
  mongo = rb_const_get(rb_cObject, rb_intern("Mongo"));
  auth = rb_const_get(mongo, rb_intern("GssapiNative"));
  c_GSSAPI_authenticator = rb_define_class_under(auth, "Authenticator", rb_cObject);
  rb_define_method(c_GSSAPI_authenticator, "initialize", a_init, 4);
  rb_define_method(c_GSSAPI_authenticator, "initialize_challenge", initialize_challenge, 0);
  rb_define_method(c_GSSAPI_authenticator, "evaluate_challenge", evaluate_challenge, 1);
  
  /* Deprecated */
  rb_define_method(rb_cObject, "valid?", valid, 0);
}