AuthorizationEngine.cpp [plain text]
#include "AuthorizationEngine.h"
#include "server.h"
#include "authority.h"
#include <Security/AuthorizationTags.h>
#include <Security/logging.h>
#include <Security/debugging.h>
#include <CoreFoundation/CFData.h>
#include <CoreFoundation/CFNumber.h>
#include <CoreFoundation/CFPropertyList.h>
#include <errno.h>
#include <fcntl.h>
#include <float.h>
#include <unistd.h>
#include <grp.h>
#include <pwd.h>
#include <netinfo/ni.h>
extern "C" {
int lookupd_query(ni_proplist *l, ni_proplist ***out);
ni_proplist *lookupd_make_query(char *cat, char *fmt, ...);
int _lu_running(void);
}
using namespace Authorization;
Error::Error(int err) : error(err)
{
}
const char *Error::what() const
{ return "Authorization error"; }
CSSM_RETURN Error::cssmError() const
{ return error; }
OSStatus Error::osStatus() const
{ return error; }
void Error::throwMe(int err) { throw Error(err); }
CredentialImpl::CredentialImpl(const string &username, const uid_t uid, const gid_t gid, bool shared) :
mUsername(username), mShared(shared), mUid(uid), mGid(gid), mCreationTime(CFAbsoluteTimeGetCurrent()), mValid(true)
{
}
CredentialImpl::CredentialImpl(const string &username, const string &password, bool shared) :
mShared(shared), mCreationTime(CFAbsoluteTimeGetCurrent()), mValid(false)
{
const char *user = username.c_str();
struct passwd *pw = getpwnam(user);
do
{
if ( !pw && _lu_running() ) {
ni_proplist **out = NULL;
ni_proplist *in = lookupd_make_query("user", "kv", "realname", user);
if (!in) break;
int results = lookupd_query(in, &out);
ni_proplist_free(in);
if (!out) break;
for (int i=0; i<results; ++i) {
ni_proplist *nipl = out[i];
for (unsigned int j=0; !pw && j< nipl->ni_proplist_len; j++) {
if ( !strcmp(nipl->ni_proplist_val[j].nip_name, "name") &&
(nipl->ni_proplist_val[j].nip_val.ni_namelist_len > 0) )
pw = getpwnam( *(nipl->ni_proplist_val[j].nip_val.ni_namelist_val) );
}
ni_proplist_free(nipl);
}
free(out);
}
if (!pw)
{
debug("autheval", "user %s not found, creating invalid credential", user);
break;
}
if (pw->pw_passwd != NULL && pw->pw_passwd[0])
{
const char *passwd = password.c_str();
if (strcmp(crypt(passwd, pw->pw_passwd), pw->pw_passwd))
{
debug("autheval", "password for user %s is invalid, creating invalid credential", user);
break;
}
}
debug("autheval", "password for user %s is ok, creating%s credential",
user, mShared ? " shared" : "");
mUsername = string ( pw->pw_name );
mUid = pw->pw_uid;
mGid = pw->pw_gid;
mValid = true;
}
while (0);
if (pw)
endpwent();
}
CredentialImpl::~CredentialImpl()
{
}
bool
CredentialImpl::operator < (const CredentialImpl &other) const
{
if (!mShared && other.mShared)
return true;
if (!other.mShared && mShared)
return false;
return mUsername < other.mUsername;
}
bool
CredentialImpl::isShared() const
{
return mShared;
}
void
CredentialImpl::merge(const CredentialImpl &other)
{
assert(mUsername == other.mUsername);
if (other.mValid && (!mValid || mCreationTime < other.mCreationTime))
{
mCreationTime = other.mCreationTime;
mUid = other.mUid;
mGid = other.mGid;
mValid = true;
}
}
CFAbsoluteTime
CredentialImpl::creationTime() const
{
return mCreationTime;
}
bool
CredentialImpl::isValid() const
{
return mValid;
}
void
CredentialImpl::invalidate()
{
mValid = false;
}
Credential::Credential() :
RefPointer<CredentialImpl>(NULL)
{
}
Credential::Credential(CredentialImpl *impl) :
RefPointer<CredentialImpl>(impl)
{
}
Credential::Credential(const string &username, const uid_t uid, const gid_t gid, bool shared) :
RefPointer<CredentialImpl>(new CredentialImpl(username, uid, gid, shared))
{
}
Credential::Credential(const string &username, const string &password, bool shared) :
RefPointer<CredentialImpl>(new CredentialImpl(username, password, shared))
{
}
Credential::~Credential()
{
}
bool
Credential::operator < (const Credential &other) const
{
if (!*this)
return other;
if (!other)
return false;
return (**this) < (*other);
}
Right &
Right::overlay(AuthorizationItem &item)
{
return static_cast<Right &>(item);
}
Right *
Right::overlay(AuthorizationItem *item)
{
return static_cast<Right *>(item);
}
Right::Right()
{
name = "";
valueLength = 0;
value = NULL;
flags = 0;
}
Right::Right(AuthorizationString inName, size_t inValueLength, const void *inValue)
{
name = inName;
valueLength = inValueLength;
value = const_cast<void *>(inValue);
}
Right::~Right()
{
}
bool
Right::operator < (const Right &other) const
{
return strcmp(name, other.name) < 0;
}
const AuthorizationRights RightSet::gEmptyRights = { 0, NULL };
RightSet::RightSet(const AuthorizationRights *rights) :
mRights(const_cast<AuthorizationRights *>(rights ? rights : &gEmptyRights))
{
}
RightSet::RightSet(const RightSet &other)
{
mRights = other.mRights;
}
RightSet::~RightSet()
{
}
RightSet::const_reference
RightSet::back() const
{
return static_cast<const_reference>(mRights->items[size() - 1]);
}
MutableRightSet::MutableRightSet(size_t count, const Right &element) :
mCapacity(count)
{
mRights = new AuthorizationRights();
mRights->items = reinterpret_cast<pointer>(malloc(sizeof(Right) * mCapacity));
if (!mRights->items)
{
delete mRights;
throw std::bad_alloc();
}
mRights->count = count;
for (size_type ix = 0; ix < count; ++ix)
mRights->items[ix] = element;
}
MutableRightSet::MutableRightSet(const RightSet &other)
{
size_type count = other.size();
mCapacity = count;
mRights = new AuthorizationRights();
mRights->items = reinterpret_cast<pointer>(malloc(sizeof(Right) * mCapacity));
if (!mRights->items)
{
delete mRights;
throw std::bad_alloc();
}
mRights->count = count;
for (size_type ix = 0; ix < count; ++ix)
mRights->items[ix] = other.mRights->items[ix];
}
MutableRightSet::~MutableRightSet()
{
free(mRights->items);
delete mRights;
}
MutableRightSet &
MutableRightSet::operator = (const RightSet &other)
{
size_type count = other.size();
if (capacity() < count)
grow(count);
mRights->count = count;
for (size_type ix = 0; ix < count; ++ix)
mRights->items[ix] = other.mRights->items[ix];
return *this;
}
void
MutableRightSet::swap(MutableRightSet &other)
{
AuthorizationRights *rights = mRights;
size_t capacity = mCapacity;
mRights = other.mRights;
mCapacity = other.mCapacity;
other.mRights = rights;
other.mCapacity = capacity;
}
MutableRightSet::reference
MutableRightSet::back()
{
return static_cast<reference>(mRights->items[size() - 1]);
}
void
MutableRightSet::push_back(const_reference right)
{
if (size() >= capacity())
grow(capacity() + 1);
mRights->items[mRights->count] = right;
mRights->count++;
}
void
MutableRightSet::pop_back()
{
if (!empty())
mRights->count--;
}
void
MutableRightSet::grow(size_type min_capacity)
{
size_type newCapacity = mCapacity * mCapacity;
if (newCapacity < min_capacity)
newCapacity = min_capacity;
void *newItems = realloc(mRights->items, sizeof(*mRights->items) * newCapacity);
if (!newItems)
throw std::bad_alloc();
mRights->items = reinterpret_cast<pointer>(newItems);
mCapacity = newCapacity;
}
CFStringRef Rule::kUserInGroupID = CFSTR("group");
CFStringRef Rule::kTimeoutID = CFSTR("timeout");
CFStringRef Rule::kSharedID = CFSTR("shared");
CFStringRef Rule::kAllowRootID = CFSTR("allow-root");
CFStringRef Rule::kDenyID = CFSTR("deny");
CFStringRef Rule::kAllowID = CFSTR("allow");
Rule::Rule() :
mType(kUserInGroup), mGroupName("admin"), mMaxCredentialAge(300.0), mShared(true), mAllowRoot(false)
{
}
Rule::Rule(CFTypeRef cfRule)
{
if (CFGetTypeID(cfRule) == CFStringGetTypeID())
{
CFStringRef tag = reinterpret_cast<CFStringRef>(cfRule);
if (CFEqual(kAllowID, tag))
{
debug("authrule", "rule always allow");
mType = kAllow;
}
else if (CFEqual(kDenyID, tag))
{
debug("authrule", "rule always deny");
mType = kDeny;
}
else
Error::throwMe();
}
else if (CFGetTypeID(cfRule) == CFDictionaryGetTypeID())
{
mType = kUserInGroup;
CFDictionaryRef dict = reinterpret_cast<CFDictionaryRef>(cfRule);
CFTypeRef groupTag = CFDictionaryGetValue(dict, kUserInGroupID);
if (!groupTag || CFGetTypeID(groupTag) != CFStringGetTypeID())
Error::throwMe();
CFStringRef group = reinterpret_cast<CFStringRef>(groupTag);
char buffer[512];
const char *ptr = CFStringGetCStringPtr(group, kCFStringEncodingUTF8);
if (ptr == NULL)
{
if (CFStringGetCString(group, buffer, 512, kCFStringEncodingUTF8))
ptr = buffer;
else
Error::throwMe();
}
mGroupName = string(ptr);
mMaxCredentialAge = DBL_MAX;
CFTypeRef timeoutTag = CFDictionaryGetValue(dict, kTimeoutID);
if (timeoutTag)
{
if (CFGetTypeID(timeoutTag) != CFNumberGetTypeID())
Error::throwMe();
CFNumberGetValue(reinterpret_cast<CFNumberRef>(timeoutTag), kCFNumberDoubleType, &mMaxCredentialAge);
}
CFTypeRef sharedTag = CFDictionaryGetValue(dict, kSharedID);
mShared = false;
if (sharedTag)
{
if (CFGetTypeID(sharedTag) != CFBooleanGetTypeID())
Error::throwMe();
mShared = CFBooleanGetValue(reinterpret_cast<CFBooleanRef>(sharedTag));
}
CFTypeRef allowRootTag = CFDictionaryGetValue(dict, kAllowRootID);
mAllowRoot = false;
if (allowRootTag)
{
if (CFGetTypeID(allowRootTag) != CFBooleanGetTypeID())
Error::throwMe();
mAllowRoot = CFBooleanGetValue(reinterpret_cast<CFBooleanRef>(allowRootTag));
}
debug("authrule", "rule user in group \"%s\" timeout %g%s%s",
mGroupName.c_str(), mMaxCredentialAge, mShared ? " shared" : "",
mAllowRoot ? " allow-root" : "");
}
}
Rule::Rule(const Rule &other) :
mType(other.mType),
mGroupName(other.mGroupName),
mMaxCredentialAge(other.mMaxCredentialAge),
mShared(other.mShared),
mAllowRoot(other.mAllowRoot)
{
}
Rule &
Rule::operator = (const Rule &other)
{
mType = other.mType;
mGroupName = other.mGroupName;
mMaxCredentialAge = other.mMaxCredentialAge;
mShared = other.mShared;
mAllowRoot = other.mAllowRoot;
return *this;
}
Rule::~Rule()
{
}
OSStatus
Rule::evaluate(const Right &inRight,
const AuthorizationEnvironment *environment, AuthorizationFlags flags,
CFAbsoluteTime now, const CredentialSet *inCredentials, CredentialSet &credentials,
const AuthorizationToken &auth)
{
switch (mType)
{
case kAllow:
debug("autheval", "rule is always allow");
return errAuthorizationSuccess;
case kDeny:
debug("autheval", "rule is always deny");
return errAuthorizationDenied;
case kUserInGroup:
debug("autheval", "rule is user in group");
break;
default:
Error::throwMe();
}
if (mAllowRoot && auth.creatorUid() == 0)
{
debug("autheval", "creator of authorization has uid == 0 granting right %s",
inRight.rightName());
return errAuthorizationSuccess;
}
for (CredentialSet::const_iterator it = credentials.begin(); it != credentials.end(); ++it)
{
OSStatus status = evaluate(inRight, environment, now, *it, true);
if (status != errAuthorizationDenied)
return status;
}
if (inCredentials)
{
for (CredentialSet::const_iterator it = inCredentials->begin(); it != inCredentials->end(); ++it)
{
OSStatus status = evaluate(inRight, environment, now, *it, false);
if (status == errAuthorizationSuccess)
{
credentials.insert(*it);
return status;
}
else if (status != errAuthorizationDenied)
return status;
}
}
if (!(flags & kAuthorizationFlagExtendRights))
return errAuthorizationDenied;
if (!(flags & kAuthorizationFlagInteractionAllowed))
return errAuthorizationInteractionNotAllowed;
QueryAuthorizeByGroup query;
string usernamehint;
uid_t uid = query.uid();
if (uid)
{
struct passwd *pw = getpwuid(uid);
if (pw != NULL)
{
if ( (pw->pw_passwd == NULL) ||
strcmp(pw->pw_passwd, "*") ) {
if (evaluate(inRight, environment, now, Credential(pw->pw_name, pw->pw_uid, pw->pw_gid, mShared), true) == errAuthorizationSuccess) {
usernamehint = string( pw->pw_gecos );
size_t comma = usernamehint.find(',');
if (comma)
usernamehint = usernamehint.substr(0, comma);
if (usernamehint.size() == 0)
usernamehint = string( pw->pw_name );
} } endpwent();
}
}
Credential newCredential;
SecurityAgent::Reason reason = SecurityAgent::userNotInGroup;
for (int tryCount = 0; tryCount < 3; ++tryCount)
{
OSStatus status = obtainCredential(query, inRight, environment, usernamehint.c_str(), newCredential, reason);
if (status)
return status;
if (!newCredential->isValid())
reason = SecurityAgent::invalidPassphrase;
else {
status = evaluate(inRight, environment, now, newCredential, true);
if (status == errAuthorizationSuccess)
{
credentials.insert(newCredential);
query.done();
return errAuthorizationSuccess;
}
else if (status != errAuthorizationDenied)
return status;
reason = SecurityAgent::userNotInGroup;
}
}
query.cancel(SecurityAgent::tooManyTries);
return errAuthorizationDenied;
}
OSStatus
Rule::evaluate(const Right &inRight, const AuthorizationEnvironment *environment, CFAbsoluteTime now,
const Credential &credential, bool ignoreShared)
{
assert(mType == kUserInGroup);
const char *user = credential->username().c_str();
if (!credential->isValid())
{
debug("autheval", "credential for user %s is invalid, denying right %s", user, inRight.rightName());
return errAuthorizationDenied;
}
if (now - credential->creationTime() > mMaxCredentialAge)
{
debug("autheval", "credential for user %s has expired, denying right %s", user, inRight.rightName());
return errAuthorizationDenied;
}
if (!ignoreShared && !mShared && credential->isShared())
{
debug("autheval", "shared credential for user %s cannot be used, denying right %s", user, inRight.rightName());
return errAuthorizationDenied;
}
if (credential->uid() == 0)
{
debug("autheval", "user %s has uid 0, granting right %s", user, inRight.rightName());
return errAuthorizationSuccess;
}
const char *groupname = mGroupName.c_str();
struct group *gr = getgrnam(groupname);
if (!gr)
return errAuthorizationDenied;
if (credential->gid() == gr->gr_gid)
{
debug("autheval", "user %s has group %s(%d) as default group, granting right %s",
user, groupname, gr->gr_gid, inRight.rightName());
endgrent();
return errAuthorizationSuccess;
}
for (char **group = gr->gr_mem; *group; ++group)
{
if (!strcmp(*group, user))
{
debug("autheval", "user %s is a member of group %s, granting right %s",
user, groupname, inRight.rightName());
endgrent();
return errAuthorizationSuccess;
}
}
debug("autheval", "user %s is not a member of group %s, denying right %s",
user, groupname, inRight.rightName());
endgrent();
return errAuthorizationDenied;
}
OSStatus
Rule::obtainCredential(QueryAuthorizeByGroup &query, const Right &inRight,
const AuthorizationEnvironment *environment, const char *usernameHint, Credential &outCredential, SecurityAgent::Reason reason)
{
char nameBuffer[SecurityAgent::maxUsernameLength];
char passphraseBuffer[SecurityAgent::maxPassphraseLength];
OSStatus status = errAuthorizationDenied;
try {
if (query(mGroupName.c_str(), usernameHint, nameBuffer, passphraseBuffer, reason))
status = noErr;
} catch (const CssmCommonError &err) {
status = err.osStatus();
} catch (...) {
status = errAuthorizationInternal;
}
if (status == CSSM_ERRCODE_USER_CANCELED)
{
debug("auth", "canceled obtaining credential for user in group %s", mGroupName.c_str());
return errAuthorizationCanceled;
}
if (status == CSSM_ERRCODE_NO_USER_INTERACTION)
{
debug("auth", "user interaction not possible obtaining credential for user in group %s", mGroupName.c_str());
return errAuthorizationInteractionNotAllowed;
}
if (status != noErr)
{
debug("auth", "failed obtaining credential for user in group %s", mGroupName.c_str());
return status;
}
debug("auth", "obtained credential for user %s", nameBuffer);
string username(nameBuffer);
string password(passphraseBuffer);
outCredential = Credential(username, password, mShared);
return errAuthorizationSuccess;
}
Engine::Engine(const char *configFile) :
mLastChecked(DBL_MIN)
{
mRulesFileName = new char[strlen(configFile) + 1];
strcpy(mRulesFileName, configFile);
memset(&mRulesFileMtimespec, 0, sizeof(mRulesFileMtimespec));
}
Engine::~Engine()
{
delete[] mRulesFileName;
}
void
Engine::updateRules(CFAbsoluteTime now)
{
if (mRules.empty())
readRules();
else
{
if (mLastChecked > now - 5.0)
return;
struct stat st;
if (stat(mRulesFileName, &st))
{
Syslog::error("Stating rules file \"%s\": %s", mRulesFileName, strerror(errno));
}
else
{
if (memcmp(&st.st_mtimespec, &mRulesFileMtimespec, sizeof(mRulesFileMtimespec)))
readRules();
}
}
mLastChecked = now;
}
void
Engine::readRules()
{
mRules.clear();
mRules.insert(RuleMap::value_type(string(), Rule()));
int fd = open(mRulesFileName, O_RDONLY, 0);
if (fd == -1)
{
Syslog::error("Opening rules file \"%s\": %s", mRulesFileName, strerror(errno));
return;
}
try
{
struct stat st;
if (fstat(fd, &st))
UnixError::throwMe(errno);
mRulesFileMtimespec = st.st_mtimespec;
off_t fileSize = st.st_size;
CFRef<CFMutableDataRef> xmlData(CFDataCreateMutable(NULL, fileSize));
CFDataSetLength(xmlData, fileSize);
void *buffer = CFDataGetMutableBytePtr(xmlData);
size_t bytesRead = read(fd, buffer, fileSize);
if (bytesRead != fileSize)
{
if (bytesRead == static_cast<size_t>(-1))
{
Syslog::error("Reading rules file \"%s\": %s", mRulesFileName, strerror(errno));
return;
}
Syslog::error("Could only read %ul out of %ul bytes from rules file \"%s\"",
bytesRead, fileSize, mRulesFileName);
return;
}
CFStringRef errorString;
CFRef<CFDictionaryRef> newRoot(reinterpret_cast<CFDictionaryRef>
(CFPropertyListCreateFromXMLData(NULL, xmlData, kCFPropertyListImmutable, &errorString)));
if (!newRoot)
{
char buffer[512];
const char *error = CFStringGetCStringPtr(errorString, kCFStringEncodingUTF8);
if (error == NULL)
{
if (CFStringGetCString(errorString, buffer, 512, kCFStringEncodingUTF8))
error = buffer;
}
Syslog::error("Parsing rules file \"%s\": %s", mRulesFileName, error);
return;
}
if (CFGetTypeID(newRoot) != CFDictionaryGetTypeID())
{
Syslog::error("Rules file \"%s\": is not a dictionary", mRulesFileName);
return;
}
parseRules(newRoot);
}
catch(...)
{
close(fd);
}
close(fd);
}
void
Engine::parseRules(CFDictionaryRef rules)
{
CFDictionaryApplyFunction(rules, parseRuleCallback, this);
}
void
Engine::parseRuleCallback(const void *key, const void *value, void *context)
{
Engine *engine = reinterpret_cast<Engine *>(context);
if (CFGetTypeID(key) != CFStringGetTypeID())
return;
CFStringRef right = reinterpret_cast<CFStringRef>(key);
engine->parseRule(right, reinterpret_cast<CFTypeRef>(value));
}
void
Engine::parseRule(CFStringRef cfRight, CFTypeRef cfRule)
{
char buffer[512];
const char *ptr = CFStringGetCStringPtr(cfRight, kCFStringEncodingUTF8);
if (ptr == NULL)
{
if (CFStringGetCString(cfRight, buffer, 512, kCFStringEncodingUTF8))
ptr = buffer;
}
string right(ptr);
try
{
mRules[right] = Rule(cfRule);
debug("authrule", "added rule for right \"%s\"", right.c_str());
}
catch (...)
{
Syslog::error("Rules file \"%s\" right \"%s\": rule is invalid", mRulesFileName, ptr);
}
}
Rule
Engine::getRule(const Right &inRight) const
{
string key(inRight.rightName());
for (;;)
{
RuleMap::const_iterator it = mRules.find(key);
if (it != mRules.end())
{
debug("authrule", "right \"%s\" using right expression \"%s\"", inRight.rightName(), key.c_str());
return it->second;
}
assert (key.size());
if (key.size() > 2) {
string::size_type index = key.rfind('.', key.size() - 2);
key = key.substr(0, index == string::npos ? 0 : index + 1);
} else
key.erase();
}
}
OSStatus
Engine::authorize(const RightSet &inRights, const AuthorizationEnvironment *environment,
AuthorizationFlags flags, const CredentialSet *inCredentials, CredentialSet *outCredentials,
MutableRightSet *outRights, const AuthorizationToken &auth)
{
CredentialSet credentials;
MutableRightSet rights;
OSStatus status = errAuthorizationSuccess;
CFAbsoluteTime now = CFAbsoluteTimeGetCurrent();
updateRules(now);
if (environment && (flags & kAuthorizationFlagExtendRights))
{
const AuthorizationItem *username = NULL, *password = NULL;
bool shared = false;
for (UInt32 ix = 0; ix < environment->count; ++ix)
{
const AuthorizationItem &item = environment->items[ix];
if (!strcmp(item.name, kAuthorizationEnvironmentUsername))
username = &item;
if (!strcmp(item.name, kAuthorizationEnvironmentPassword))
password = &item;
if (!strcmp(item.name, kAuthorizationEnvironmentShared))
shared = true;
}
if (username && password)
{
Credential newCredential(string(reinterpret_cast<const char *>(username->value), username->valueLength),
string(reinterpret_cast<const char *>(password->value), password->valueLength), shared);
if (newCredential->isValid())
credentials.insert(newCredential);
}
}
RightSet::const_iterator end = inRights.end();
for (RightSet::const_iterator it = inRights.begin(); it != end; ++it)
{
OSStatus result = getRule(*it).evaluate(*it, environment, flags, now,
inCredentials, credentials, auth);
if (result == errAuthorizationSuccess)
rights.push_back(*it);
else if (result == errAuthorizationDenied || result == errAuthorizationInteractionNotAllowed)
{
if (!(flags & kAuthorizationFlagPartialRights))
{
status = result;
break;
}
}
else if (result == errAuthorizationCanceled)
{
status = result;
break;
}
else
{
Syslog::error("Engine::authorize: Rule::evaluate returned %ld returning errAuthorizationInternal", result);
status = errAuthorizationInternal;
break;
}
}
if (outCredentials)
outCredentials->swap(credentials);
if (outRights)
outRights->swap(rights);
return status;
}