Skip to content
This repository was archived by the owner on Feb 17, 2022. It is now read-only.

Commit e11ed5b

Browse files
committed
Add a flag to configure Shiro at runtime.
Testing Done: ./gradlew -Pq build Bugs closed: AURORA-809 Reviewed at https://reviews.apache.org/r/32055/
1 parent 7d68df3 commit e11ed5b

File tree

5 files changed

+230
-21
lines changed

5 files changed

+230
-21
lines changed

src/main/java/org/apache/aurora/scheduler/http/api/security/ApiSecurityModule.java

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,13 @@
1313
*/
1414
package org.apache.aurora.scheduler.http.api.security;
1515

16+
import java.util.Set;
17+
1618
import javax.inject.Singleton;
1719

1820
import com.google.common.annotations.VisibleForTesting;
19-
import com.google.common.base.Optional;
21+
import com.google.common.collect.ImmutableSet;
22+
import com.google.inject.Module;
2023
import com.google.inject.Provides;
2124
import com.google.inject.matcher.Matchers;
2225
import com.google.inject.name.Names;
@@ -31,13 +34,14 @@
3134
import org.apache.aurora.gen.AuroraSchedulerManager;
3235
import org.apache.aurora.scheduler.http.api.ApiModule;
3336
import org.apache.shiro.SecurityUtils;
34-
import org.apache.shiro.config.Ini;
3537
import org.apache.shiro.guice.aop.ShiroAopModule;
3638
import org.apache.shiro.guice.web.ShiroWebModule;
3739
import org.apache.shiro.realm.text.IniRealm;
3840
import org.apache.shiro.subject.Subject;
3941
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
4042

43+
import static java.util.Objects.requireNonNull;
44+
4145
/**
4246
* Provides HTTP Basic Authentication for the API using Apache Shiro. When enabled, prevents
4347
* unauthenticated access to write APIs. Write API access must also be authorized, with permissions
@@ -51,25 +55,26 @@ public class ApiSecurityModule extends ServletModule {
5155
help = "Enable security for the Thrift API (beta).")
5256
private static final Arg<Boolean> ENABLE_API_SECURITY = Arg.create(false);
5357

54-
@CmdLine(name = "shiro_ini_path",
55-
help = "Path to shiro.ini for authentication and authorization configuration.")
56-
private static final Arg<Ini> SHIRO_INI_PATH = Arg.create(null);
58+
@CmdLine(name = "shiro_realm_modules",
59+
help = "Guice modules for configuring Shiro Realms.")
60+
private static final Arg<Set<Module>> SHIRO_REALM_MODULE =
61+
Arg.<Set<Module>>create(ImmutableSet.<Module>of(new IniShiroRealmModule()));
5762

5863
private final boolean enableApiSecurity;
59-
private final Optional<Ini> shiroIni;
64+
private final Set<Module> shiroConfigurationModules;
6065

6166
public ApiSecurityModule() {
62-
this(ENABLE_API_SECURITY.get(), Optional.fromNullable(SHIRO_INI_PATH.get()));
67+
this(ENABLE_API_SECURITY.get(), SHIRO_REALM_MODULE.get());
6368
}
6469

6570
@VisibleForTesting
66-
ApiSecurityModule(Ini shiroIni) {
67-
this(true, Optional.of(shiroIni));
71+
ApiSecurityModule(Module shiroConfigurationModule) {
72+
this(true, ImmutableSet.of(shiroConfigurationModule));
6873
}
6974

70-
private ApiSecurityModule(boolean enableApiSecurity, Optional<Ini> shiroIni) {
75+
private ApiSecurityModule(boolean enableApiSecurity, Set<Module> shiroConfigurationModules) {
7176
this.enableApiSecurity = enableApiSecurity;
72-
this.shiroIni = shiroIni;
77+
this.shiroConfigurationModules = requireNonNull(shiroConfigurationModules);
7378
}
7479

7580
@Override
@@ -85,10 +90,10 @@ private void doConfigureServlets() {
8590
@Override
8691
@SuppressWarnings("unchecked")
8792
protected void configureShiroWeb() {
88-
try {
89-
bindRealm().toConstructor(IniRealm.class.getConstructor(Ini.class));
90-
} catch (NoSuchMethodException e) {
91-
addError(e);
93+
for (Module module : shiroConfigurationModules) {
94+
// We can't wrap this in a PrivateModule because Guice Multibindings don't work with them
95+
// and we need a Set<Realm>.
96+
install(module);
9297
}
9398

9499
addFilterChain("/**",
@@ -97,11 +102,6 @@ protected void configureShiroWeb() {
97102
}
98103
});
99104

100-
if (shiroIni.isPresent()) {
101-
bind(Ini.class).toInstance(shiroIni.get());
102-
} else {
103-
addError("shiro.ini is required.");
104-
}
105105
bind(IniRealm.class).in(Singleton.class);
106106
bindConstant().annotatedWith(Names.named("shiro.applicationName")).to(HTTP_REALM_NAME);
107107

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
*
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
package org.apache.aurora.scheduler.http.api.security;
15+
16+
import com.google.common.annotations.VisibleForTesting;
17+
import com.google.common.base.Optional;
18+
import com.google.inject.AbstractModule;
19+
import com.google.inject.multibindings.Multibinder;
20+
import com.twitter.common.args.Arg;
21+
import com.twitter.common.args.CmdLine;
22+
23+
import org.apache.shiro.config.Ini;
24+
import org.apache.shiro.realm.Realm;
25+
import org.apache.shiro.realm.text.IniRealm;
26+
27+
/**
28+
* Provides an implementation of a Shiro {@link IniRealm} that uses a flat shiro.ini file for
29+
* authentication and authorization. Should be used in conjunction with the
30+
* {@link org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter} or other filter that
31+
* produces {@link org.apache.shiro.authc.UsernamePasswordToken}s.
32+
*/
33+
public class IniShiroRealmModule extends AbstractModule {
34+
@CmdLine(name = "shiro_ini_path",
35+
help = "Path to shiro.ini for authentication and authorization configuration.")
36+
private static final Arg<Ini> SHIRO_INI_PATH = Arg.create(null);
37+
38+
private final Optional<Ini> ini;
39+
40+
public IniShiroRealmModule() {
41+
this(Optional.fromNullable(SHIRO_INI_PATH.get()));
42+
}
43+
44+
@VisibleForTesting
45+
IniShiroRealmModule(Ini ini) {
46+
this(Optional.of(ini));
47+
}
48+
49+
private IniShiroRealmModule(Optional<Ini> ini) {
50+
this.ini = ini;
51+
}
52+
53+
@Override
54+
protected void configure() {
55+
if (ini.isPresent()) {
56+
bind(Ini.class).toInstance(ini.get());
57+
} else {
58+
addError("shiro.ini is required.");
59+
}
60+
61+
try {
62+
Multibinder.newSetBinder(binder(), Realm.class).addBinding()
63+
.toConstructor(IniRealm.class.getConstructor(Ini.class));
64+
} catch (NoSuchMethodException e) {
65+
addError(e);
66+
}
67+
}
68+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/**
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
*
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
package org.apache.aurora.scheduler.http.api.security;
15+
16+
import java.lang.reflect.Constructor;
17+
import java.lang.reflect.InvocationTargetException;
18+
19+
import com.google.inject.Module;
20+
import com.twitter.common.args.ArgParser;
21+
import com.twitter.common.args.parsers.NonParameterizedTypeParser;
22+
23+
/**
24+
* ArgParser for Guice modules. Constructs an instance of a Module with a given FQCN if it has a
25+
* public no-args constructor.
26+
*/
27+
@ArgParser
28+
public class ModuleParser extends NonParameterizedTypeParser<Module> {
29+
@Override
30+
public Module doParse(String raw) throws IllegalArgumentException {
31+
Class<?> rawClass;
32+
try {
33+
rawClass = Class.forName(raw);
34+
} catch (ClassNotFoundException e) {
35+
throw new IllegalArgumentException(e);
36+
}
37+
38+
if (!Module.class.isAssignableFrom(rawClass)) {
39+
throw new IllegalArgumentException(
40+
"Class " + raw + " must implement " + Module.class.getName());
41+
}
42+
@SuppressWarnings("unchecked")
43+
Class<? extends Module> moduleClass = (Class<? extends Module>) rawClass;
44+
45+
Constructor<? extends Module> moduleConstructor;
46+
try {
47+
moduleConstructor = moduleClass.getConstructor();
48+
} catch (NoSuchMethodException e) {
49+
throw new IllegalArgumentException(
50+
"Module " + raw + " must have a public no-args constructor.",
51+
e);
52+
}
53+
54+
try {
55+
return moduleConstructor.newInstance();
56+
} catch (InvocationTargetException | InstantiationException | IllegalAccessException e) {
57+
throw new IllegalArgumentException(e);
58+
}
59+
}
60+
}

src/test/java/org/apache/aurora/scheduler/http/api/security/ApiSecurityIT.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ public void setUp() {
106106
protected Module getChildServletModule() {
107107
return Modules.combine(
108108
new ApiModule(),
109-
new ApiSecurityModule(ini),
109+
new ApiSecurityModule(new IniShiroRealmModule(ini)),
110110
new AbstractModule() {
111111
@Override
112112
protected void configure() {
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/**
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
*
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
package org.apache.aurora.scheduler.http.api.security;
15+
16+
import com.google.inject.Binder;
17+
import com.google.inject.Module;
18+
19+
import org.junit.Test;
20+
21+
public class ModuleParserTest {
22+
static class NoOpModule implements Module {
23+
public NoOpModule() {
24+
// No-op.
25+
}
26+
27+
@Override
28+
public void configure(Binder binder) {
29+
// No-op.
30+
}
31+
}
32+
33+
static class NoNullaryConstructorModule implements Module {
34+
private String name;
35+
36+
NoNullaryConstructorModule(String name) {
37+
this.name = name;
38+
}
39+
40+
@Override
41+
public void configure(Binder binder) {
42+
binder.bind(String.class).toInstance(name);
43+
}
44+
}
45+
46+
static class ThrowingConstructorModule implements Module {
47+
ThrowingConstructorModule() {
48+
throw new UnsupportedOperationException();
49+
}
50+
51+
@Override
52+
public void configure(Binder binder) {
53+
// Unreachable.
54+
}
55+
}
56+
57+
@Test
58+
public void testDoParseSuccess() {
59+
new ModuleParser().doParse(NoOpModule.class.getName());
60+
}
61+
62+
@Test(expected = IllegalArgumentException.class)
63+
public void testDoParseClassNotFound() {
64+
new ModuleParser().doParse("invalid.class.name");
65+
}
66+
67+
@Test(expected = IllegalArgumentException.class)
68+
public void testDoParseNotModule() {
69+
new ModuleParser().doParse(String.class.getCanonicalName());
70+
}
71+
72+
@Test(expected = IllegalArgumentException.class)
73+
public void testDoParseNoNullaryConstructor() {
74+
new ModuleParser().doParse(NoNullaryConstructorModule.class.getCanonicalName());
75+
}
76+
77+
@Test(expected = IllegalArgumentException.class)
78+
public void testDoParseThrowingConstructorModule() {
79+
new ModuleParser().doParse(ThrowingConstructorModule.class.getCanonicalName());
80+
}
81+
}

0 commit comments

Comments
 (0)