-
Notifications
You must be signed in to change notification settings - Fork 534
RUBY-530 sasl gssapi support for MRI #459
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0577968
31bd049
1db171a
af23133
71c97b2
c86d810
d2167aa
ab9b6ec
1f4d91d
0e67ff8
7a59f76
10d647d
ce2c5d6
ec40832
13bcbaa
247f927
4486548
8296363
02ec836
90c027e
ac72aba
6c83ce8
058ff27
9bf6131
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,167 @@ | ||
| // Copyright (C) 2014 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 void mongo_sasl_conn_free(void* data) { | ||
| sasl_conn_t *conn = (sasl_conn_t*) data; | ||
| // Ideally we would use sasl_client_done() but that's only available as of cyrus sasl 2.1.25 | ||
| if(conn) sasl_done(); | ||
| } | ||
|
|
||
| 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) | ||
| { | ||
| if (sasl_client_init(NULL) == SASL_OK) { | ||
| rb_iv_set(self, "@valid", Qtrue); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this normal in ruby to do these "if not ok - otherwise"? My brain strongly prefers if (true) {} else {/* failure branch_/ } over if (not true) { /_ failure branch */ } else { } So I personally would have done: |
||
| 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); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This one (canonicalize_host_name) seems unused?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, good eye = ) |
||
| } | ||
|
|
||
| else { | ||
| rb_iv_set(self, "@valid", Qfalse); | ||
| } | ||
|
|
||
| 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 = 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 = "GSSAPI"; | ||
| 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) { | ||
| sasl_dispose(&conn); | ||
| return Qfalse; | ||
| } | ||
|
|
||
| context = Data_Wrap_Struct(rb_cObject, NULL, mongo_sasl_conn_free, conn); | ||
| rb_iv_set(self, "@context", context); | ||
|
|
||
| result = sasl_client_start(conn, mechanism_list, NULL, &raw_payload, &raw_payload_len, &mechanism_selected); | ||
| if (is_sasl_failure(result)) { | ||
| return Qfalse; | ||
| } | ||
|
|
||
| if (result != SASL_CONTINUE) { | ||
| return Qfalse; | ||
| } | ||
|
|
||
| result = sasl_encode64(raw_payload, raw_payload_len, encoded_payload, sizeof(encoded_payload), &encoded_payload_len); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. result isn't checked for error... |
||
| if (is_sasl_failure(result)) { | ||
| return Qfalse; | ||
| } | ||
|
|
||
| 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 = RSTRING_LEN(rb_payload); | ||
|
|
||
| result = sasl_decode64(step_payload, step_payload_len, base_payload, sizeof(base_payload), &base_payload_len); | ||
| if (is_sasl_failure(result)) { | ||
| return Qfalse; | ||
| } | ||
|
|
||
| result = sasl_client_step(conn, base_payload, base_payload_len, NULL, &out, &outlen); | ||
| if (is_sasl_failure(result)) { | ||
| return Qfalse; | ||
| } | ||
|
|
||
| result = sasl_encode64(out, outlen, payload, sizeof(payload), &payload_len); | ||
| if (is_sasl_failure(result)) { | ||
| return Qfalse; | ||
| } | ||
|
|
||
| return rb_str_new(payload, payload_len); | ||
| } | ||
|
|
||
| VALUE c_GSSAPI_authenticator; | ||
|
|
||
| void Init_csasl() { | ||
| VALUE mongo, sasl; | ||
| mongo = rb_const_get(rb_cObject, rb_intern("Mongo")); | ||
| sasl = rb_const_get(mongo, rb_intern("Sasl")); | ||
| c_GSSAPI_authenticator = rb_define_class_under(sasl, "GSSAPIAuthenticator", rb_cObject); | ||
| rb_define_method(c_GSSAPI_authenticator, "initialize", a_init, 4); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm a little unsure how ruby land works... but a_init is only allowed to be called once per process (sasl_client_init()).
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hmm, ok. I've been able to create multiple authenticator objects (and thus call (a_init) / (sasl_client_init()) more than once per process when testing. Maybe it has only worked because the sasl context is identical between tests ? In other words, I'm always authenticating to the same instance with the same credentials. I need to find out about a cleanup hook for a Ruby C extension. I'd imagine there's a function that is called when garbage collecting the Ruby object from which I can call sasl_done()... thanks for pointing that out.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, the sasl state is cleaned up. The context is wrapped in a struct here and a function, mongo_sasl_conn_free is called when it is garbage collected: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looking at the implementation: http://git.cyrusimap.org/cyrus-sasl/tree/lib/client.c#n271 This may however be an implementation detail that can change in the future and not to be relied upon... unsure... The manpage doesn't explicitly cover this.. The norm for libs that need to init/cleanedup is to only call these once :]
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. makes sense. I'm going to put sasl_done() in the cleanup function (mongo_sasl_conn_free) since it will be called when the Ruby object is gc-ed. This seems to make sense since it would parallel the initialization of the Ruby object, which calls sasl_client_init()
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, sasl_done() cleans up the conn even though it's not passed in, right? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You have to be careful with sasl_dispose(). Today we do not use SASL to negotiate a crypto connection so you can call it right after authenticating or to cleanup a failed authentication. Note that sasl_client_done() (the recommended way) was introduced in 2.1.25, while sasl_done() has been available forever. See also: https://cyrusimap.org/docs/cyrus-sasl/2.1.25/programming.php
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wow, that is a good resource. They say sasl_client_done() is recommended but don't say why. |
||
| rb_define_method(c_GSSAPI_authenticator, "initialize_challenge", initialize_challenge, 0); | ||
| rb_define_method(c_GSSAPI_authenticator, "evaluate_challenge", evaluate_challenge, 1); | ||
| rb_define_method(rb_cObject, "valid?", valid, 0); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| require 'mkmf' | ||
| find_header('sasl/sasl.h') | ||
| have_library('sasl2', 'sasl_version') | ||
|
|
||
| create_makefile('csasl/csasl') |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| # Copyright (C) 2009-2014 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. | ||
|
|
||
| module Mongo | ||
| module Sasl | ||
|
|
||
| module GSSAPI | ||
|
|
||
| def self.authenticate(username, client, socket, opts={}) | ||
| raise LoadError, | ||
| "The Sasl GSSAPI authentication mechanism cannot be used because " + | ||
| "its extension did not load properly" unless Mongo::HAS_SASL | ||
|
|
||
| db = client.db('$external') | ||
| hostname = socket.pool.host | ||
| servicename = opts[:gssapi_service_name] || 'mongodb' | ||
| canonicalize = opts[:canonicalize_host_name] ? opts[:canonicalize_host_name] : false | ||
| authenticator = Mongo::Sasl::GSSAPIAuthenticator.new(username, hostname, servicename, canonicalize) | ||
|
|
||
| return { } unless authenticator.valid? | ||
|
|
||
| token = authenticator.initialize_challenge | ||
| cmd = BSON::OrderedHash['saslStart', 1, 'mechanism', 'GSSAPI', 'payload', token, 'autoAuthorize', 1] | ||
| response = db.command(cmd, :check_response => false, :socket => socket) | ||
|
|
||
| until response['done'] do | ||
| break unless Support.ok?(response) | ||
| token = authenticator.evaluate_challenge(response['payload']) | ||
| cmd = BSON::OrderedHash['saslContinue', 1, 'conversationId', response['conversationId'], 'payload', token] | ||
| response = db.command(cmd, :check_response => false, :socket => socket) | ||
| end | ||
| response | ||
| end | ||
| end | ||
| end | ||
| end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this file is missing a license header