Skip to content

Commit 37e7cd0

Browse files
chen-andersmreiferson
authored andcommitted
nsqadmin: X-Forwarded-User based ACL
1 parent 6c64ce0 commit 37e7cd0

File tree

13 files changed

+69
-8
lines changed

13 files changed

+69
-8
lines changed

apps/nsqadmin/main.go

+3
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,17 @@ var (
4747
httpClientTLSKey = flagSet.String("http-client-tls-key", "", "path to key file for the HTTP client")
4848

4949
allowConfigFromCIDR = flagSet.String("allow-config-from-cidr", "127.0.0.1/8", "A CIDR from which to allow HTTP requests to the /config endpoint")
50+
aclHttpHeader = flagSet.String("acl-http-header", "X-Forwarded-User", "HTTP header to check for authenticated admin users")
5051

52+
adminUsers = app.StringArray{}
5153
nsqlookupdHTTPAddresses = app.StringArray{}
5254
nsqdHTTPAddresses = app.StringArray{}
5355
)
5456

5557
func init() {
5658
flagSet.Var(&nsqlookupdHTTPAddresses, "lookupd-http-address", "lookupd HTTP address (may be given multiple times)")
5759
flagSet.Var(&nsqdHTTPAddresses, "nsqd-http-address", "nsqd HTTP address (may be given multiple times)")
60+
flagSet.Var(&adminUsers, "admin-user", "admin user (may be given multiple times; if specified, only these users will be able to perform privileged actions; acl-http-header is used to determine the authenticated user)")
5861
}
5962

6063
func main() {

nsqadmin/http.go

+35
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ func (s *httpServer) indexHandler(w http.ResponseWriter, req *http.Request, ps h
134134
StatsdGaugeFormat string
135135
StatsdPrefix string
136136
NSQLookupd []string
137+
IsAdmin bool
137138
}{
138139
Version: version.Binary,
139140
ProxyGraphite: s.ctx.nsqadmin.getOpts().ProxyGraphite,
@@ -144,6 +145,7 @@ func (s *httpServer) indexHandler(w http.ResponseWriter, req *http.Request, ps h
144145
StatsdGaugeFormat: s.ctx.nsqadmin.getOpts().StatsdGaugeFormat,
145146
StatsdPrefix: s.ctx.nsqadmin.getOpts().StatsdPrefix,
146147
NSQLookupd: s.ctx.nsqadmin.getOpts().NSQLookupdHTTPAddresses,
148+
IsAdmin: s.isAuthorizedAdminRequest(req),
147149
})
148150

149151
return nil, nil
@@ -420,6 +422,11 @@ func (s *httpServer) createTopicChannelHandler(w http.ResponseWriter, req *http.
420422
Topic string `json:"topic"`
421423
Channel string `json:"channel"`
422424
}
425+
426+
if !s.isAuthorizedAdminRequest(req) {
427+
return nil, http_api.Err{403, "FORBIDDEN"}
428+
}
429+
423430
err := json.NewDecoder(req.Body).Decode(&body)
424431
if err != nil {
425432
return nil, http_api.Err{400, err.Error()}
@@ -458,6 +465,10 @@ func (s *httpServer) createTopicChannelHandler(w http.ResponseWriter, req *http.
458465
func (s *httpServer) deleteTopicHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) (interface{}, error) {
459466
var messages []string
460467

468+
if !s.isAuthorizedAdminRequest(req) {
469+
return nil, http_api.Err{403, "FORBIDDEN"}
470+
}
471+
461472
topicName := ps.ByName("topic")
462473

463474
err := s.ci.DeleteTopic(topicName,
@@ -483,6 +494,10 @@ func (s *httpServer) deleteTopicHandler(w http.ResponseWriter, req *http.Request
483494
func (s *httpServer) deleteChannelHandler(w http.ResponseWriter, req *http.Request, ps httprouter.Params) (interface{}, error) {
484495
var messages []string
485496

497+
if !s.isAuthorizedAdminRequest(req) {
498+
return nil, http_api.Err{403, "FORBIDDEN"}
499+
}
500+
486501
topicName := ps.ByName("topic")
487502
channelName := ps.ByName("channel")
488503

@@ -523,6 +538,11 @@ func (s *httpServer) topicChannelAction(req *http.Request, topicName string, cha
523538
var body struct {
524539
Action string `json:"action"`
525540
}
541+
542+
if !s.isAuthorizedAdminRequest(req) {
543+
return nil, http_api.Err{403, "FORBIDDEN"}
544+
}
545+
526546
err := json.NewDecoder(req.Body).Decode(&body)
527547
if err != nil {
528548
return nil, http_api.Err{400, err.Error()}
@@ -755,6 +775,21 @@ func (s *httpServer) doConfig(w http.ResponseWriter, req *http.Request, ps httpr
755775
return v, nil
756776
}
757777

778+
func (s *httpServer) isAuthorizedAdminRequest(req *http.Request) bool {
779+
adminUsers := s.ctx.nsqadmin.getOpts().AdminUsers
780+
if len(adminUsers) == 0 {
781+
return true
782+
}
783+
aclHttpHeader := s.ctx.nsqadmin.getOpts().AclHttpHeader
784+
user := req.Header.Get(aclHttpHeader)
785+
for _, v := range adminUsers {
786+
if v == user {
787+
return true
788+
}
789+
}
790+
return false
791+
}
792+
758793
func getOptByCfgName(opts interface{}, name string) (interface{}, bool) {
759794
val := reflect.ValueOf(opts).Elem()
760795
typ := val.Type()

nsqadmin/http_test.go

+7
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ func mustStartNSQLookupd(opts *nsqlookupd.Options) (*net.TCPAddr, *net.TCPAddr,
5555
}
5656

5757
func bootstrapNSQCluster(t *testing.T) (string, []*nsqd.NSQD, []*nsqlookupd.NSQLookupd, *NSQAdmin) {
58+
return bootstrapNSQClusterWithAuth(t, false)
59+
}
60+
61+
func bootstrapNSQClusterWithAuth(t *testing.T, withAuth bool) (string, []*nsqd.NSQD, []*nsqlookupd.NSQLookupd, *NSQAdmin) {
5862
lgr := test.NewTestLogger(t)
5963

6064
nsqlookupdOpts := nsqlookupd.NewOptions()
@@ -85,6 +89,9 @@ func bootstrapNSQCluster(t *testing.T) (string, []*nsqd.NSQD, []*nsqlookupd.NSQL
8589
nsqadminOpts.HTTPAddress = "127.0.0.1:0"
8690
nsqadminOpts.NSQLookupdHTTPAddresses = []string{nsqlookupd1.RealHTTPAddr().String()}
8791
nsqadminOpts.Logger = lgr
92+
if withAuth {
93+
nsqadminOpts.AdminUsers = []string{"matt"}
94+
}
8895
nsqadmin1 := New(nsqadminOpts)
8996
go nsqadmin1.Main()
9097

nsqadmin/options.go

+5
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ type Options struct {
3838
AllowConfigFromCIDR string `flag:"allow-config-from-cidr"`
3939

4040
NotificationHTTPEndpoint string `flag:"notification-http-endpoint"`
41+
42+
AclHttpHeader string `flag:"acl-http-header"`
43+
AdminUsers []string `flag:"admin-user" cfg:"admin_users"`
4144
}
4245

4346
func NewOptions() *Options {
@@ -52,5 +55,7 @@ func NewOptions() *Options {
5255
HTTPClientConnectTimeout: 2 * time.Second,
5356
HTTPClientRequestTimeout: 5 * time.Second,
5457
AllowConfigFromCIDR: "127.0.0.1/8",
58+
AclHttpHeader: "X-Forwarded-User",
59+
AdminUsers: []string{},
5560
}
5661
}

nsqadmin/static/html/index.html

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
var STATSD_INTERVAL = {{.StatsdInterval}};
2424
var STATSD_PREFIX = '{{.StatsdPrefix}}';
2525
var NSQLOOKUPD = [{{range .NSQLookupd}}'{{.}}',{{end}}];
26+
var IS_ADMIN ={{.IsAdmin}};
2627
</script>
2728
<script src="/static/vendor.js"></script>
2829
<script src="/static/main.js"></script>

nsqadmin/static/js/app_state.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ var AppState = Backbone.Model.extend({
1212
'STATSD_GAUGE_FORMAT': STATSD_GAUGE_FORMAT,
1313
'STATSD_PREFIX': STATSD_PREFIX,
1414
'NSQLOOKUPD': NSQLOOKUPD,
15-
'graph_interval': '2h'
15+
'graph_interval': '2h',
16+
'IS_ADMIN': IS_ADMIN
1617
};
1718
},
1819

nsqadmin/static/js/views/app.js

+3-4
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,6 @@ var AppView = BaseView.extend({
7373
.fail(function() { $el.html('ERROR'); });
7474
});
7575
});
76-
7776
this.render();
7877
},
7978

@@ -98,21 +97,21 @@ var AppView = BaseView.extend({
9897

9998
showTopic: function(topic) {
10099
this.showView(function() {
101-
var model = new Topic({'name': topic});
100+
var model = new Topic({'name': topic, 'isAdmin': AppState.get('IS_ADMIN')});
102101
return new TopicView({'model': model});
103102
});
104103
},
105104

106105
showChannel: function(topic, channel) {
107106
this.showView(function() {
108-
var model = new Channel({'topic': topic, 'name': channel});
107+
var model = new Channel({'topic': topic, 'name': channel, 'isAdmin': AppState.get('IS_ADMIN')});
109108
return new ChannelView({'model': model});
110109
});
111110
},
112111

113112
showLookup: function() {
114113
this.showView(function() {
115-
return new LookupView();
114+
return new LookupView({'isAdmin': AppState.get('IS_ADMIN')});
116115
});
117116
},
118117

nsqadmin/static/js/views/channel.hbs

+2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
</div>
2727
</div>
2828
{{else}}
29+
{{#if isAdmin}}
2930
<div class="row channel-actions">
3031
<div class="col-md-2">
3132
<button class="btn btn-medium btn-warning" data-action="empty">Empty Queue</button>
@@ -41,6 +42,7 @@
4142
{{/if}}
4243
</div>
4344
</div>
45+
{{/if}}
4446

4547
<div class="row">
4648
<div class="col-md-12">

nsqadmin/static/js/views/channel.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@ var ChannelView = BaseView.extend({
2121
initialize: function() {
2222
BaseView.prototype.initialize.apply(this, arguments);
2323
this.listenTo(AppState, 'change:graph_interval', this.render);
24+
var isAdmin = this.model.get('isAdmin');
2425
this.model.fetch()
2526
.done(function(data) {
2627
this.template = require('./channel.hbs');
27-
this.render({'message': data['message']});
28+
this.render({'message': data['message'], 'isAdmin': isAdmin});
2829
}.bind(this))
2930
.fail(this.handleViewError.bind(this))
3031
.always(Pubsub.trigger.bind(Pubsub, 'view:ready'));

nsqadmin/static/js/views/lookup.hbs

+2
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
</div>
5252
</div>
5353

54+
{{#if isAdmin}}
5455
<div class="row">
5556
<div class="col-md-4">
5657
<form class="hierarchy">
@@ -68,4 +69,5 @@
6869
</form>
6970
</div>
7071
</div>
72+
{{/if}}
7173
{{/unless}}

nsqadmin/static/js/views/lookup.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,16 @@ var LookupView = BaseView.extend({
2121

2222
initialize: function() {
2323
BaseView.prototype.initialize.apply(this, arguments);
24+
var isAdmin = arguments[0]['isAdmin'];
2425
$.ajax(AppState.url('/topics?inactive=true'))
2526
.done(function(data) {
2627
this.template = require('./lookup.hbs');
2728
this.render({
2829
'topics': _.map(data['topics'], function(v, k) {
2930
return {'name': k, 'channels': v};
3031
}),
31-
'message': data['message']
32+
'message': data['message'],
33+
'isAdmin': isAdmin
3234
});
3335
}.bind(this))
3436
.fail(this.handleViewError.bind(this))

nsqadmin/static/js/views/topic.hbs

+2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
</div>
2626
</div>
2727
{{else}}
28+
{{#if isAdmin}}
2829
<div class="row topic-actions">
2930
<div class="col-md-2">
3031
<button class="btn btn-medium btn-warning" data-action="empty">Empty Queue</button>
@@ -40,6 +41,7 @@
4041
{{/if}}
4142
</div>
4243
</div>
44+
{{/if}}
4345

4446
<div class="row">
4547
<div class="col-md-12">

nsqadmin/static/js/views/topic.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@ var TopicView = BaseView.extend({
2121
initialize: function() {
2222
BaseView.prototype.initialize.apply(this, arguments);
2323
this.listenTo(AppState, 'change:graph_interval', this.render);
24+
var isAdmin = this.model.get('isAdmin')
2425
this.model.fetch()
2526
.done(function(data) {
2627
this.template = require('./topic.hbs');
27-
this.render({'message': data['message']});
28+
this.render({'message': data['message'], 'isAdmin': isAdmin});
2829
}.bind(this))
2930
.fail(this.handleViewError.bind(this))
3031
.always(Pubsub.trigger.bind(Pubsub, 'view:ready'));

0 commit comments

Comments
 (0)