Skip to content

Commit

Permalink
feat: allow invalid username for specific charge stations
Browse files Browse the repository at this point in the history
  • Loading branch information
subnova committed Mar 15, 2024
1 parent b0ef610 commit 8f52de0
Show file tree
Hide file tree
Showing 18 changed files with 295 additions and 141 deletions.
14 changes: 7 additions & 7 deletions .talismanrc
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,21 @@ fileignoreconfig:
- filename: gateway/cmd/serve.go
checksum: f08fe8f8c8710239376098c6e1b778d3fa9a1fb5f0653ff2a9a34e9c70d4194e
- filename: gateway/registry/remote.go
checksum: bcdef78dfc66e140acd32d12df1806b95c3541f51cc0208a43abff49552fdcd8
checksum: 2ccb715b917495fca8090f1d85a146f97b436b9490e10f70a9f0883dba5e2fc6
- filename: gateway/registry/remote_test.go
checksum: f5aa4dbb5e14d772613612eeb02df83ae4458875487e3c408eff2950b460c298
- filename: gateway/server/tracing.go
checksum: 10c205849723d591f5c90fbf0068fa5cf77b8e545e351bc538610b950bc18c3f
- filename: gateway/server/ws.go
checksum: 4cbde936242380603e07cf8bd049dbca9d1c3108843d10e58f588540176c6d23
- filename: gateway/server/ws_test.go
checksum: 844d3cbd6d95dab9555f901c21fe37b6882831a18ea2b6b9120a9230ff2e6006
checksum: e16bac32f52486a9f05e97758a96077296741fb6126689f408bf1e8d9eae84e6
- filename: manager/Dockerfile
checksum: db69536d308b905d6214db1fb0e10442fe3e770ef3fed47809ca1f3ec099409d
- filename: manager/adminui/server.go
checksum: 826cbac1161c5c8b29eeeb7b7caaf6127da578808d2af1b6a941134737ae032b
checksum: aa3d352ed0bddb533861d410ddf4de8877c7839a7d00c611ba31706dcf9d9399
- filename: manager/adminui/server_test.go
checksum: 3a2329f89ca93816c474c0407eacd79f02bbcb7f3f6a3340fa7b3a44a71826ff
checksum: 497c91994bbce5defae822b26b78be805ac8b2f2c051c1ea163bb5addd0794e4
- filename: manager/adminui/templates/connect.gohtml
checksum: d127a65b7f328d5aee708e4967f921ad5c96bb9e797d4307a0cdb9666a05e2a3
- filename: manager/adminui/templates/error.gohtml
Expand All @@ -42,9 +42,9 @@ fileignoreconfig:
- filename: manager/adminui/templates/token.gohtml
checksum: 6af3e3014820fe20e8f6b6e7fd954da0e67f8657f1b559b6452112d01ea90f09
- filename: manager/api/API.md
checksum: c9a715fef138947fcaa341a4ee531a1c87216f180de576565ff5edb42cec936e
checksum: d1078a7ba98967a6a9918dd8095edf77e06a52e78d02276ff387a134b328e46e
- filename: manager/api/api.gen.go
checksum: 7938cf66adf3f3cca66581111dea5b2f9b9ceb28d252493bc78f8746a920b36d
checksum: cea123ebc20489d2a3faeaa2d4139c77ed2289b4397931835a6a7fb1b81ebfd9
- filename: manager/api/server.go
checksum: 98cbd1df522ea428f7e669c01721f3070133062378a4f2e4b167d55eaa8d079b
- filename: manager/api/server_test.go
Expand Down Expand Up @@ -184,7 +184,7 @@ fileignoreconfig:
- filename: manager/store/firestore/cert_test.go
checksum: 5882e08beca19cfcaab9abda418c87aa13251b49424817e9c3a54a4be4bb268c
- filename: manager/store/firestore/cs.go
checksum: 04aa0b00b290595b5d8d7a4693bf526a3d6ee1df8a1a2e7ca9a25f0a54eb7dd3
checksum: 497c91994bbce5defae822b26b78be805ac8b2f2c051c1ea163bb5addd0794e4
- filename: manager/store/firestore/cs_test.go
checksum: b31b2ed998237c77a6cd3501ce4231c2cbe15a2aca68db8715f4f43f9fc7e01c
- filename: manager/store/firestore/store.go
Expand Down
7 changes: 4 additions & 3 deletions gateway/registry/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ const (
)

type ChargeStation struct {
ClientId string
SecurityProfile SecurityProfile
Base64SHA256Password string
ClientId string
SecurityProfile SecurityProfile
Base64SHA256Password string
InvalidUsernameAllowed bool
}

type DeviceRegistry interface {
Expand Down
12 changes: 7 additions & 5 deletions gateway/registry/remote.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ type RemoteRegistry struct {
}

type ChargeStationAuthDetailsResponse struct {
SecurityProfile int `json:"securityProfile"`
Base64SHA256Password string `json:"base64SHA256Password,omitempty"`
SecurityProfile int `json:"securityProfile"`
Base64SHA256Password string `json:"base64SHA256Password,omitempty"`
InvalidUsernameAllowed bool `json:"invalidUsernameAllowed,omitempty"`
}

func (r RemoteRegistry) LookupChargeStation(clientId string) (*ChargeStation, error) {
Expand Down Expand Up @@ -46,9 +47,10 @@ func (r RemoteRegistry) LookupChargeStation(clientId string) (*ChargeStation, er
return nil, fmt.Errorf("unmarshaling data: %w", err)
}
return &ChargeStation{
ClientId: clientId,
SecurityProfile: SecurityProfile(chargeStationAuthDetails.SecurityProfile),
Base64SHA256Password: chargeStationAuthDetails.Base64SHA256Password,
ClientId: clientId,
SecurityProfile: SecurityProfile(chargeStationAuthDetails.SecurityProfile),
Base64SHA256Password: chargeStationAuthDetails.Base64SHA256Password,
InvalidUsernameAllowed: chargeStationAuthDetails.InvalidUsernameAllowed,
}, nil
}

Expand Down
2 changes: 1 addition & 1 deletion gateway/server/ws.go
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ func checkAuthorization(ctx context.Context, r *http.Request, cs *registry.Charg
span.SetAttributes(attribute.String("auth.failure_reason", "no basic auth"))
return false
}
if username != cs.ClientId {
if username != cs.ClientId && !cs.InvalidUsernameAllowed {
span.SetAttributes(
attribute.String("auth.failure_reason", "invalid username"),
attribute.String("auth.username", username))
Expand Down
78 changes: 78 additions & 0 deletions gateway/server/ws_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,84 @@ func TestHttpConnectionWithBasicAuthWrongPassword(t *testing.T) {
}
}

func TestHttpConnectionWithBasicAuthWrongUsername(t *testing.T) {
//defer goleak.VerifyNone(t)

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

cs := &registry.ChargeStation{
ClientId: "basicAuthCS1WrongUsername",
SecurityProfile: registry.UnsecuredTransportWithBasicAuth,
Base64SHA256Password: "XohImNooBHFR0OVvjcYpJ3NgPQ1qq73WKhHvch0VQtg=", // password,
}

mockRegistry := registry.NewMockRegistry()
mockRegistry.ChargeStations[cs.ClientId] = cs

srv := httptest.NewServer(server.NewWebsocketHandler(server.WithDeviceRegistry(mockRegistry)))
defer srv.Close()

authHeader := "Basic " + base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", "wrong", "password")))
dialOptions := &websocket.DialOptions{
Subprotocols: []string{"ocpp1.6", "ocpp2.0.1"},
HTTPHeader: http.Header{
"authorization": []string{authHeader},
},
}

conn, resp, err := websocket.Dial(ctx, fmt.Sprintf("%s/ws/%s", srv.URL, cs.ClientId), dialOptions)
if err == nil {
_ = conn.Close(websocket.StatusGoingAway, "Shutdown")
t.Fatalf("expected error dialing CSMS")
}
if resp.StatusCode != http.StatusUnauthorized {
t.Fatalf("status code: want %d, got %d", http.StatusUnauthorized, resp.StatusCode)
}
}

func TestHttpConnectionWithBasicAuthInvalidUsernameAllowed(t *testing.T) {
//defer goleak.VerifyNone(t)

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

cs := &registry.ChargeStation{
ClientId: "basicAuthCS1WrongUsername",
SecurityProfile: registry.UnsecuredTransportWithBasicAuth,
Base64SHA256Password: "XohImNooBHFR0OVvjcYpJ3NgPQ1qq73WKhHvch0VQtg=", // password,
InvalidUsernameAllowed: true,
}

mockRegistry := registry.NewMockRegistry()
mockRegistry.ChargeStations[cs.ClientId] = cs

srv := httptest.NewServer(server.NewWebsocketHandler(server.WithDeviceRegistry(mockRegistry)))
defer srv.Close()

authHeader := "Basic " + base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", "wrong", "password")))
dialOptions := &websocket.DialOptions{
Subprotocols: []string{"ocpp1.6", "ocpp2.0.1"},
HTTPHeader: http.Header{
"authorization": []string{authHeader},
},
}

conn, resp, err := websocket.Dial(ctx, fmt.Sprintf("%s/ws/%s", srv.URL, cs.ClientId), dialOptions)
if err != nil {
t.Fatalf("dialing CSMS: %v", err)
}
defer func() {
err := conn.Close(websocket.StatusGoingAway, "Shutdown")
if err != nil {
t.Logf("WARN: websocket close: %v", err)
}
}()
if resp.StatusCode != http.StatusSwitchingProtocols {
t.Fatalf("status code: want %d, got %d", http.StatusSwitchingProtocols, resp.StatusCode)
}
}

func TestTlsConnectionWithBasicAuth(t *testing.T) {
//defer goleak.VerifyNone(t)

Expand Down
32 changes: 18 additions & 14 deletions manager/adminui/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ func NewServer(externalAddr, orgName string, engine store.Engine, certificatePro

csId := r.PostFormValue("csid")
auth := r.PostFormValue("auth")
invalidUsername := r.PostFormValue("invalidUsername")

if csId == "" || auth == "" {
slog.Error("missing form parameters")
Expand All @@ -79,17 +80,18 @@ func NewServer(externalAddr, orgName string, engine store.Engine, certificatePro
_ = templates.ExecuteTemplate(w, "error.gohtml", nil)
return
}
err = registerChargeStation(r.Context(), engine, csId, 0, password)
err = registerChargeStation(r.Context(), engine, csId, 0, password, invalidUsername == "on")
if err != nil {
slog.Error("registering charge station", "err", err)
_ = templates.ExecuteTemplate(w, "error.gohtml", nil)
return
}
data = map[string]string{
"csid": csId,
"auth": auth,
"url": fmt.Sprintf("ws://%s/ws/%s", externalAddr, csId),
"password": password,
"csid": csId,
"auth": auth,
"url": fmt.Sprintf("ws://%s/ws/%s", externalAddr, csId),
"password": password,
"invalidUsername": invalidUsername,
}
case "basic":
password, err := createPassword()
Expand All @@ -98,17 +100,18 @@ func NewServer(externalAddr, orgName string, engine store.Engine, certificatePro
_ = templates.ExecuteTemplate(w, "error.gohtml", nil)
return
}
err = registerChargeStation(r.Context(), engine, csId, 1, password)
err = registerChargeStation(r.Context(), engine, csId, 1, password, invalidUsername == "on")
if err != nil {
slog.Error("registering charge station", "err", err)
_ = templates.ExecuteTemplate(w, "error.gohtml", nil)
return
}
data = map[string]string{
"csid": csId,
"auth": auth,
"url": fmt.Sprintf("wss://%s/ws/%s", externalAddr, csId),
"password": password,
"csid": csId,
"auth": auth,
"url": fmt.Sprintf("wss://%s/ws/%s", externalAddr, csId),
"password": password,
"invalidUsername": invalidUsername,
}
case "mtls":
clientKey, clientCert, err := createSignedKeyPair(r.Context(), csId, orgName, certificateProvider)
Expand All @@ -117,7 +120,7 @@ func NewServer(externalAddr, orgName string, engine store.Engine, certificatePro
_ = templates.ExecuteTemplate(w, "error.gohtml", nil)
return
}
err = registerChargeStation(r.Context(), engine, csId, 2, "")
err = registerChargeStation(r.Context(), engine, csId, 2, "", false)
if err != nil {
slog.Error("registering charge station", "err", err)
_ = templates.ExecuteTemplate(w, "error.gohtml", nil)
Expand Down Expand Up @@ -254,7 +257,7 @@ func createSignedKeyPair(ctx context.Context, csId string, orgName string, certi
return string(pemKey), chain, nil
}

func registerChargeStation(ctx context.Context, engine store.Engine, csId string, scheme int, password string) error {
func registerChargeStation(ctx context.Context, engine store.Engine, csId string, scheme int, password string, invalidUsername bool) error {
var profile store.SecurityProfile

switch scheme {
Expand All @@ -275,8 +278,9 @@ func registerChargeStation(ctx context.Context, engine store.Engine, csId string
}

err := engine.SetChargeStationAuth(ctx, csId, &store.ChargeStationAuth{
SecurityProfile: profile,
Base64SHA256Password: b64sha256,
SecurityProfile: profile,
Base64SHA256Password: b64sha256,
InvalidUsernameAllowed: invalidUsername,
})
if err != nil {
return err
Expand Down
4 changes: 4 additions & 0 deletions manager/adminui/templates/connect.gohtml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
<label for="mtls" class="form-check-label">TLS with client certificates</label>
</div>
</fieldset>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="invalidUsername" name="invalidUsername"/>
<label class="form-check-label" for="invalidUsername">Invalid Username Allowed</label>
</div>
<input type="submit" name="Register" value="Register" class="btn btn-primary">
</form>
</div>
Expand Down
8 changes: 8 additions & 0 deletions manager/adminui/templates/post-connect.gohtml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@
<label for="password">Password:</label>
<input type="text" class="form-control" id="password" name="password" value="{{ .password }}" readonly>
</div>
<div class="form-group">
<label for="invalidUsername">Invalid Username:</label>
{{- if eq .invalidUsername "on" }}
<input type="text" class="form-control" id="invalidUsername" name="invalidUsername" value="Allowed" readonly>
{{- else }}
<input type="text" class="form-control" id="invalidUsername" name="invalidUsername" value="Disallowed" readonly>
{{- end }}
</div>
{{ end }}
{{- if .clientCert }}
<div class="form-group">
Expand Down
10 changes: 7 additions & 3 deletions manager/api/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ so it can been configured when it sends a boot notification.
```json
{
"securityProfile": 0,
"base64SHA256Password": "string"
"base64SHA256Password": "string",
"invalidUsernameAllowed": true
}
```

Expand Down Expand Up @@ -210,7 +211,8 @@ the charge station
```json
{
"securityProfile": 0,
"base64SHA256Password": "string"
"base64SHA256Password": "string",
"invalidUsernameAllowed": true
}
```

Expand Down Expand Up @@ -721,7 +723,8 @@ This operation does not require authentication
```json
{
"securityProfile": 0,
"base64SHA256Password": "string"
"base64SHA256Password": "string",
"invalidUsernameAllowed": true
}

```
Expand All @@ -734,6 +737,7 @@ Connection details for a charge station
|---|---|---|---|---|
|securityProfile|integer|true|none|The security profile to use for the charge station: * `0` - unsecured transport with basic auth * `1` - TLS with basic auth * `2` - TLS with client certificate|
|base64SHA256Password|string|false|none|The base64 encoded, SHA-256 hash of the charge station password|
|invalidUsernameAllowed|boolean|false|none|If set to true then an invalid username will not prevent the charge station connecting|

<h2 id="tocS_ChargeStationSettings">ChargeStationSettings</h2>
<!-- backwards compatibility -->
Expand Down
3 changes: 3 additions & 0 deletions manager/api/api-spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,9 @@ components:
type: "string"
maxLength: 64
description: "The base64 encoded, SHA-256 hash of the charge station password"
invalidUsernameAllowed:
type: "boolean"
description: "If set to true then an invalid username will not prevent the charge station connecting"
ChargeStationSettings:
type: "object"
description: "Settings for a charge station"
Expand Down
Loading

0 comments on commit 8f52de0

Please sign in to comment.