Skip to content

Commit 3a0359f

Browse files
committed
wip
1 parent 2d04f70 commit 3a0359f

File tree

8 files changed

+259
-1
lines changed

8 files changed

+259
-1
lines changed

api/pkg/responses/system.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ type SystemInfo struct {
99

1010
type SystemAuthenticationInfo struct {
1111
Local bool `json:"local"`
12+
SAML bool `json:"saml"`
1213
}
1314

1415
type SystemEndpointsInfo struct {

api/services/system.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ func (s *service) GetSystemInfo(ctx context.Context, req *requests.GetSystemInfo
3636
},
3737
Authentication: &responses.SystemAuthenticationInfo{
3838
Local: system.Authentication.Local.Enabled,
39+
SAML: system.Authentication.SAML.Enabled,
3940
},
4041
}
4142

api/store/mongo/migrations/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ func GenerateMigrations() []migrate.Migration {
9898
migration86,
9999
migration87,
100100
migration88,
101+
migration89,
101102
}
102103
}
103104

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package migrations
2+
3+
import (
4+
"context"
5+
6+
"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 migration89 = migrate.Migration{
13+
Version: 89,
14+
Description: "Adding an 'authentication.saml' attributes to system collection",
15+
Up: migrate.MigrationFunc(func(ctx context.Context, db *mongo.Database) error {
16+
logrus.WithFields(logrus.Fields{
17+
"component": "migration",
18+
"version": 89,
19+
"action": "Up",
20+
}).Info("Applying migration")
21+
22+
filter := bson.M{
23+
"authentication.saml": bson.M{"$exists": false},
24+
}
25+
26+
update := bson.M{
27+
"$set": bson.M{
28+
"authentication.saml": bson.M{
29+
"enabled": false,
30+
"idp": bson.M{
31+
"entity_id": "",
32+
"signon_url": "",
33+
"certificates": []string{},
34+
},
35+
"sp": bson.M{
36+
"sign_auth_requests": false,
37+
"certificate": "",
38+
"private_key": "",
39+
},
40+
},
41+
},
42+
}
43+
44+
_, err := db.
45+
Collection("system").
46+
UpdateMany(ctx, filter, update)
47+
48+
return err
49+
}),
50+
Down: migrate.MigrationFunc(func(ctx context.Context, db *mongo.Database) error {
51+
logrus.WithFields(logrus.Fields{
52+
"component": "migration",
53+
"version": 89,
54+
"action": "Down",
55+
}).Info("Reverting migration")
56+
57+
filter := bson.M{
58+
"authentication.saml": bson.M{"$exists": true},
59+
}
60+
61+
update := bson.M{
62+
"$unset": bson.M{
63+
"authentication.saml": "",
64+
},
65+
}
66+
67+
_, err := db.
68+
Collection("system").
69+
UpdateMany(ctx, filter, update)
70+
71+
return err
72+
}),
73+
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package migrations
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/shellhub-io/shellhub/pkg/envs"
8+
envmock "github.com/shellhub-io/shellhub/pkg/envs/mocks"
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
migrate "github.com/xakep666/mongo-migrate"
12+
"go.mongodb.org/mongo-driver/bson"
13+
"go.mongodb.org/mongo-driver/bson/primitive"
14+
)
15+
16+
func TestMigration89Up(t *testing.T) {
17+
ctx := context.Background()
18+
19+
mock := &envmock.Backend{}
20+
envs.DefaultBackend = mock
21+
22+
tests := []struct {
23+
description string
24+
setup func() error
25+
}{
26+
{
27+
description: "Apply up on migration 89",
28+
setup: func() error {
29+
_, err := c.
30+
Database("test").
31+
Collection("system").
32+
InsertOne(ctx, map[string]interface{}{
33+
"authentication": map[string]interface{}{
34+
"local": map[string]interface{}{
35+
"enabled": true,
36+
},
37+
},
38+
})
39+
40+
return err
41+
},
42+
},
43+
}
44+
45+
for _, tc := range tests {
46+
t.Run(tc.description, func(tt *testing.T) {
47+
tt.Cleanup(func() {
48+
assert.NoError(tt, srv.Reset())
49+
})
50+
51+
assert.NoError(tt, tc.setup())
52+
53+
migrates := migrate.NewMigrate(c.Database("test"), GenerateMigrations()[88])
54+
require.NoError(tt, migrates.Up(context.Background(), migrate.AllAvailable))
55+
56+
query := c.
57+
Database("test").
58+
Collection("system").
59+
FindOne(context.TODO(), bson.M{})
60+
61+
system := make(map[string]interface{})
62+
require.NoError(tt, query.Decode(&system))
63+
64+
saml, ok := system["authentication"].(map[string]interface{})["saml"].(map[string]interface{})
65+
require.Equal(tt, true, ok)
66+
67+
enabled, ok := saml["enabled"]
68+
require.Equal(tt, true, ok)
69+
require.Equal(tt, false, enabled)
70+
71+
idp, ok := saml["idp"].(map[string]interface{})
72+
require.Equal(tt, true, ok)
73+
require.Equal(tt, map[string]interface{}{"entity_id": "", "signon_url": "", "certificates": primitive.A{}}, idp)
74+
75+
sp, ok := saml["sp"].(map[string]interface{})
76+
require.Equal(tt, true, ok)
77+
require.Equal(tt, map[string]interface{}{"sign_auth_requests": false, "certificate": "", "private_key": ""}, sp)
78+
})
79+
}
80+
}
81+
82+
func TestMigration89Down(t *testing.T) {
83+
ctx := context.Background()
84+
85+
mock := &envmock.Backend{}
86+
envs.DefaultBackend = mock
87+
88+
mock.On("Get", "SHELLHUB_CLOUD").Return("false")
89+
mock.On("Get", "SHELLHUB_ENTERPRISE").Return("false")
90+
91+
tests := []struct {
92+
description string
93+
setup func() error
94+
}{
95+
{
96+
description: "Apply up on migration 89",
97+
setup: func() error {
98+
_, err := c.
99+
Database("test").
100+
Collection("system").
101+
InsertOne(ctx, map[string]interface{}{
102+
"authentication": map[string]interface{}{
103+
"local": true,
104+
},
105+
})
106+
107+
return err
108+
},
109+
},
110+
}
111+
112+
for _, tc := range tests {
113+
t.Run(tc.description, func(tt *testing.T) {
114+
tt.Cleanup(func() {
115+
assert.NoError(tt, srv.Reset())
116+
})
117+
118+
assert.NoError(tt, tc.setup())
119+
120+
migrates := migrate.NewMigrate(c.Database("test"), GenerateMigrations()[88])
121+
require.NoError(tt, migrates.Up(context.Background(), migrate.AllAvailable))
122+
require.NoError(tt, migrates.Down(context.Background(), migrate.AllAvailable))
123+
124+
query := c.
125+
Database("test").
126+
Collection("system").
127+
FindOne(context.TODO(), bson.M{})
128+
129+
system := make(map[string]interface{})
130+
require.NoError(tt, query.Decode(&system))
131+
132+
_, ok := system["authentication"].(map[string]interface{})["saml"]
133+
require.Equal(tt, false, ok)
134+
})
135+
}
136+
}

gateway/nginx/conf.d/shellhub.conf

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,15 @@ server {
452452
{{ end -}}
453453

454454
{{ if $cfg.EnableEnterprise -}}
455+
location /api/saml/acs {
456+
{{ set_upstream "cloud-api" 8080 }}
457+
458+
auth_request off;
459+
proxy_set_header X-Real-IP $x_real_ip;
460+
proxy_set_header X-Forwarded-Host $host;
461+
proxy_pass http://upstream_router;
462+
}
463+
455464
location /api/user/mfa {
456465
{{ set_upstream "cloud-api" 8080 }}
457466

pkg/models/system.go

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,44 @@ type System struct {
99
}
1010

1111
type SystemAuthentication struct {
12-
Local *SystemAuthenticationLocal `json:"manual" bson:"manual"`
12+
Local *SystemAuthenticationLocal `json:"local" bson:"local"`
13+
SAML *SystemAuthenticationSAML `json:"saml" bson:"saml"`
1314
}
1415

1516
type SystemAuthenticationLocal struct {
1617
// Enabled indicates whether manual authentication using a username and password is enabled or
1718
// not.
1819
Enabled bool `json:"enabled" bool:"enabled"`
1920
}
21+
22+
type SystemAuthenticationSAML struct {
23+
// Enabled indicates whether SAML authentication is enabled.
24+
Enabled bool `json:"enabled" bson:"enabled"`
25+
Idp *SystemIdpSAML `json:"idp" bson:"idp"`
26+
Sp *SystemSpSAML `json:"sp" bson:"sp"`
27+
}
28+
29+
type SystemIdpSAML struct {
30+
EntityID string `json:"entity_id" bson:"entity_id"`
31+
SignonURL string `json:"signon_url" bson:"signon_url"`
32+
// Certificates is a list of X.509 certificates provided by the IdP. These certificates are used
33+
// by the SP to validate the digital signatures of SAML assertions issued by the IdP.
34+
Certificates []string `json:"certificates" bson:"certificates"`
35+
}
36+
37+
type SystemSpSAML struct {
38+
// SignRequests indicates whether ShellHub should sign authentication requests.
39+
// If enabled, an X509 certificate is used to sign the request, and the IdP must authenticate
40+
// the request using the corresponding public certificate. Enabling this option disables
41+
// the "IdP-initiated" authentication pipeline.
42+
SignAuthRequests bool `json:"sign_auth_requests" bson:"sign_auth_requests"`
43+
// Certificate is an X509 certificate that the IdP uses to verify the authenticity of the
44+
// authentication request signed by ShellHub. This certificate corresponds to the private key
45+
// in the [SystemSpSAML.PrivateKey] and it is only populated when [SystemSpSAML.SignAuthRequests]
46+
// is true.
47+
Certificate string `json:"certificate" bson:"certificate"`
48+
// PrivateKey is an encrypted private key used by ShellHub to sign authentication requests.
49+
// The IdP verifies the signature using the [SystemSpSAML.Certificate]. It is only populated
50+
// when [SystemSpSAML.SignAuthRequests] is true.
51+
PrivateKey string `json:"-" bson:"private_key"`
52+
}

pkg/models/user.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ const (
3333
// UserOriginLocal indicates that the user was created through the standard signup process, without
3434
// using third-party integrations like SSO IdPs.
3535
UserOriginLocal UserOrigin = "local"
36+
// UserOriginSAML indicates that the user was created using a SAML authentication method.
37+
UserOriginSAML UserOrigin = "SSO"
3638
)
3739

3840
func (o UserOrigin) String() string {
@@ -44,6 +46,8 @@ type UserAuthMethod string
4446
const (
4547
// UserAuthMethodLocal indicates that the user can authenticate using an email and password.
4648
UserAuthMethodLocal UserAuthMethod = "local"
49+
// UserAuthMethodManual indicates that the user can authenticate using a third-party SAML application.
50+
UserAuthMethodSAML UserAuthMethod = "saml"
4751
)
4852

4953
func (a UserAuthMethod) String() string {

0 commit comments

Comments
 (0)