Authentication Methods

The implementation of authentication methods follows the specification given in the etcd authentication and security API. etcd permits the use of authentication only on authentication and key-value methods.

There are three types of authentication resources in etcd. Users, roles and permissions.

  1. A user is an identity to be authenticated and under which one can get access to different resources in etcd. A user has a list of permissions and a list of roles.
  2. A role is basically a list of permissions that can be granted to a number of users.
  3. A permission grants access to resources in etcd. It can be either read access, write access, or both.

With the help of authentication methods, a system administrator can enable or disable authentication. It is also possible to create, update or delete users and roles with different levels of access to etcd. Once authentication is enabled, users must get authorization in order to get or set data in etcd.

getAuthenticationStatus

Checks whether authentication is enabled or disabled:

getAuthenticationStatus: Future[EtcdGetAuthenticationStatusResult]

Implemented according to etcd Auth and Security API: “API endpoints ”.

Returns a Future wrapped around either EtcdGetAuthenticationStatusResponse or EtcdStandardError extending EtcdGetAuthenticationStatusResult.

enableAuthentication

Enables authentication:

enableAuthentication(): Future[EtcdDefaultResult]

Implemented according to etcd Auth and Security API: “API endpoints ”.

In order to enable authentication, the root user must be created first using addUpdateUser. After authentication only the root user or users with root rights (for example, with the root role) can access and modify permission resources.

Returns a Future wrapped around either EtcdConfirmationResponse or EtcdStandardError extending EtcdConfirmationResult.

disableAuthentication

Disables authentication:

disableAuthentication(): Future[EtcdDefaultResult]

Implemented according to etcd Auth and Security API: “API endpoints ”.

Returns a Future wrapped around either EtcdConfirmationResponse or EtcdStandardError extending EtcdConfirmationResult.

addUpdateUser

Creates an user or updates the details of an existing one:

addUpdateUser(user: EtcdUserRequestForm): Future[EtcdAddUpdateUserResult]

Implemented according to etcd Auth and Security API: “API endpoints ”.

In order to add a new user, one must fill a EtcdUserRequestForm and pass it as an argument to this method.

Parameters:

  • user: EtcdUserRequestForm containing the details (login, password, granted and revoked permissions, and roles) to be updated for a given user.

Returns a Future wrapped around either EtcdAddUpdateUserResponse or EtcdStandardError extending EtcdAddUpdateUserResult.

deleteUser

Deletes an existing user:

deleteUser(user: EtcdUserRequestForm): Future[EtcdDefaultResult]

Implemented according to etcd Auth and Security API: “API endpoints ”.

Parameters:

Returns a Future wrapped around either EtcdConfirmationResponse or EtcdStandardError extending EtcdConfirmationResult.

getUsers

Gets a list of users and their details:

getUsers(): Future[EtcdGetUsersResult]

Implemented according to etcd Auth and Security API: “API endpoints ”.

Returns a Future wrapped around either EtcdGetUsersResponse or EtcdStandardError extending EtcdGetUsersResult.

getUserDetails

Gets the details of a user:

getUserDetails(user: EtcdUserRequestForm): Future[EtcdGetUserDetailsResult]

Implemented according to etcd Auth and Security API: “API endpoints ”.

In this case, it suffices to provide the user field in the EtcdUserRequestForm.

Parameters:

Returns a Future wrapped around either EtcdGetUserDetailsResponse or EtcdStandardError extending EtcdGetUserDetailsResult.

addUpdateRole

Creates a role or updates an existing one:

addUpdateRole(role: EtcdRole): Future[EtcdCreateUpdateRoleResult]

Implemented according to etcd Auth and Security API: “API endpoints ”.

Parameters:

  • role: EtcdRole with details of the role to be created or updated.

Returns a Future wrapped around either EtcdAddUpdateRoleResponse or EtcdStandardError extending EtcdAddUpdateRoleResult.

deleteRole

Deletes a role:

deleteRole(role: EtcdRole): Future[EtcdDefaultResult]

Implemented according to etcd Auth and Security API: “API endpoints ”.

In this case, it suffices to provide the role field in the EtcdRole.

Parameters:

  • role: EtcdRole with details of the role to be created or updated.

Returns a Future wrapped around either EtcdConfirmationResponse or EtcdStandardError extending EtcdConfirmationResult.

getRoles

Gets a list of the existing roles and their details:

getRoles(): Future[EtcdGetRolesResult]

Implemented according to etcd Auth and Security API: “API endpoints ”.

Returns a Future wrapped around either EtcdGetRolesResponse or EtcdStandardError extending EtcdGetRolesResult.

getRoleDetails

Lists the details of a role:

getRoleDetails(role: EtcdRole): Future[EtcdGetRoleDetailsResult]

Implemented according to etcd Auth and Security API: “API endpoints ”.

In this case, it suffices to provide the role field in the EtcdRole.

Parameters:

  • role: EtcdRole with details of the role to be created or updated.

Returns a Future wrapped around either EtcdGetRoleDetailsResponse or EtcdStandardError extending EtcdGetRoleDetailsResult.

A possible work flow

The following snippets of code are meant to be read linearly. Each depends on the previous one. They present subsequent operations using EtcdClient’s authentication methods.

We first check if authentication is enabled. By default, it is disabled:

val statusQuery: Future[EtcdGetAuthenticationStatusResult] = etcdcli.getAuthenticationStatus

statusQuery onSuccess {
  case EtcdGetAuthenticationStatusResponse(clusterId, body) =>
    // should be Some(false) if authentication is not enabled
    // Some(true) otherwise.
    body.enabled
}

Next, we would like to enable authentication. In order to do so, we must first create the root user using addUpdateUser. If we try to enable authentication before this, we get an error:

val enableFail: Future[EtcdConfirmationResult] = for {
  status <- statusQuery
  enable <- etcdcli.enableAuthentication()
} yield enable

enableFail onSuccess {
  case error @ EtcdStandardError(status, clusterId, message) =>
    // when there is no root user
    // should be "auth: No root user available, please create one"
    message.message
}

The following example shows how to add the root user. First we fill an EtcdUserRequestForm:

val rootUser: EtcdUserRequestForm = EtcdUserRequestForm("root", Some("password"))

Then we can add a new user:

val rootUserCreated: Future[EtcdAddUpdateUserResult] = for {
  enable <- enableFail
  added <- etcdcli.addUpdateUser(rootUser)
} yield added

rootUserCreated onSuccess {
  case EtcdAddUpdateUserResponse(clusterId, body) =>
    // should be rootUser.user
    body.user
    // should be List("root")
    body.roles
}

We also add another user, in order to have a more involved example:

val newUser: EtcdUserRequestForm = EtcdUserRequestForm("antonio", Some("password"))

val newUserCreated: Future[EtcdAddUpdateUserResult] = for {
  rootAdded <- rootUserCreated
  added <- etcdcli.addUpdateUser(newUser)
} yield added

newUserCreated onSuccess {
  case EtcdAddUpdateUserResponse(clusterId, body) =>
    // should be newUser.user
    body.user
    // should be List()
    body.roles
}

And we create a new role, with permissions to read and write in the user space, i.e. it can add, delete and update users. First we specify the name and permissions of the new role through the EtcdRole case class, and then we perform an addUpdateRole operation:

val newRole: EtcdRole =
  EtcdRole("usersAdmin", Some(EtcdPermission(EtcdKV(List("/users/*"), List("/users/*")))))

val newRoleCreated: Future[EtcdAddUpdateRoleResult] = for {
  newUserAdded <- newUserCreated
  added <- etcdcli.addUpdateRole(newRole)
} yield added

newRoleCreated onSuccess {
  case EtcdAddUpdateRoleResponse(clusterId, body) =>
    //  should be newRole.role
    body.role
    // should be List("/users/*")
    body.permissions.get.kv.read
    // should be List("/users/*")
    body.permissions.get.kv.write
}

Once the root user has been created, we can enable authentication:

val authEnabled: Future[EtcdConfirmationResult] = for {
  added <- newRoleCreated
  enabled <- etcdcli.enableAuthentication()
} yield enabled

authEnabled onSuccess {
  // a response without a body which only contains an id found in the headers
  case EtcdConfirmationResponse(clusterID) =>
    // should be a string with an id of the member of the cluster
    clusterID.id
}

After authentication is enabled, only the root user or users with root rights can access and modify permission resources. For instance, let us try to get the details of a user without authentication:

val getUserFailed: Future[EtcdGetUserDetailsResult] = for {
  enabled <- authEnabled
  // without authorization
  // rootUser was defined in an example above
  details <- etcdcli.getUserDetails(rootUser)
} yield details

getUserFailed onSuccess {
  case error @ EtcdStandardError(status, clusterId, message) =>
    // should be "Insufficient credentials"
    message.message
}

Now, we provide some data in order to get authorization:

val rootAccess = EtcdUserAuthenticationData("root", "password")

val etcdClientRoot: EtcdClient = etcdcli.withAuthorization(rootAccess)

val getUserSuccess: Future[EtcdGetUserDetailsResult] = for {
  failed <- getUserFailed
  //with authorization
  success <- etcdClientRoot.getUserDetails(rootUser)
} yield success

getUserSuccess onSuccess {
  case EtcdGetUserDetailsResponse(clusterId, body) =>
    // should be rootUser.user
    body.user
    // should be
    // List(EtcdRole("root", Some(EtcdPermission(EtcdKV(List("/*"), List("/*")))), None, None))
    body.roles
}

The same applies to the getRoleDetails method:

val getRole: Future[EtcdGetRoleDetailsResult] = for {
  user <- getUserSuccess
  // newRole was defined in an example above
  //with authorization
  role <- etcdClientRoot.getRoleDetails(newRole)
} yield role

getRole onSuccess {
  case EtcdGetRoleDetailsResponse(clusterId, body) =>
    // should be newRole.role
    body.role
    // should be "/users/*"
    body.permissions.get.kv.read.head
}

Now let us list all users and all roles:

val listOfUsers: Future[EtcdGetUsersResult] = for {
  role <- getRole
  list <- etcdClientRoot.getUsers()
} yield list

listOfUsers onSuccess {
  case EtcdGetUsersResponse(clusterId, body) =>
    // should be "antonio"
    body.users.head.user
    // should be "root"
    body.users.tail.head.user
    // should be "root"
    body.users.tail.head.roles.head.role
    // should be "/*"
    body.users.tail.head.roles.head.permissions.get.kv.read.head
}

val listOfRoles: Future[EtcdGetRolesResult] = for {
  users <- listOfUsers
  //with authorization
  list <- etcdClientRoot.getRoles()
} yield list

listOfRoles onSuccess {
  case EtcdGetRolesResponse(clusterId, body) =>
    // should be "guest"
    body.roles.head.role
    //  should be "root"
    body.roles.tail.head.role
    // should be "/*"
    body.roles.tail.head.permissions.get.kv.read.head
}

Notice that there is a role named “guest”. The guest role defines what users can do without authentication.

Let us next update a user with a new role. Again, first we fill a EtcdUserRequestForm, and then we perform the operation:

val newUserUpdate: EtcdUserRequestForm =
  EtcdUserRequestForm("antonio", None, List(), List("usersAdmin"))

val newUserUpdated: Future[EtcdAddUpdateUserResult] = for {
  list <- listOfRoles
  updated <- etcdClientRoot.addUpdateUser(newUserUpdate)
} yield updated

newUserUpdated onSuccess {
  case EtcdAddUpdateUserResponse(clusterId, body) =>
    // should be newUser.user
    body.user
    // should be "usersAdmin"
    body.roles.head
}

We would also like to update a role. In the following example the guest role is updated; read and write permissions on the key space are revoked:

// role with a permissions to read and write in the key space
// placed in the revoke field, i.e. a request to revoke read
// and write permissions to the guest role
val guestRole: EtcdRole =
  EtcdRole("guest", None, None, Some(EtcdPermission(EtcdKV(List("/*"), List("/*")))))

val guestRoleUpdated: Future[EtcdAddUpdateRoleResult] = for {
  newUserUpdated <- newUserUpdated
  updated <- etcdClientRoot.addUpdateRole(guestRole)
} yield updated

guestRoleUpdated onSuccess {
  case EtcdAddUpdateRoleResponse(clusterId, body) =>
    // should be guestRole.role
    body.role
    // should be List()
    body.permissions.get.kv.read
    // should be List()
    body.permissions.get.kv.write
}

After such operation, it is no longer possible to make key-value operations without authentication:

val newKey = EtcdModel.key("/foo")
val newValue = EtcdModel.value("bar")

val setFailure: Future[EtcdSetKeyResult] = for {
  updated <- guestRoleUpdated
  // without the proper credentials
  failure <- etcdcli.setKey(newKey, newValue)
} yield failure

setFailure onSuccess {
  case error @ EtcdRequestError(status, headers, body) =>
    body match {
      case EtcdError(cause, errorCode, index, message) =>
        // should be 110
        errorCode
        // should be "The request requires user authentication"
        message
    }
}

val setSuccess: Future[EtcdSetKeyResult] = for {
  failure <- setFailure
  //with root access
  success <- etcdClientRoot.setKey(newKey, newValue, None, None)
} yield success

setSuccess onSuccess {
  case EtcdSetKeyResponse(headers, body) =>
    // should be "set"
    body.action
    // should be newValue
    body.node.value
}

Now, suppose we no longer have a use for a user. There is a method to delete users:

val newUserDeleted: Future[EtcdConfirmationResult] = for {
  success <- setSuccess
  // with root access
  deleted <- etcdClientRoot.deleteUser(newUser)
} yield deleted

newUserDeleted onSuccess {
  case EtcdConfirmationResponse(clusterID) =>
    clusterID.id
}

The same applies to roles:

val newRoleDeleted: Future[EtcdConfirmationResult] = for {
  userDeleted <- newUserDeleted
  // with root access
  deleted <- etcdClientRoot.deleteRole(newRole)
} yield deleted

newRoleDeleted onSuccess {
  case EtcdConfirmationResponse(clusterID) =>
    // should be a string
    clusterID.id
}

Finally, let us suppose we choose to disable authentication:

val disableAuthFailure: Future[EtcdConfirmationResult] = for {
  deleted <- newRoleDeleted
  // without the proper credentials
  failure <- etcdcli.disableAuthentication()
} yield failure

disableAuthFailure onSuccess {
  case error @ EtcdStandardError(status, clusterId, message) =>
    // should be "Insufficient credentials"
    message.message
}

val disableAuthSuccess: Future[EtcdConfirmationResult] = for {
  failure <- disableAuthFailure
  //with root access
  success <- etcdClientRoot.disableAuthentication()
} yield success

disableAuthSuccess onSuccess {
  case EtcdConfirmationResponse(clusterID) =>
    clusterID.id
    etcdClientRoot.close()
}