stupidpupil/kerberos_authenticator

View on GitHub
lib/kerberos_authenticator/krb5/keytab.rb

Summary

Maintainability
A
25 mins
Test Coverage
module KerberosAuthenticator
  module Krb5
    typedef :pointer, :krb5_keytab

    attach_function :krb5_kt_resolve, [:krb5_context, :string, :pointer], :krb5_error_code
    attach_function :krb5_kt_default, [:krb5_context, :pointer], :krb5_error_code

    attach_function :krb5_kt_close, [:krb5_context, :krb5_keytab], :krb5_error_code

    begin
      # Heimdal
      attach_function :krb5_kt_get_full_name, [:krb5_context, :krb5_keytab, :pointer], :krb5_error_code
    rescue FFI::NotFoundError
      # MIT
      attach_function :krb5_kt_get_name, [:krb5_context, :krb5_keytab, :buffer_out, :int], :krb5_error_code
    end

    begin
      attach_function :krb5_kt_have_content, [:krb5_context, :krb5_keytab], :krb5_error_code
    rescue FFI::NotFoundError
      # REVIEW: Then we're probably using an old version of the library that doesn't support kt_have_content.
    end

    # Storage for locally-stored keys.
    class Keytab
      # @!attribute [r] ptr
      #   @return [FFI::Pointer] the pointer to the wrapped krb5_keytab struct


      attr_reader :ptr

      # Resolves a keytab identified by name.
      # The keytab is not opened and may not be accessible or contain any entries. (Use {#has_content?} to check.)
      # @param name [String] a name of the form 'type:residual', where usually type is 'FILE' and residual the path to that file
      # @raise [Error] if the type is unknown
      # @return [Keytab] a resolved, but not opened, keytab
      # @see http://web.mit.edu/Kerberos/krb5-1.14/doc/appdev/refs/api/krb5_kt_resolve.html krb5_kt_resolve
      def self.new_with_name(name)
        pointer = FFI::MemoryPointer.new :pointer
        Krb5.kt_resolve(Context.context.ptr, name, pointer)

        new(pointer)
      end

      # Resolves the default keytab, usually the file at `/etc/krb5.keytab`.
      # The keytab is not opened and may not be accessible or contain any entries. (Use {#has_content?} to check.)
      # @return [Keytab] the default keytab
      # @see http://web.mit.edu/Kerberos/krb5-1.14/doc/appdev/refs/api/krb5_kt_default.html krb5_kt_default
      def self.default
        pointer = FFI::MemoryPointer.new :pointer
        Krb5.kt_default(Context.context.ptr, pointer)

        new(pointer)
      end

      # Initializes a new Keytab with a pointer to a pointer to a krb5_keytab structure.
      # @param pointer [FFI::Buffer]
      # @return [Keytab]
      def initialize(pointer)
        @ptr = FFI::AutoPointer.new pointer.get_pointer(0), self.class.method(:release)

        self
      end

      # Checks if the underlying keytab file or other store exists and contains entries.
      # (When `krb5_kt_have_content` isn't provided by the Kerberos library, then only some very limited checks are performed.)
      # @return [TrueClass] if the keytab exists and contains entries
      # @raise [Error] if there is a problem finding entries in the keytab
      # @see http://web.mit.edu/Kerberos/krb5-1.14/doc/appdev/refs/api/krb5_kt_have_content.html krb5_kt_have_content
      def assert_has_content
        if defined?(Krb5.kt_have_content)
          Krb5.kt_have_content(Context.context.ptr, ptr)
        else # HACK
          if file?
            raise Error, "Could not read #{name}" if !FileTest.readable?(path)
            raise Error, "#{name} does not appear to be a MIT keytab file" if File.read(path).unpack('C').first != 5
          end
        end
        true
      end

      # @return [Boolean] whether the keytab exists and contains entries
      # @see #assert_has_content
      def has_content?
        assert_has_content
        true
      rescue Error
        false
      end

      # The maximum length, in bytes, that can be read by #name .
      # HACK: Any value here might not be long enough.
      # The value here is based on a maximum prefix/type length of 30
      # and a maximum residual/path length of 1024
      GET_NAME_MAX_LENGTH = 30 + 1024

      # The seperator between the type and the residual in a keytab's name
      FULL_NAME_DELIMITER = ':'.freeze

      # @return [String] the name of the key table
      # @see http://web.mit.edu/Kerberos/krb5-1.14/doc/appdev/refs/api/krb5_kt_get_name.html kt_get_name
      def name
        if defined?(Krb5.kt_get_full_name)
          pointer = FFI::MemoryPointer.new :pointer
          Krb5.kt_get_full_name(Context.context.ptr, ptr, pointer)
          pointer = pointer.read_pointer
          copy = String.new(pointer.read_string).force_encoding('UTF-8')
          Krb5.xfree(pointer)
          copy
        else
          buffer = FFI::Buffer.new :char, GET_NAME_MAX_LENGTH
          Krb5.kt_get_name(Context.context.ptr, ptr, buffer, GET_NAME_MAX_LENGTH)
          buffer.read_bytes(GET_NAME_MAX_LENGTH).force_encoding('UTF-8').rstrip
        end
      end

      # @return [String] the type of the key table
      def type
        name.split(FULL_NAME_DELIMITER, 2).first
      end

      # @return [String] the residual of the key table, which means different things depending on the type
      def residual
        name.split(FULL_NAME_DELIMITER, 2).last
      end

      # @return [Boolean] if the keytab has a type of 'FILE' or 'file'
      def file?
        type =~ /^FILE$/i
      end

      # @return [String, nil] the path to the keytab file if the keytab is a file, nil otherwise
      def path
        file? ? residual : nil
      end

      # Closes a Keytab
      # @api private
      # @see http://web.mit.edu/kerberos/krb5-1.14/doc/appdev/refs/api/krb5_kt_close.html krb5_kt_close
      def self.release(pointer)
        Krb5.kt_close(Context.context.ptr, pointer)
      end
    end
  end
end