-
-
Notifications
You must be signed in to change notification settings - Fork 420
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[REST Auth] API tokens & openhab:users console command
This adds API tokens as a new credential type. Their format is: `oh.<name>.<random chars>` The "oh." prefix is used to tell them apart from a JWT access token, because they're both used as a Bearer authorization scheme, but there is no semantic value attached to any of the other parts. They are stored hashed in the user's profile, and can be listed, added or removed managed with the new `openhab:users` console command. Currently the scopes are still not checked, but ultimately they could be, for instance a scope of e.g. `user admin.items` would mean that the API token can be used to perform user operations like retrieving info or sending a command, _and_ managing the items, but nothing else - even if the user has more permissions because of their role (which will of course still be checked). Tokens are normally passed in the Authorization header with the Bearer scheme, or the X-OPENHAB-TOKEN header, like access tokens. As a special exception, API tokens can also be used with the Basic authorization scheme, **even if the allowBasicAuth** option is not enabled in the "API Security" service, because there's no additional security risk in allowing that. In that case, the token should be passed as the username and the password MUST be empty. In short, this means that all these curl commands will work: - `curl -H 'Authorization: Bearer <token>' http://localhost:8080/rest/inbox` - `curl -H 'X-OPENHAB-TOKEN: <token>' http://localhost:8080/rest/inbox` - `curl -u '<token>[:]' http://localhost:8080/rest/inbox` - `curl http://<token>@localhost:8080/rest/inbox` 2 REST API operations were adding to the AuthResource, to allow authenticated users to list their tokens or remove (revoke) one. Self-service for creating a token or changing the password is more sensitive so these should be handled with a servlet and pages devoid of any JavaScript instead of REST API calls, therefore for now they'll have to be done with the console. This also fixes regressions introduced with #1713 - the operations annotated with @RolesAllowed({ Role.USER }) only were not authorized for administrators anymore. Signed-off-by: Yannick Schaus <github@schaus.net>
- Loading branch information
Showing
14 changed files
with
577 additions
and
45 deletions.
There are no files selected for viewing
197 changes: 197 additions & 0 deletions
197
...main/java/org/openhab/core/io/console/internal/extension/UserConsoleCommandExtension.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,197 @@ | ||
/** | ||
* Copyright (c) 2010-2020 Contributors to the openHAB project | ||
* | ||
* See the NOTICE file(s) distributed with this work for additional | ||
* information. | ||
* | ||
* This program and the accompanying materials are made available under the | ||
* terms of the Eclipse Public License 2.0 which is available at | ||
* http://www.eclipse.org/legal/epl-2.0 | ||
* | ||
* SPDX-License-Identifier: EPL-2.0 | ||
*/ | ||
package org.openhab.core.io.console.internal.extension; | ||
|
||
import java.util.Arrays; | ||
import java.util.List; | ||
import java.util.Optional; | ||
import java.util.Set; | ||
|
||
import org.eclipse.jdt.annotation.NonNullByDefault; | ||
import org.openhab.core.auth.ManagedUser; | ||
import org.openhab.core.auth.User; | ||
import org.openhab.core.auth.UserApiToken; | ||
import org.openhab.core.auth.UserRegistry; | ||
import org.openhab.core.io.console.Console; | ||
import org.openhab.core.io.console.extensions.AbstractConsoleCommandExtension; | ||
import org.openhab.core.io.console.extensions.ConsoleCommandExtension; | ||
import org.osgi.service.component.annotations.Activate; | ||
import org.osgi.service.component.annotations.Component; | ||
import org.osgi.service.component.annotations.Reference; | ||
|
||
/** | ||
* Console command extension to manage users, sessions and API tokens | ||
* | ||
* @author Yannick Schaus - Initial contribution | ||
*/ | ||
@Component(service = ConsoleCommandExtension.class) | ||
@NonNullByDefault | ||
public class UserConsoleCommandExtension extends AbstractConsoleCommandExtension { | ||
|
||
private static final String SUBCMD_LIST = "list"; | ||
private static final String SUBCMD_ADD = "add"; | ||
private static final String SUBCMD_REMOVE = "remove"; | ||
private static final String SUBCMD_CHANGEPASSWORD = "changePassword"; | ||
private static final String SUBCMD_LISTAPITOKENS = "listApiTokens"; | ||
private static final String SUBCMD_ADDAPITOKEN = "addApiToken"; | ||
private static final String SUBCMD_RMAPITOKEN = "rmApiToken"; | ||
private static final String SUBCMD_CLEARSESSIONS = "clearSessions"; | ||
|
||
private final UserRegistry userRegistry; | ||
|
||
@Activate | ||
public UserConsoleCommandExtension(final @Reference UserRegistry userRegistry) { | ||
super("users", "Access the user registry."); | ||
this.userRegistry = userRegistry; | ||
} | ||
|
||
@Override | ||
public List<String> getUsages() { | ||
return Arrays.asList(new String[] { buildCommandUsage(SUBCMD_LIST, "lists all users"), | ||
buildCommandUsage(SUBCMD_ADD + " <userId> <password> <role>", | ||
"adds a new user with the specified role"), | ||
buildCommandUsage(SUBCMD_REMOVE + " <userId>", "removes the given user"), | ||
buildCommandUsage(SUBCMD_CHANGEPASSWORD + " <userId> <newPassword>", "changes the password of a user"), | ||
buildCommandUsage(SUBCMD_LISTAPITOKENS, "lists the API keys for all users"), | ||
buildCommandUsage(SUBCMD_ADDAPITOKEN + " <userId> <tokenName> <scope>", | ||
"adds a new API token on behalf of the specified user for the specified scope"), | ||
buildCommandUsage(SUBCMD_RMAPITOKEN + " <userId> <tokenName>", | ||
"removes (revokes) the specified API token"), | ||
buildCommandUsage(SUBCMD_CLEARSESSIONS + " <userId>", | ||
"clear the refresh tokens associated with the user (will sign the user out of all sessions)") }); | ||
} | ||
|
||
@Override | ||
public void execute(String[] args, Console console) { | ||
if (args.length > 0) { | ||
String subCommand = args[0]; | ||
switch (subCommand) { | ||
case SUBCMD_LIST: | ||
userRegistry.getAll().forEach(user -> console.println(user.toString())); | ||
break; | ||
case SUBCMD_ADD: | ||
if (args.length == 4) { | ||
User existingUser = userRegistry.get(args[1]); | ||
if (existingUser == null) { | ||
User newUser = userRegistry.register(args[1], args[2], Set.of(args[3])); | ||
console.println(newUser.toString()); | ||
console.println("User created."); | ||
} else { | ||
console.println("The user already exists."); | ||
} | ||
} else { | ||
console.printUsage(findUsage(SUBCMD_ADD)); | ||
} | ||
break; | ||
case SUBCMD_REMOVE: | ||
if (args.length == 2) { | ||
User user = userRegistry.get(args[1]); | ||
if (user != null) { | ||
userRegistry.remove(user.getName()); | ||
console.println("User removed."); | ||
} else { | ||
console.println("User not found."); | ||
} | ||
} else { | ||
console.printUsage(findUsage(SUBCMD_REMOVE)); | ||
} | ||
break; | ||
case SUBCMD_CHANGEPASSWORD: | ||
if (args.length == 3) { | ||
User user = userRegistry.get(args[1]); | ||
if (user != null) { | ||
userRegistry.changePassword(user, args[2]); | ||
console.println("Password changed."); | ||
} else { | ||
console.println("User not found."); | ||
} | ||
} else { | ||
console.printUsage(findUsage(SUBCMD_CHANGEPASSWORD)); | ||
} | ||
break; | ||
case SUBCMD_LISTAPITOKENS: | ||
userRegistry.getAll().forEach(user -> { | ||
ManagedUser managedUser = (ManagedUser) user; | ||
if (managedUser.getApiTokens().size() > 0) { | ||
managedUser.getApiTokens().forEach(t -> { | ||
console.println("user=" + user.toString() + ", " + t.toString()); | ||
}); | ||
} | ||
}); | ||
break; | ||
case SUBCMD_ADDAPITOKEN: | ||
if (args.length == 4) { | ||
ManagedUser user = (ManagedUser) userRegistry.get(args[1]); | ||
if (user != null) { | ||
Optional<UserApiToken> userApiToken = user.getApiTokens().stream() | ||
.filter(t -> args[2].equals(t.getName())).findAny(); | ||
if (userApiToken.isEmpty()) { | ||
String tokenString = userRegistry.addUserApiToken(user, args[2], args[3]); | ||
// inform the user that the token will not be printed again, and they should save it? | ||
console.println(tokenString); | ||
} else { | ||
console.println("Cannot create API token: another one with the same name was found."); | ||
} | ||
} else { | ||
console.println("User not found."); | ||
} | ||
} else { | ||
console.printUsage(findUsage(SUBCMD_ADDAPITOKEN)); | ||
} | ||
break; | ||
case SUBCMD_RMAPITOKEN: | ||
if (args.length == 3) { | ||
ManagedUser user = (ManagedUser) userRegistry.get(args[1]); | ||
if (user != null) { | ||
Optional<UserApiToken> userApiToken = user.getApiTokens().stream() | ||
.filter(t -> args[2].equals(t.getName())).findAny(); | ||
if (userApiToken.isPresent()) { | ||
userRegistry.removeUserApiToken(user, userApiToken.get()); | ||
console.println("API token revoked."); | ||
} else { | ||
console.println("No matching API token found."); | ||
} | ||
} else { | ||
console.println("User not found."); | ||
} | ||
} else { | ||
console.printUsage(findUsage(SUBCMD_RMAPITOKEN)); | ||
} | ||
break; | ||
case SUBCMD_CLEARSESSIONS: | ||
if (args.length == 2) { | ||
User user = userRegistry.get(args[1]); | ||
if (user != null) { | ||
userRegistry.clearSessions(user); | ||
console.println("User sessions cleared."); | ||
} else { | ||
console.println("User not found."); | ||
} | ||
} else { | ||
console.printUsage(findUsage(SUBCMD_CLEARSESSIONS)); | ||
} | ||
break; | ||
default: | ||
console.println("Unknown command '" + subCommand + "'"); | ||
printUsage(console); | ||
break; | ||
} | ||
} else { | ||
printUsage(console); | ||
} | ||
} | ||
|
||
private String findUsage(String cmd) { | ||
return getUsages().stream().filter(u -> u.contains(cmd)).findAny().get(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.