-
Notifications
You must be signed in to change notification settings - Fork 3.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Basic auth extension #5099
Basic auth extension #5099
Conversation
754b0b3
to
b3557b7
Compare
@jon-wei thanks for raising a new PR, but it looks that this single patch contains quite large changes. Is it possible to split into several smaller PRs? It would be much helpful for reviewers. |
f077814
to
82f2e28
Compare
I can split the PR into two if really needed, but the changes are pretty self contained in an extension with few changes to core druid. Within the extension the authentication and authorization portions are also pretty self-contained. I'd rather keep this as one PR if possible since the features are closely related and to avoid the churn/overhead of having two PRs to go through the review process. |
I'm reviewing this now. |
@jon-wei ok. I'm also reviewing. |
bc64372
to
ac419d3
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jon-wei thank you for the nice work! I did a first round of review. Also please let me know what do you think about the below.
- There is the sync issue between the coordinator and other nodes. So, an API to check the load status will be much helpful. See the lookup document as an example.
- I think credential data is usually not much big. What do you think about adding a coordinator API to sync over the whole cluster immediately?
|`druid.auth.authenticator.MyBasicAuthenticator.cacheNotificationTimeout`|The timeout in milliseconds for the cache notifications.|5000|No| | ||
|`druid.auth.authenticator.MyBasicAuthenticator.authorizerName`|Authorizer that requests should be directed to|N/A|Yes| | ||
|
||
enableCacheNotifications |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks unnecessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
deleted, thanks
|Property|Description|Default|required| | ||
|--------|-----------|-------|--------| | ||
|`druid.escalator.internalClientUsername`|The escalator will use this username for requests made as the internal systerm user.|n/a|Yes| | ||
|`druid.escalator.internalClientPassword`|The escalator will use this password for requests made as the internal system user.|n/a|Yes| |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How does this relate to druid.auth.authenticator.MyBasicAuthenticator.initialInternalClientPassword
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It should be set to the same value, unless the user changes the password later on
#### Properties | ||
|Property|Description|Default|required| | ||
|--------|-----------|-------|--------| | ||
|`druid.auth.authenticator.MyBasicAuthenticator.initialAdminPassword`|Initial password for the automatically created default admin user. If no password is specified, the default admin user will not be created.|null|No| |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the meaning of initial
? Is it possible to change the password using other APIs?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens if the password is changed and then the cluster is restarted? The adminPassword is reset to the initial one?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
initial is the password these users will have when the user is first automatically created.
The password can be changed afterwards and won't be reset after cluster restart, I added a comment about that.
|
||
Please see [Authentication and Authorization](../../configuration/auth.html) for more information on the extension interfaces being implemented. | ||
|
||
## Configuration |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should these configurations added to common.runtime.properties
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a note about common.runtime.properties
here
|Property|Description|Default|required| | ||
|--------|-----------|-------|--------| | ||
|`druid.auth.authenticator.MyBasicAuthenticator.initialAdminPassword`|Initial password for the automatically created default admin user. If no password is specified, the default admin user will not be created.|null|No| | ||
|`druid.auth.authenticator.MyBasicAuthenticator.initialInternalClientPassword`|Initial password for the default internal system user, used for internal node communication. If no password is specified, the default internal system user will not be created.|null|No| |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same with initialAdminPassword
.
public class DefaultBasicAuthorizerResourceHandler implements BasicAuthorizerResourceHandler | ||
{ | ||
private static final Logger log = new Logger(DefaultBasicAuthorizerResourceHandler.class); | ||
private static final Response NOT_FOUND_RESPONSE = Response.status(Response.Status.NOT_FOUND).build(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe FORBIDDEN
this.resourceNamePattern = Pattern.compile(resourceAction.getResource().getName()); | ||
} | ||
catch (PatternSyntaxException pse) { | ||
throw new BasicSecurityDBResourceException( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pse
should be included.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Included exception
|
||
BasicAuthorizerPermission that = (BasicAuthorizerPermission) o; | ||
|
||
if (getResourceAction() != null |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can use Objects.equals()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was autogenerated by IntelliJ
public int hashCode() | ||
{ | ||
int result = getResourceAction() != null ? getResourceAction().hashCode() : 0; | ||
result = 31 * result + (getResourceNamePattern().pattern() != null |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can use Objects.hash()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also autogenerated, they're not used outside of tests
connector = derbyConnectorRule.getConnector(); | ||
tablesConfig = derbyConnectorRule.metadataTablesConfigSupplier().get(); | ||
connector.createConfigTable(); | ||
//injector = setupInjector(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please remove this line.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
removed line
Thanks @jon-wei for working on the extension and @jihoonson for reviewing. Beyond what @jihoonson has said (especially an API to check the load status -- it's always helpful) I think we should cache the credential db on the local disk of each node, so when they start up, they can use their cache if the coordinator is not accessible. Maybe they should use their cache if they can't contact the coordinator within some amount of time. That way the coordinator being down doesn't torch the cluster completely. |
1301600
to
c937dc8
Compare
Thanks for the reviews, I addressed the comments and added the following features:
|
c937dc8
to
4f762cd
Compare
4f762cd
to
1b1f022
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jon-wei thanks for the update. Left more comments.
return objectMapper.writeValueAsBytes(userMap); | ||
} | ||
catch (IOException ioe) { | ||
throw new ISE("WTF? Couldn't serialize userMap!"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add ioe
to the exception.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added
return objectMapper.writeValueAsBytes(userMap); | ||
} | ||
catch (IOException ioe) { | ||
throw new ISE("WTF? Couldn't serialize userMap!"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add ioe
to the exception.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added
return objectMapper.writeValueAsBytes(roleMap); | ||
} | ||
catch (IOException ioe) { | ||
throw new ISE("WTF? Couldn't serialize roleMap!"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add ioe
to the exception.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
added
private final EmittingLogger logger; | ||
private final String threadName; | ||
|
||
private Thread notifierThread; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thread should be stopped on stop(). Suggest to use ExecutorService for easy maintenance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added stop(), changed to use ExecutorService
synchronized (itemsToUpdate) { | ||
itemsToUpdate.add(updatedItemName); | ||
serializedMaps.put(updatedItemName, updatedItemData); | ||
itemsToUpdate.notify(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks to the notifier notifies each update every time. Maybe it's better to collect updates for a given time (like 100 ms) and send them in bulk to reduce API calls. I'm not sure about this because I guess credentials are not frequently updated.
If you want to keep the current behavior, we don't have to keep itemsToUpdate and serializedMaps because there will be only a single element in them. Instead, I think it's better to use AtomicReference<Pair<String, byte[]>>
or simply put a Pair<String, byte[]>
to the blockingQueue if you decide to use it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the current behavior is okay for now, since the updates should be pretty infrequent, I changed this to use Pair<String, byte[]> and a BlockingQueue
) | ||
{ | ||
this.storageUpdater = storageUpdater; | ||
this.objectMapper = new ObjectMapper(new SmileFactory()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks that objectMapper can be injected using @Smile
annotation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changed to use @Smile
annotation
} | ||
} | ||
catch (Exception e) { | ||
LOG.makeAlert(e, "WTF? Could not deserialize user/role map received from coordinator.").emit(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this intended to does nothing for all kinds of exceptions but emitting an alert?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, see the previous comment
} | ||
|
||
@Nullable | ||
private UserAndRoleMap loadUserAndRoleMapFromDisk(String prefix) throws Exception |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can be reduced to throws IOException
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reduced to IOException
); | ||
} | ||
|
||
private void writeMapToDisk(String prefix, byte[] userMapBytes) throws Exception |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can be reduced to IOException.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reduced to IOException
File cacheDir = new File(commonCacheConfig.getCacheDirectory()); | ||
cacheDir.mkdirs(); | ||
File userMapFile = new File(commonCacheConfig.getCacheDirectory(), getUserRoleMapFilename(prefix)); | ||
Files.write(userMapBytes, userMapFile); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similar comment. What if the write fails?
12e5a0e
to
a8ae159
Compare
a8ae159
to
08a4e8f
Compare
private final Map<String, BasicAuthDBConfig> itemConfigMap; | ||
private final String baseUrl; | ||
private final EmittingLogger logger; | ||
private final String threadName; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused variable.
HttpClient httpClient, | ||
String baseUrl, | ||
String threadName, | ||
EmittingLogger logger |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah it's needed. But I'm still concerned with that it's difficult to find where the logging code is from the log if the logging class is different from the actual class. What do you think about passing a caller name in constructor and adding it to all logs?
return loadUserMapFromDisk(prefix); | ||
} | ||
catch (Exception e2) { | ||
LOG.makeAlert(e2, "Encountered exception while loading user map snapshot for authenticator [%s]", prefix); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add e
to e2
like e2.addSuppressed(e)
.
return loadUserAndRoleMapFromDisk(prefix); | ||
} | ||
catch (Exception e2) { | ||
LOG.makeAlert(e2, "Encountered exception while loading user-role map snapshot for authorizer [%s]", prefix); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add e
to e2
.
d81f604
to
895ce3e
Compare
@jon-wei thanks for the quick update! +1 after CI. |
@jihoonson thanks for the review! |
* Basic auth extension * Add auth configuration integration test * Fix missing authorizerName property * PR comments * Fix missing @JsonProperty annotation * PR comments * more PR comments
{ | ||
private static final EmittingLogger LOG = new EmittingLogger(CommonCacheNotifier.class); | ||
|
||
private static final List<String> NODE_TYPES = Arrays.asList( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jon-wei does it purposely skip the Coordinator type?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@leventov Yes, the coordinator gets its information about the auth state directly from metadata storage, this update notification is for other service types
Why basic security can't use EnvironmentVariablePasswordProvider, such as initialAdminPassword? |
@zhouyuanchao It can, check the master branch |
@jon-wei internalClientPassword can not use EnvironmentVariablePasswordProvider in master branch |
This PR adds an extension that provides implementations of the auth interfaces introduced in #4271 :
With the large amount of changes (almost a rewrite) from the last round of PR reviews on #4823, I felt like it would be easier to close the old PR and start a fresh one.
Some design notes: