Accumulo controls access to data in its tables in a number of ways: authentication, permissions, and authorizations.
These can be thought of as applying at two levels: authentication and permissions at the higher application and table level, and authorizations—which are used along with column visibilities—at the lower, key-value–pair level. Authentication relates to Accumulo users and how a user confirms its identity to Accumulo. Permissions control what operations Accumulo users are allowed to perform. Authorizations control which key-value pairs Accumulo users are allowed to see.
Accumulo provides the ability to create accounts, grant permissions, and grant authorizations. All of these mechanisms are pluggable, with their defaults being to store and retrieve user information in ZooKeeper. Custom security mechanisms are discussed in “Custom Authentication, Permissions, and Authorization”.
High-level security-related operations such as creating users and granting permissions and authorizations are carried out via the SecurityOperations
object, obtained from a Connector
object:
SecurityOperations
secOps
=
conn
.
securityOperations
();
Security operations can be logged to an audit log if Accumulo is configured to do so (see “Auditing Security Operations”).
Low-level key-value–pair security occurs naturally whenever ColumnVisibility
and Authorizations
objects are used when reading and writing data.
For any given set of security mechanisms, there are essentially two ways to manage access control: create an account for every user using Accumulo’s security mechanisms, or create accounts for each application and delegate authentication, permissions, and authorization for each user to the application. In the latter case, it is the application’s job to authenticate individual users, look up their permissions and authorization tokens, and pass their authorizations faithfully onto Accumulo when data is read or written. This is discussed further in “Using an Application Account for Multiple Users”.
Accumulo user accounts are used to limit the permissions that an application or an individual user can carry out, and to limit the set of authorization tokens that can be used in lookups.
Some basic instance information such as instance ID, locations of master processes, and location of the root tablet can be retrieved from the Instance
object itself.
This information is available to anyone.
To retrieve any additional information from Accumulo, an application must authenticate as a particular user.
Before authenticating, the user must exist and have an AuthenticationToken
associated with it.
The default type of AuthenticationToken
is the PasswordToken
that simply wraps a password for the user.
AuthenticationToken
can be extended to support other authentication methods such as Lightweight Directory Access Protocol (LDAP).
To create a new user, use the createLocalUser()
method:
String
principal
=
"myApplications"
;
PasswordToken
password
=
new
PasswordToken
(
"appSecret"
);
secOps
.
createLocalUser
(
principal
,
password
);
// in version 1.4 and earlier
Authorizations
initialAuthorizations
=
new
Authorizations
();
secOps
.
createUser
(
principal
,
"password"
.
getBytes
(),
initialAuthorizations
);
After initialization Accumulo only has one user, the root user, with a password set at initialization time. The root user can be used to create other user accounts and grant privileges. See “Initialization” for more details on setting up the root account.
To authenticate as a user, provide a username, or principal, and an AuthenticationToken
when obtaining a Connector
object from an Instance
:
String
principal
=
"myApp"
;
AuthenticationToken
token
=
new
PasswordToken
(
"appSecret"
);
Connector
connector
=
instance
.
getConnector
(
principal
,
token
);
In addition, the following methods will simply return whether a particular principal and AuthenticationToken
are valid:
String
principal
=
"myApp"
;
AuthenticationToken
token
=
new
PasswordToken
(
"appSecret"
);
boolean
authenticated
=
authenticateUser
(
principal
,
token
);
// deprecated since 1.5
boolean
authenticated
=
authenticateUser
(
String
user
,
byte
[]
password
);
To set a user’s password, use changeLocalUserPassword()
:
String
principal
=
"myUser"
;
PasswordToken
token
=
new
PasswordToken
(
"newPassword"
);
secOps
.
changeLocalUserPassword
(
principal
,
token
);
// in 1.5 and older
secOps
.
changeUserPassword
(
principal
,
"newPassword"
.
getBytes
());
To obtain a list of users, use the listLocalUsers()
method:
Set
<
String
>
users
=
secOps
.
listLocalUsers
();
// in 1.4 and earlier
Set
<
String
>
users
=
secOps
.
listUsers
();
To remove a user from the system, use the dropLocalUser()
method:
secOps
.
dropLocalUser
(
"user"
);
// in 1.4 and earlier
secOps
.
dropUser
(
"user"
);
Once a user is authenticated to Accumulo, the types of operations allowed are governed by the permissions assigned to the Accumulo user.
There are system permissions, which are global; namespace permissions assigned per namespace; and table permissions assigned per table.
Some permission names are repeated in more than one scope.
For example, there are DROP_TABLE
permissions for the system, namespace, and table scopes.
These three permissions allow a user to delete any table, delete a table within a namespace, and delete a specific table, respectively.
The CREATE_TABLE
permissions only appear in system and namespace, because it does not make sense to create a specific table that already exists.
The user that creates a table is assigned all table permissions for that table. Users must be granted table permissions manually for tables they did not create, with the exception that all users can read the root and metadata tables.
If a user tries to perform an operation that is not allowed by the user’s current permissions, an exception will be thrown.
System permissions allow users to perform the following actions:
GRANT
Grant and revoke permissions for users
CREATE_TABLE
Create and import tables
DROP_TABLE
Remove tables
ALTER_TABLE
Configure table properties, perform actions on tables (compact, merge, online/offline, rename, and split), and grant or revoke permissions on tables
CREATE_USER
Create users and check permissions for users
DROP_USER
Remove users and check permissions for users
ALTER_USER
Change user authentication token or authorizations, and check permissions for users
CREATE_NAMESPACE
Create namespaces
DROP_NAMESPACE
Remove namespaces
ALTER_NAMESPACE
Rename namespaces, configure namespace properties, and grant or revoke permissions on namespaces
SYSTEM
Perform administrative actions including granting and revoking the SYSTEM
permission, checking authentication for users, checking permissions and authorizations for users, and performing table actions (merge, online/offline, split, and delete a range of rows)
To grant a system permission to a user, use the grantSystemPermission()
method. For example:
String
principal
=
"user"
;
secOps
.
grantSystemPermission
(
principal
,
SystemPermission
.
CREATE_TABLE
);
To see whether a user has a particular system permission, use the hasSystemPermission()
method:
boolean
hasPermission
=
secOps
.
hasSystemPermission
(
principal
,
SystemPermission
.
CREATE_TABLE
);
Permissions can be revoked for users via the revokeSystemPermission()
method:
secOps
.
revokeSystemPermission
(
principal
,
SystemPermission
.
CREATE_TABLE
);
An example of granting a user system-wide permissions is as follows:
// get a connector as the root user
Connector
adminConn
=
instance
.
getConnector
(
"root"
,
rootPasswordToken
);
// get a security operations object as the root user
SecurityOperations
adminSecOps
=
adminConn
.
securityOperations
();
// admin creates a new user
String
principal
=
"testUser"
;
PasswordToken
token
=
new
PasswordToken
(
"password"
);
adminSecOps
.
createLocalUser
(
principal
,
token
);
// get a connector as our new user
Connector
userConn
=
instance
.
getConnector
(
principal
,
token
);
// ...
// user tries to create user table in default namespace
String
userTable
=
"userTable"
;
try
{
userConn
.
tableOperations
().
create
(
userTable
);
}
catch
(
AccumuloSecurityException
ex
)
{
System
.
out
.
println
(
"user unauthorized to create table in default namespace"
);
}
adminSecOps
.
grantSystemPermission
(
principal
,
SystemPermission
.
CREATE_TABLE
);
userConn
.
tableOperations
().
create
(
userTable
);
System
.
out
.
println
(
"table creation in default namespace succeeded"
);
Permissions can apply to namespaces as well. Namespace permissions are granted for a particular namespace. Some of the permissions apply to actions performed on the namespace itself, and some apply to all tables within the namespace:
ALTER_NAMESPACE
Grant and revoke table permissions for tables in the namespace, and alter the namespace
ALTER_TABLE
Alter tables in the namespace
BULK_IMPORT
Import into tables in the namespace
CREATE_TABLE
Create tables in the namespace
DROP_NAMESPACE
Delete the namespace
DROP_TABLE
Delete a table from the namespace
GRANT
Grant and revoke namespace permissions on a namespace, and alter the namespace
READ
Read tables in the namespace
WRITE
Write to tables in the namespace
To check whether a user has a permission for a given namespace, use the hasNamespacePermission()
method:
String
namespace
=
"myNamespace"
;
boolean
hasNSWritePermission
=
secOps
.
hasNamespacePermission
(
principal
,
namespace
,
NamespacePermission
.
WRITE
);
To grant a user a permission for a namespace, use the grantNamespacePermission()
method.
The permission will apply to all tables within the namespace:
secOps
.
grantNamespacePermission
(
principal
,
namespace
,
NamespacePermission
.
WRITE
);
To revoke a permission from a user for a namespace, use the revokeNamespacePermission()
method:
secOps
.
revokeNamespacePermission
(
principal
,
namespace
,
NamespacePermission
.
WRITE
);
A short example:
String
adminNS
=
"adminNamespace"
;
adminConn
.
namespaceOperations
().
create
(
adminNS
);
try
{
userConn
.
tableOperations
().
create
(
adminNS
+
".userTable"
);
}
catch
(
AccumuloSecurityException
ex
)
{
System
.
out
.
println
(
"user unauthorized to create table in adminNamespace"
);
}
// allow user to create tables in the root NS
adminSecOps
.
grantNamespacePermission
(
principal
,
adminNS
,
NamespacePermission
.
CREATE_TABLE
);
userConn
.
tableOperations
().
create
(
adminNS
+
".userTable"
);
System
.
out
.
println
(
"table creation in adminNamespace succeeded"
);
Table permissions are granted per table, allowing users to perform actions on specific tables:
READ
Scan and export the table
WRITE
Write to the table, including deleting data, and perform some administrative actions for the table including flushing and compaction
BULK_IMPORT
Import files to the table
ALTER_TABLE
Configure table properties, perform actions on the table (compact, flush, merge, online/offline, rename, and split), and grant or revoke permissions on the table
GRANT
Grant and revoke permissions for the table
DROP_TABLE
Remove the table
Some actions require a combination of permissions. These include:
TablePermission.READ
and TablePermission.WRITE
SystemPermission.CREATE_TABLE
or NamespacePermission.CREATE_TABLE
and TablePermission.READ
on table being cloned
To grant a table permission to a user for a table, use the grantTablePermission()
method:
String
table
=
"myTable"
;
secOps
.
grantTablePermission
(
principal
,
table
,
TablePermission
.
WRITE
);
To check whether a user has a specific permission on a table, use the hasTablePermission()
method:
boolean
canWrite
=
secOps
.
hasTablePermission
(
principal
,
table
,
TablePermission
.
WRITE
);
To revoke a permission from a user for a given table, use the revokeTablePermission()
method:
secOps
.
revokeTablePermission
(
principal
,
table
,
TablePermission
.
WRITE
);
These actions can be carried out in the shell as well, as detailed in “Application Permissions”.
An example of using table permissions is as follows:
String
adminTable
=
"adminTable"
;
adminConn
.
tableOperations
().
create
(
adminTable
);
// user tries to write data
BatchWriterConfig
config
=
new
BatchWriterConfig
();
BatchWriter
writer
=
userConn
.
createBatchWriter
(
adminTable
,
config
);
Mutation
m
=
new
Mutation
(
"testRow"
);
m
.
put
(
""
,
"testColumn"
,
"testValue"
);
try
{
writer
.
addMutation
(
m
);
writer
.
close
();
}
catch
(
Exception
ex
)
{
System
.
out
.
println
(
"user unable to write to admin table"
);
}
// admin grants permission for user to write data
adminSecOps
.
grantTablePermission
(
principal
,
adminTable
,
TablePermission
.
WRITE
);
writer
=
userConn
.
createBatchWriter
(
adminTable
,
config
);
writer
.
addMutation
(
m
);
writer
.
close
();
System
.
out
.
println
(
"user can write to admin table"
);
See the full listing of PermissionsExample.java for more detail.
Once a user is authenticated to Accumulo and is given permission to read a table, the user’s authorizations govern which key-value pairs can be retrieved.
Authorizations are applied to Scanner
s and BatchScanner
s.
The set of authorizations that can be used for a particular scan is limited by the set of authorizations associated with the user account.
If a user attempts to scan with an authorization that is not already associated with the user account specified in the Connector
, an exception will be thrown.
To see the list of authorizations associated with a user, use the getUserAuthorizations()
method:
Authorizations
auths
=
secOps
.
getUserAuthorizations
(
principal
);
To associate authorizations with a user, use the changeUserAuthorizations()
method.
This will replace any existing authorizations associated with the user:
Authorizations
auths
=
new
Authorizations
(
"a"
,
"b"
,
"c"
);
secOps
.
changeUserAuthorizations
(
principal
,
auths
);
Be sure to include existing authorizations when using the changeUserAuthorizations()
method to add new authorization tokens, or else previous tokens will be lost.
Existing tokens can be retrieved with the getUserAuthorizations()
method.
A user’s authorizations encapsulate a set of strings, sometimes referred to as authorization tokens. These strings have no intrinsic meaning for Accumulo, but an application can assign its own meaning to them, such as groups or roles for its users.
For a user to be able to read data from Accumulo, a table name and a set of authorization tokens must be provided to either the createScanner()
or the createBatchScanner()
method of the Connector
:
Scanner
scan
=
conn
.
createScanner
(
"myTable"
,
new
Authorizations
(
"a"
,
"b"
,
"c"
));
Because the Connector
is associated with a specific user, the authorizations provided when a Scanner
or BatchScanner
is obtained must be a subset of the authorizations assigned to that user.
If they are not, an exception will be thrown.
Passing in a set of authorizations at scan time allows a user to act in different roles at different times. It also allows applications to manage their own users apart from Accumulo. You can choose to have one Accumulo user account for an entire application and to let the application set the authorizations for each scan based on the current user of the application.
Although this requires the application to take on the responsibility for managing accurate authorizations for their users, it also prevents users from having to interact with Accumulo or the underlying Hadoop system directly, allowing more strict control over access to your data.
See “An Example of Using Authorizations” for an example of using less than all of the possible authorizations associated with a user.
To determine which key-value pairs can be seen given a particular set of authorizations, each key has a column visibility portion.
A column visibility consists of a Boolean expression containing tokens, &
(and), |
(or), and parentheses—such as (a&bc)|def
. Evaluation of the Boolean expression requires each string to be interpreted as true or false.
For a given set of authorizations, a string is interpreted as true if it is contained in the set of authorizations.
The visibility (a&bc)|def
would evaluate to true for authorization sets containing the string def
or containing both of the strings a
and bc
.
When the visibility evaluates to true for a given key and a set of authorizations, that key-value pair is returned to the user.
If not, the key-value pair is not included in the set of key-value pairs returned to the client.
Thus it isn’t possible to find out that a particular key-value pair exists, or to see the full key or value, without satisfying the column visibility.
Tokens used in column visibilities can consist of letters, numbers, underscore, dash, colon, and as of Accumulo version 1.5, can contain periods and forward slashes.
As of version 1.5, tokens can also contain arbitrary characters if the token is surrounded by quotes, as in "a?b"&c
.
The corresponding authorizations do not need to be quoted, so the minimum set of authorizations needed to view this example visibility would contain a?b
and c
.
By default, if users have write permission for a table, they can write keys that they do not have authorization to retrieve. You can change this behavior by configuring a constraint on the table.
With the VisibilityConstraint
, users cannot write data they are not allowed to read:
connector
.
tableOperations
().
addConstraint
(
tableName
,
VisibilityConstraint
.
class
.
getName
());
This can also be accomplished through the Accumulo shell.
When the table is created, add an -evc
flag to the createtable
command:
user@accumulo> createtable -evc tableName
To add the constraint to an existing table, use the constraint
command instead:
user@accumulo> constraint -t tableName -a org.apache.accumulo.core.security.VisibilityConstraint
We’ll illustrate bringing the concepts of users, permissions, authorizations, and column visibilities together in a quick example. Let’s say we are writing an application to keep track of the information associated with a safe in a bank. The safe contains a set of safety deposit boxes that are used by bank employees and customers to store objects securely.
There is an outer door to the safe that is protected by a combination known only to a few bank employees. Other bank employees can see privileged information about the safe, but not information about the contents of customers’ boxes.
Customers can write down and read information about safety deposit boxes they rent but cannot see any information privileged to bank employees or other customers.
First we’ll create a table as an administrator and write initial information about a particular safe:
// get a connector as the root user
Connector
adminConn
=
instance
.
getConnector
(
"root"
,
rootPasswordToken
);
// get a security operations object as the root user
SecurityOperations
secOps
=
adminConn
.
securityOperations
();
// admin creates a new table and writes some data
// protected with Column Visibilities
System
.
out
.
println
(
" --- creating table ---"
);
String
safeTable
=
"safeTable"
;
adminConn
.
tableOperations
().
create
(
safeTable
);
The admin writes the initial information about a safe, including the name, location, and combination:
// admin writes initial data
System
.
out
.
println
(
" --- writing initial data ---"
);
BatchWriterConfig
config
=
new
BatchWriterConfig
();
BatchWriter
writer
=
adminConn
.
createBatchWriter
(
safeTable
,
config
);
Mutation
m
=
new
Mutation
(
"safe001"
);
// write information about this particular safe
m
.
put
(
"info"
,
"safeName"
,
new
ColumnVisibility
(
"public"
),
"Super Safe Number 17"
);
m
.
put
(
"info"
,
"safeLocation"
,
new
ColumnVisibility
(
"bankEmployee"
),
"3rd floor of bank 2"
);
m
.
put
(
"info"
,
"safeOuterDoorCombo"
,
new
ColumnVisibility
(
"bankEmployee&safeWorker"
),
"123-456-789"
);
// store some information about bank owned contents stored in the safe
m
.
put
(
"contents"
,
"box001"
,
new
ColumnVisibility
(
"bankEmployee"
),
"bank charter"
);
// commit mutations
writer
.
addMutation
(
m
);
writer
.
close
();
Next the administrator will need to create user accounts for customers. In this example we’re using one account per individual user.
Each customer gets a unique user ID and authorization token, in addition to the public
token:
// admin creates a new customer user
String
customer
=
"customer003"
;
PasswordToken
customerToken
=
new
PasswordToken
(
"customerPassword"
);
secOps
.
createLocalUser
(
customer
,
customerToken
);
// set authorizations for user and grant permission to read and write
// to the safe table
Authorizations
customerAuths
=
new
Authorizations
(
"public"
,
"customer003"
);
secOps
.
changeUserAuthorizations
(
customer
,
customerAuths
);
secOps
.
grantTablePermission
(
customer
,
safeTable
,
TablePermission
.
READ
);
Now the newly created customer can log in and is prevented from seeing any information privileged to bank employees:
// get a connector as our customer user
Connector
customerConn
=
instance
.
getConnector
(
customer
,
customerToken
);
// user attempts to get a scanner with
// authorizations not associated with the user
System
.
out
.
println
(
" --- customer scanning table for bank employee "
+
"privileged information ---"
);
Scanner
scanner
;
try
{
scanner
=
customerConn
.
createScanner
(
safeTable
,
new
Authorizations
(
"public"
,
"bankEmployee"
));
for
(
Map
.
Entry
<
Key
,
Value
>
e
:
scanner
)
{
System
.
out
.
println
(
e
);
}
}
catch
(
Exception
ex
)
{
System
.
out
.
println
(
"problem scanning table: "
+
ex
.
getMessage
());
}
This results in the output:
--- customer scanning table for bank employee privileged information --- problem scanning table: org.apache.accumulo.core.client.AccumuloSecurityException: Error BAD_AUTHORIZATIONS for user customer003 on table safeTable(ID:1) - The user does not have the specified authorizations assigned
If the customer scans the table with all the authorizations associated with his account, she will see the information marked as public:
// user reads data with authorizations associated with the user
System
.
out
.
println
(
" --- customer scanning table for allowed information ---"
);
scanner
=
customerConn
.
createScanner
(
safeTable
,
customerAuths
);
for
(
Map
.
Entry
<
Key
,
Value
>
e
:
scanner
)
{
System
.
out
.
println
(
e
);
}
The output is:
--- customer scanning table for allowed information --- safe001 info:safeName [public] 1409424734681 false Super Safe Number 17
The customer must be granted write access to the table before writing any information. The customer can then write information protected with a column visibility consisting of just his own unique authorization token. Subsequent scans will return this information along with the public safe information:
// admin grants write permission to user
secOps
.
grantTablePermission
(
customer
,
safeTable
,
TablePermission
.
WRITE
);
// user writes information only she can see to the table
// describing the contents of a rented safety deposit box
System
.
out
.
println
(
" --- customer writing own information ---"
);
BatchWriter
userWriter
=
customerConn
.
createBatchWriter
(
safeTable
,
config
);
Mutation
userM
=
new
Mutation
(
"safe001"
);
userM
.
put
(
"contents"
,
"box004"
,
new
ColumnVisibility
(
"customer003"
),
"jewelry, extra cash"
);
userWriter
.
addMutation
(
userM
);
userWriter
.
flush
();
// scan to see the bank info and our own info
System
.
out
.
println
(
" --- customer scanning table for allowed information ---"
);
scanner
=
customerConn
.
createScanner
(
safeTable
,
customerAuths
);
for
(
Map
.
Entry
<
Key
,
Value
>
e
:
scanner
)
{
System
.
out
.
println
(
e
);
}
The output is:
--- customer writing own information --- --- customer scanning table for allowed information --- safe001 contents:box004 [customer003] 1409424734828 false jewelry, extra cash safe001 info:safeName [public] 1409424734681 false Super Safe Number 17
Now the administrator will create an account for a bank employee. The bank employee will have access to bank privileged information, public information, but not any information associated with any customer:
// admin creates a new bank employee user
String
bankEmployee
=
"bankEmployee005"
;
PasswordToken
bankEmployeeToken
=
new
PasswordToken
(
"bankEmployeePassword"
);
secOps
.
createLocalUser
(
bankEmployee
,
bankEmployeeToken
);
// admin sets authorizations for bank employee
// and grants read permission for the table
Authorizations
bankEmployeeAuths
=
new
Authorizations
(
"bankEmployee"
,
"public"
);
secOps
.
changeUserAuthorizations
(
bankEmployee
,
bankEmployeeAuths
);
secOps
.
grantTablePermission
(
bankEmployee
,
safeTable
,
TablePermission
.
READ
);
// connect as bank employee
Connector
bankConn
=
instance
.
getConnector
(
bankEmployee
,
bankEmployeeToken
);
If the bank employee attempts to scan for customer information, an exception will be thrown:
// attempt to scan customer information
System
.
out
.
println
(
" --- bank employee scanning table for customer "
+
"information ---"
);
Scanner
bankScanner
;
try
{
bankScanner
=
bankConn
.
createScanner
(
safeTable
,
new
Authorizations
(
"customer003"
));
for
(
Map
.
Entry
<
Key
,
Value
>
e
:
bankScanner
)
{
System
.
out
.
println
(
e
);
}
}
catch
(
Exception
ex
)
{
System
.
out
.
println
(
"problem scanning table: "
+
ex
.
getMessage
());
}
Resulting in the output:
--- bank employee scanning table for customer information --- problem scanning table: org.apache.accumulo.core.client.AccumuloSecurityException: Error BAD_AUTHORIZATIONS for user bankEmployee005 on table safeTable(ID:1) - The user does not have the specified authorizations assigned
Now we’ll have the bank employee scan for all information she is allowed to see. Because this employee has a set of authorizations different from the customer’s, this view of the table will be different than the view the customer gets when doing the same scan:
// bank employee scans all information they are allowed to see
System
.
out
.
println
(
" --- bank employee scanning table for allowed "
+
"information ---"
);
bankScanner
=
bankConn
.
createScanner
(
safeTable
,
bankEmployeeAuths
);
for
(
Map
.
Entry
<
Key
,
Value
>
e
:
bankScanner
)
{
System
.
out
.
println
(
e
);
}
Here is the output:
--- bank employee scanning table for allowed information --- safe001 contents:box001 [bankEmployee] 1409424734681 false bank charter safe001 info:safeLocation [bankEmployee] 1409424734681 false 3rd floor of bank 2 safe001 info:safeName [public] 1409424734681 false Super Safe Number 17
It is also possible to perform a scan using less than all the authorizations we possess.
In this case, the bank employee will generate a view of the table that is viewable by users with only the public
token:
// bank employee scans using a subset of authorizations
// to check which information is viewable to the public
System
.
out
.
println
(
" --- bank employee scanning table for only public "
+
"information ---"
);
bankScanner
=
bankConn
.
createScanner
(
safeTable
,
new
Authorizations
(
"public"
));
for
(
Map
.
Entry
<
Key
,
Value
>
e
:
bankScanner
)
{
System
.
out
.
println
(
e
);
}
Here is the view generated:
--- bank employee scanning table for only public information --- safe001 info:safeName [public] 1409424734681 false Super Safe Number 17
Finally, we may want to protect the table against attempts to write information to a key-value pair that is protected with a visibility that the writing user cannot satisfy. This prevents confusing situations in which a user writes data but then cannot read it out:
// admin protects table against users writing new data they cannot read
adminConn
.
tableOperations
().
addConstraint
(
safeTable
,
"org.apache.accumulo.core.security.VisibilityConstraint"
);
// customer attempts to write information protected with a bank authorization
// which would erase the combination for the outer door of the safe
System
.
out
.
println
(
" --- customer attempting to overwrite bank "
+
"information ---"
);
try
{
userM
=
new
Mutation
(
"safe001"
);
userM
.
put
(
"info"
,
"safeOuterDoorCombo"
,
new
ColumnVisibility
(
"bankEmployee&safeWorker"
),
"------"
);
userWriter
.
addMutation
(
userM
);
userWriter
.
flush
();
}
catch
(
Exception
e
)
{
System
.
out
.
println
(
"problem attempting to write data: "
+
e
.
getMessage
());
}
This results in the error:
--- customer attempting to overwrite bank information --- problem attempting to write data: # constraint violations : 1 security codes: {} # server errors 0 # exceptions 0
Even if users are able to write a new key-value pair using the same row ID and column as an existing key, they can only cause the newly written key-value pair to obscure the old key-value pair, via Accumulo’s VersioningIterator
, which by default returns only the newest version of a key-value pair.
It would be possible in this case to configure a scan to read more than one version for a key, which would allow authorized users to see the old key-value pair.
But it would not be possible for the new key-value pair to cause the value of the old key-value pair to become visible. According to the column visibility of the new key-value pair, it would simply be obscured.
This inability to expose information this way, by writing new key-value pairs, makes it possible to build highly secure applications more easily, because applications do not have to explicitly prevent this issue.
You may have noticed in our example application that all key-value pairs were protected with at least one token in a column visibility.
We used the public
token to denote information that everyone was able to read, and distributed the public
authorization token to all users.
It is possible to have a table in which some key-value pairs have column visibilities and others do not. The default behavior for unlabeled data is to allow any user to read it. This can be changed by applying a default visibility to a table.
When the default visibility is specified, unlabeled key-value pairs will be treated as if they are labeled with the default column visibility.
To specify the default visibility for a table, set the table.security.scan.visibility.default
property to the desired column visibility expression.
For example:
ops
.
setProperty
(
"table.security.scan.visibility.default"
,
"public"
);
When key-value pairs with empty labels are scanned, if they are returned as part of the scan they are displayed as having a blank column visibility, even when a default visibility is set.
Here is an example of the way a view of a table will change after the default visibility is set. First we’ll create a table that has a key-value pair with a blank column visibility and see it show up in all scans:
// get a connector as the root user
Connector
conn
=
instance
.
getConnector
(
"root"
,
rootPasswordToken
);
// create an example table
String
exampleTable
=
"example"
;
conn
.
tableOperations
().
create
(
exampleTable
);
// write some data with col vis and others without
BatchWriterConfig
config
=
new
BatchWriterConfig
();
BatchWriter
writer
=
conn
.
createBatchWriter
(
exampleTable
,
config
);
Mutation
m
=
new
Mutation
(
"one"
);
m
.
put
(
""
,
"col1"
,
"value in unlabeled entry"
);
m
.
put
(
""
,
"col2"
,
new
ColumnVisibility
(
"public"
),
"value in public entry"
);
m
.
put
(
""
,
"col3"
,
new
ColumnVisibility
(
"private"
),
"value in private entry"
);
writer
.
addMutation
(
m
);
writer
.
close
();
// add auths to root account
conn
.
securityOperations
().
changeUserAuthorizations
(
"root"
,
new
Authorizations
(
"public"
,
"private"
));
// scan with no auths
System
.
out
.
println
(
" no auths:"
);
Scanner
scan
=
conn
.
createScanner
(
exampleTable
,
Authorizations
.
EMPTY
);
for
(
Map
.
Entry
<
Key
,
Value
>
e
:
scan
)
{
System
.
out
.
println
(
e
);
}
// scan with public auth
System
.
out
.
println
(
" public auth:"
);
scan
=
conn
.
createScanner
(
exampleTable
,
new
Authorizations
(
"public"
));
for
(
Map
.
Entry
<
Key
,
Value
>
e
:
scan
)
{
System
.
out
.
println
(
e
);
}
// scan with public and private auth
System
.
out
.
println
(
" public and private auths:"
);
scan
=
conn
.
createScanner
(
exampleTable
,
new
Authorizations
(
"public"
,
"private"
));
for
(
Map
.
Entry
<
Key
,
Value
>
e
:
scan
)
{
System
.
out
.
println
(
e
);
}
The output of this is as follows:
no auths: one :col1 [] 1409429068159 false value in unlabeled entry
public auth: one :col1 [] 1409429068159 false value in unlabeled entry one :col2 [public] 1409429068159 false value in public entry
public and private auths: one :col1 [] 1409429068159 false value in unlabeled entry one :col2 [public] 1409429068159 false value in public entry one :col3 [private] 1409429068159 false value in private entry
Now we’ll add a default visibility:
// turn on default visibility
System
.
out
.
println
(
" turning on default visibility"
);
conn
.
tableOperations
().
setProperty
(
exampleTable
,
"table.security.scan.visibility.default"
,
"x"
);
// scan with no auths
System
.
out
.
println
(
" no auths:"
);
scan
=
conn
.
createScanner
(
exampleTable
,
Authorizations
.
EMPTY
);
for
(
Map
.
Entry
<
Key
,
Value
>
e
:
scan
)
{
System
.
out
.
println
(
e
);
}
// scan with public auth
System
.
out
.
println
(
" public auth:"
);
scan
=
conn
.
createScanner
(
exampleTable
,
new
Authorizations
(
"public"
));
for
(
Map
.
Entry
<
Key
,
Value
>
e
:
scan
)
{
System
.
out
.
println
(
e
);
}
// scan with public and private auth
System
.
out
.
println
(
" public and private auths:"
);
scan
=
conn
.
createScanner
(
exampleTable
,
new
Authorizations
(
"public"
,
"private"
));
for
(
Map
.
Entry
<
Key
,
Value
>
e
:
scan
)
{
System
.
out
.
println
(
e
);
}
The output for this now appears as:
turning on default visibility no auths:
public auth: one :col2 [public] 1409429068159 false value in public entry
public & private auths: one :col2 [public] 1409429068159 false value in public entry one :col3 [private] 1409429068159 false value in private entry
For authorizations to be effective in protecting access to data in Accumulo, applications and users must:
Properly apply column visibilities to data at ingest time.
Apply the right authorizations at scan time.
Often Accumulo applications will rely on using specially vetted libraries for creating the proper column visibilities. If not, then ingest clients can be individually reviewed and trusted.
For retrieving authorizations, a separate service can be employed to manage the association of individual users to their sets of authorizations. This service is trusted by the application, and the application itself is trusted to faithfully pass along the authorizations retrieved from such a service to Accumulo.
A typical deployment can be like that shown in Figure 5-1.
Accumulo can be configured to log security operations.
Auditing is configured in the auditLog.xml file in the Accumulo conf/ directory.
The logging is done via the Java log4j
package and by default is configured to log via a DailyRollingFileAppender
to a local file named <hostname>.audit in the Accumulo log directory.
The following section of the auditLog.xml file configures the logging level:
<logger
name=
"Audit"
additivity=
"false"
>
<appender-ref
ref=
"Audit"
/>
<level
value=
"OFF"
/>
</logger>
By default, logging is turned off. To enable logging security operations that fail due to lack of permissions, set the level to WARN
:
<level
value=
"WARN"
/>
To log all security operations, set the level to INFO
.
This will include successful security operations logged as operation: permitted as well as unsuccessful operations logged as operation: denied.
Scanning with an authorization the user does not possess is an example of an operation that would be logged as denied at the INFO
level:
<level
value=
"INFO"
/>
The authentication, permissions, and authorization tasks for Accumulo accounts are handled in ZooKeeper by default.
These tasks are handled by three classes: ZKAuthenticator
for authenticating users, ZKAuthorizor
for associating users with authorizations, and ZKPermHandler
for determining what actions a user can carry out on the system and tables.
As of Accumulo version 1.5 developers can provide custom classes that override these default security mechanisms. This allows organizations that manage users and their authorizations in a centralized system to integrate those existing systems with Accumulo. In these cases, the custom classes must be available to server processes and specified in the accumulo-site.xml configuration file.
Not all of the three mechanisms must be overridden at the same time. For example, you can choose to rely on ZooKeeper for permissions handling and authentication, while using a custom authorization mechanism.
The default configuration of these mechanisms’ properties is shown in Table 5-1.
Setting name | Default | Purpose |
---|---|---|
instance.security.authorizor |
|
Associate users with authorization tokens |
instance.security.authenticator |
|
Authenticate users |
instance.security.permissionHandler |
|
Manage users’ system and table-level permissions |
These settings cannot be changed in ZooKeeper on a running cluster. They must be changed in the accumulo-site.xml file and require a restart of Accumulo for the changes to take effect.
Creating a custom mechanism is done by implementing the Authenticator
, Authorizor
, or PermissionHandler
interface.
These interfaces define the methods required by Accumulo to determine the access restrictions for user requests.
Here we’ll implement a trivial authenticator that uses only one hardcoded username and password. This would be impractical for any real-world deployment because no changes to the initial settings are possible, but it will help us illustrate the process of configuring and deploying a custom authentication scheme.
For this incredibly simple example we’ll implement only a few methods, shown in the following code. The rest of the methods of the interface that must be implemented we will leave empty:
public
class
HardCodedAuthenticator
implements
Authenticator
{
@Override
public
boolean
authenticateUser
(
String
principal
,
AuthenticationToken
token
)
throws
AccumuloSecurityException
{
return
principal
.
equals
(
"onlyUser"
)
&&
new
String
(((
PasswordToken
)
token
).
getPassword
()).
equals
(
"onlyPassword"
);
}
@Override
public
Set
<
String
>
listUsers
()
throws
AccumuloSecurityException
{
HashSet
<
String
>
users
=
new
HashSet
<
String
>();
users
.
add
(
"onlyUser"
);
return
users
;
}
@Override
public
boolean
userExists
(
String
user
)
throws
AccumuloSecurityException
{
return
user
.
equals
(
"onlyUser"
);
}
@Override
public
Set
<
Class
<?
extends
AuthenticationToken
>>
getSupportedTokenTypes
()
{
return
(
Set
)
Sets
.
newHashSet
(
PasswordToken
.
class
);
}
@Override
public
boolean
validTokenClass
(
String
tokenClass
)
{
return
tokenClass
.
equals
(
PasswordToken
.
class
.
toString
());
}
...
}
We can build and deploy our example code JAR as described in “Deploying JARs”.
Next we need to stop Accumulo if it’s running and configure it to use our Authenticator
.
In practice, custom security mechanisms like this should most likely be configured before Accumulo is initialized, so that the proper authorizations and permissions can be coordinated with the creation of the initial root user.
We’ll only change the authenticator in accumulo-site.xml in this example:
<property>
<name>
instance.security.authenticator</name>
<value>
com.accumulobook.tableapi.HardCodedAuthenticator</value>
</property>
Once configuration is done, we can start up Accumulo and attempt to authenticate using the username onlyUser
and password onlyPassword
:
[centos@centos]$ bin/accumulo shell -u onlyUser Password: ************
Shell - Apache Accumulo Interactive Shell - - version: 1.6.0 - instance name: test - - type 'help' for a list of available commands - onlyUser@test> tables accumulo.metadata accumulo.root trace
Any attempt to use our previous root account will fail:
[centos@centos]$ bin/accumulo shell -u root [shell.Shell] ERROR: org.apache.accumulo.core.client.AccumuloSecurityException: Error BAD_CREDENTIALS for user root - Username or Password is Invalid [centos@centos]$
Our hardcoded user account will not have permissions to manipulate anything, or any authorizations, so it is not very practical. In practice, these custom mechanisms will need to store information in a centralized location accessible to all processes, as the default ZooKeeper implementation does. For example, you could use a simple relational database or an LDAP service.
Custom authorizers and permissions handlers can be created and deployed similarly.
In addition to column visibilities being properly applied at ingest time and the proper authorizations retrieved and used in scans, there are some other things to consider when building a secure application on the Accumulo API:
Direct access to tablet servers must be limited to trusted applications—because the application is trusted to present the proper authorizations at scan time. A rogue client may be configured to pass in authorizations the user does not have.
Access to the underlying HDFS instance must not be allowed. Otherwise an HDFS client could open and read all the key-value pairs stored in Accumulo’s files without presenting the proper authorizations.
Similarly, access should be disallowed to the underlying Linux filesystem on machines on which tablet server and HDFS DataNode processes run.
Access to ZooKeeper should be restricted because Accumulo uses it to store configuration information about the cluster, including the list of Accumulo accounts and passwords.
Many Accumulo applications do not create accounts through Accumulo for each individual user. This is because some clients choose to do their own authentication and authorization of individual users via a centralized service within an organization. Clients are therefore trusted to present user credentials properly.
When applications are deployed this way, client applications must still authenticate themselves to Accumulo before performing any reads or writes. Administrators and application designers can restrict the privileges that a client has to particular tables, as well as the maximal set of authorizations the client is allowed to pass for any of the users it is serving. This way, even though users can have more authorizations granted to them than an application requires, a client application’s account can be restricted to those authorizations deemed necessary to carry out the actions of that particular application.
The network that Accumulo uses to communicate between nodes and to HDFS and ZooKeeper should be protected against unauthorized access. Most Accumulo deployments do not use Secure Socket Layer (SSL) between nodes, but rather use SSL between user browsers and trusted web applications.
See “Network Security” for more information on securing the network for an Accumulo deployment.
Disks can be encrypted to prevent unauthorized reading of the data should a physical hard drive be stolen. But if those with physical access to the cluster are not trusted, then the operating system and memory of the machines participating in the Accumulo cluster would have to be similarly protected. When running Accumulo in multitenant environments, such as a cloud infrastructure-as-a-service provider like Amazon’s EC2 or Rackspace, consideration should be given to the security precautions implemented by the service provider.
For both situations—running in a cluster without trusting those with physical access or running in the cloud—it may be feasible to employ application-level encryption of values and to devise keys that are not sensitive. This is problematic when it comes to building a secondary index, which can rely on the ordering of values to perform scans.
If scans across ranges of terms in an index can be foregone, then using a strategy involving hashes of values as keys can still provide fast simple lookups. Ranges of terms could no longer be scanned because secure hashes of index terms would, by virtue of the design of hash functions, no longer have any meaningful sort order. In this case, adjacent keys would have no relationship to each other.
Accumulo also supports encryption of data at rest via modules that implement the org.apache.accumulo.core.security.crypto.CryptoModule
interface, which consists of the following methods:
CryptoModuleParameters
getEncryptingOutputStream
(
CryptoModuleParameters
params
)
CryptoModuleParameters
getDecryptingInputStream
(
CryptoModuleParameters
params
)
CryptoModuleParameters
generateNewRandomSessionKey
(
CryptoModuleParameters
params
)
CryptoModuleParameters
initializeCipher
(
CryptoModuleParameters
params
)
The DefaultCryptoModule
class is an example that can be used to encrypt data stored in HDFS. This implementation stores the master key along with files in HDFS, which may not meet security requirements. For details on configuring Accumulo to use this or other modules, see “Encryption of Data at Rest”.
3.141.202.30