Skip to content

Commit 2f24e04

Browse files
committed
feat: add disable password and disapble public key on namespace config
Add two configuration fields to namespace settings that allow administrators to restrict SSH authentication methods (password, public key) for all devices within their namespace. When a connection is rejected due to these restrictions, the server logs the rejection event with details, while the client receives a generic authentication failure message. Closes #6136
1 parent 50df770 commit 2f24e04

31 files changed

Lines changed: 700 additions & 40 deletions

api/services/namespace.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ func (s *service) CreateNamespace(ctx context.Context, req *requests.NamespaceCr
6666
Settings: &models.NamespaceSettings{
6767
SessionRecord: true,
6868
ConnectionAnnouncement: "",
69+
DisablePassword: false,
70+
DisablePublicKey: false,
6971
},
7072
TenantID: req.TenantID,
7173
Type: models.NewDefaultType(),
@@ -176,6 +178,14 @@ func (s *service) EditNamespace(ctx context.Context, req *requests.NamespaceEdit
176178
namespace.Settings.ConnectionAnnouncement = *req.Settings.ConnectionAnnouncement
177179
}
178180

181+
if req.Settings.DisablePassword != nil {
182+
namespace.Settings.DisablePassword = *req.Settings.DisablePassword
183+
}
184+
185+
if req.Settings.DisablePublicKey != nil {
186+
namespace.Settings.DisablePublicKey = *req.Settings.DisablePublicKey
187+
}
188+
179189
if err := s.store.NamespaceUpdate(ctx, namespace); err != nil {
180190
return nil, err
181191
}

api/services/namespace_test.go

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1026,7 +1026,13 @@ func TestEditNamespace(t *testing.T) {
10261026
requiredMocks func()
10271027
tenantID string
10281028
namespaceName string
1029-
expected Expected
1029+
settings struct {
1030+
SessionRecord *bool
1031+
ConnectionAnnouncement *string
1032+
DisablePassword *bool
1033+
DisablePublicKey *bool
1034+
}
1035+
expected Expected
10301036
}{
10311037
{
10321038
description: "fails when namespace does not exist",
@@ -1111,10 +1117,112 @@ func TestEditNamespace(t *testing.T) {
11111117
nil,
11121118
},
11131119
},
1120+
{
1121+
description: "succeeds changing DisablePassword",
1122+
tenantID: "xxxxx",
1123+
settings: struct {
1124+
SessionRecord *bool
1125+
ConnectionAnnouncement *string
1126+
DisablePassword *bool
1127+
DisablePublicKey *bool
1128+
}{
1129+
DisablePassword: func(b bool) *bool { return &b }(true),
1130+
},
1131+
requiredMocks: func() {
1132+
namespace := &models.Namespace{
1133+
TenantID: "xxxxx",
1134+
Name: "oldname",
1135+
Settings: &models.NamespaceSettings{DisablePassword: false},
1136+
}
1137+
storeMock.
1138+
On("NamespaceResolve", ctx, store.NamespaceTenantIDResolver, "xxxxx").
1139+
Return(namespace, nil).
1140+
Once()
1141+
1142+
expectedNamespace := *namespace
1143+
expectedNamespace.Settings.DisablePassword = true
1144+
storeMock.
1145+
On("NamespaceUpdate", ctx, &expectedNamespace).
1146+
Return(nil).
1147+
Once()
1148+
1149+
finalNamespace := &models.Namespace{
1150+
TenantID: "xxxxx",
1151+
Name: "oldname",
1152+
Settings: &models.NamespaceSettings{DisablePassword: true},
1153+
}
1154+
storeMock.
1155+
On("NamespaceResolve", ctx, store.NamespaceTenantIDResolver, "xxxxx").
1156+
Return(finalNamespace, nil).
1157+
Once()
1158+
},
1159+
expected: Expected{
1160+
&models.Namespace{
1161+
TenantID: "xxxxx",
1162+
Name: "oldname",
1163+
Settings: &models.NamespaceSettings{DisablePassword: true},
1164+
},
1165+
nil,
1166+
},
1167+
},
1168+
{
1169+
description: "succeeds changing DisablePublicKey",
1170+
tenantID: "xxxxx",
1171+
settings: struct {
1172+
SessionRecord *bool
1173+
ConnectionAnnouncement *string
1174+
DisablePassword *bool
1175+
DisablePublicKey *bool
1176+
}{
1177+
DisablePublicKey: func(b bool) *bool { return &b }(true),
1178+
},
1179+
requiredMocks: func() {
1180+
namespace := &models.Namespace{
1181+
TenantID: "xxxxx",
1182+
Name: "oldname",
1183+
Settings: &models.NamespaceSettings{DisablePublicKey: false},
1184+
}
1185+
storeMock.
1186+
On("NamespaceResolve", ctx, store.NamespaceTenantIDResolver, "xxxxx").
1187+
Return(namespace, nil).
1188+
Once()
1189+
1190+
expectedNamespace := *namespace
1191+
expectedNamespace.Settings.DisablePublicKey = true
1192+
storeMock.
1193+
On("NamespaceUpdate", ctx, &expectedNamespace).
1194+
Return(nil).
1195+
Once()
1196+
1197+
finalNamespace := &models.Namespace{
1198+
TenantID: "xxxxx",
1199+
Name: "oldname",
1200+
Settings: &models.NamespaceSettings{DisablePublicKey: true},
1201+
}
1202+
storeMock.
1203+
On("NamespaceResolve", ctx, store.NamespaceTenantIDResolver, "xxxxx").
1204+
Return(finalNamespace, nil).
1205+
Once()
1206+
},
1207+
expected: Expected{
1208+
&models.Namespace{
1209+
TenantID: "xxxxx",
1210+
Name: "oldname",
1211+
Settings: &models.NamespaceSettings{DisablePublicKey: true},
1212+
},
1213+
nil,
1214+
},
1215+
},
11141216
{
11151217
description: "succeeds",
11161218
namespaceName: "newname",
11171219
tenantID: "xxxxx",
1220+
settings: struct {
1221+
SessionRecord *bool
1222+
ConnectionAnnouncement *string
1223+
DisablePassword *bool
1224+
DisablePublicKey *bool
1225+
}{},
11181226
requiredMocks: func() {
11191227
namespace := &models.Namespace{
11201228
TenantID: "xxxxx",
@@ -1163,6 +1271,11 @@ func TestEditNamespace(t *testing.T) {
11631271
TenantParam: requests.TenantParam{Tenant: tc.tenantID},
11641272
Name: tc.namespaceName,
11651273
}
1274+
req.Settings.SessionRecord = tc.settings.SessionRecord
1275+
req.Settings.ConnectionAnnouncement = tc.settings.ConnectionAnnouncement
1276+
req.Settings.DisablePassword = tc.settings.DisablePassword
1277+
req.Settings.DisablePublicKey = tc.settings.DisablePublicKey
1278+
11661279
namespace, err := service.EditNamespace(ctx, req)
11671280

11681281
assert.Equal(t, tc.expected, Expected{namespace, err})

api/store/mongo/migrations/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ func GenerateMigrations() []migrate.Migration {
130130
migration118,
131131
migration119,
132132
migration120,
133+
migration121,
133134
}
134135
}
135136

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package migrations
2+
3+
import (
4+
"context"
5+
6+
log "github.com/sirupsen/logrus"
7+
migrate "github.com/xakep666/mongo-migrate"
8+
"go.mongodb.org/mongo-driver/bson"
9+
"go.mongodb.org/mongo-driver/mongo"
10+
)
11+
12+
var migration121 = migrate.Migration{
13+
Version: 121,
14+
Description: "Add disable_password and disable_public_key to namespace settings",
15+
Up: migrate.MigrationFunc(func(ctx context.Context, db *mongo.Database) error {
16+
log.WithFields(log.Fields{"component": "migration", "version": 121, "action": "Up"}).Info("Applying migration")
17+
18+
if _, err := db.Collection("namespaces").UpdateMany(ctx, bson.M{}, bson.M{
19+
"$set": bson.M{
20+
"settings.disable_password": false,
21+
"settings.disable_public_key": false,
22+
},
23+
}); err != nil {
24+
log.WithError(err).Error("Failed to add disable_password and disable_public_key to namespace settings")
25+
26+
return err
27+
}
28+
29+
return nil
30+
}),
31+
Down: migrate.MigrationFunc(func(ctx context.Context, db *mongo.Database) error {
32+
log.WithFields(log.Fields{"component": "migration", "version": 121, "action": "Down"}).Info("Reverting migration")
33+
34+
if _, err := db.Collection("namespaces").UpdateMany(ctx, bson.M{}, bson.M{
35+
"$unset": bson.M{
36+
"settings.disable_password": "",
37+
"settings.disable_public_key": "",
38+
},
39+
}); err != nil {
40+
log.WithError(err).Error("Failed to remove disable_password and disable_public_key from namespace settings")
41+
42+
return err
43+
}
44+
45+
return nil
46+
}),
47+
}

api/store/pg/entity/namespace.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ type NamespaceSettings struct {
2828
MaxDevices int `bun:"max_devices"`
2929
SessionRecord bool `bun:"record_sessions"`
3030
ConnectionAnnouncement string `bun:"connection_announcement,type:text"`
31+
DisablePassword bool `bun:"disable_password"`
32+
DisablePublicKey bool `bun:"disable_public_key"`
3133
}
3234

3335
func NamespaceFromModel(model *models.Namespace) *Namespace {
@@ -55,6 +57,8 @@ func NamespaceFromModel(model *models.Namespace) *Namespace {
5557
if model.Settings != nil {
5658
namespace.Settings.SessionRecord = model.Settings.SessionRecord
5759
namespace.Settings.ConnectionAnnouncement = model.Settings.ConnectionAnnouncement
60+
namespace.Settings.DisablePassword = model.Settings.DisablePassword
61+
namespace.Settings.DisablePublicKey = model.Settings.DisablePublicKey
5862
}
5963

6064
namespace.Memberships = make([]Membership, len(model.Members))
@@ -85,6 +89,8 @@ func NamespaceToModel(entity *Namespace) *models.Namespace {
8589
Settings: &models.NamespaceSettings{
8690
SessionRecord: entity.Settings.SessionRecord,
8791
ConnectionAnnouncement: entity.Settings.ConnectionAnnouncement,
92+
DisablePassword: entity.Settings.DisablePassword,
93+
DisablePublicKey: entity.Settings.DisablePublicKey,
8894
},
8995
}
9096

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
ALTER TABLE namespaces DROP COLUMN disable_password;
2+
ALTER TABLE namespaces DROP COLUMN disable_public_key;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
ALTER TABLE namespaces ADD COLUMN disable_password BOOLEAN DEFAULT FALSE NOT NULL;
2+
ALTER TABLE namespaces ADD COLUMN disable_public_key BOOLEAN DEFAULT FALSE NOT NULL;

openapi/spec/components/schemas/namespaceSettings.yaml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,16 @@ properties:
1212
maxLength: 4096
1313
format: alphanumunicode
1414
example: my awesome connection announcement
15-
required:
15+
disable_password:
16+
description: The disable password define when the namespace should block or not password authentication.
17+
type: boolean
18+
example: false
19+
disable_public_key:
20+
description: The disable public key define when the namespace should block or not public key authentication.
21+
type: boolean
22+
example: false
23+
required:
1624
- session_record
1725
- connection_announcement
26+
- disable_password
27+
- disable_public_key

pkg/api/requests/namespace.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ type NamespaceEdit struct {
5252
Settings struct {
5353
SessionRecord *bool `json:"session_record" validate:"omitempty"`
5454
ConnectionAnnouncement *string `json:"connection_announcement" validate:"omitempty,min=0,max=4096"`
55+
DisablePassword *bool `json:"disable_password" validate:"omitempty"`
56+
DisablePublicKey *bool `json:"disable_public_key" validate:"omitempty"`
5557
} `json:"settings"`
5658
}
5759

pkg/models/namespace.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ func (n *Namespace) FindMember(id string) (*Member, bool) {
5151
type NamespaceSettings struct {
5252
SessionRecord bool `json:"session_record" bson:"session_record,omitempty"`
5353
ConnectionAnnouncement string `json:"connection_announcement" bson:"connection_announcement"`
54+
DisablePassword bool `json:"disable_password" bson:"disable_password,omitempty"`
55+
DisablePublicKey bool `json:"disable_public_key" bson:"disable_public_key,omitempty"`
5456
}
5557

5658
// default Announcement Message for the shellhub namespace

0 commit comments

Comments
 (0)