-
Notifications
You must be signed in to change notification settings - Fork 23
fix(sbi): enforce inbound OAuth2 on NEF service routes #23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 7 commits
b16537c
9956662
8f787a7
1fa8e77
f3d06e8
b787c67
a732272
994069e
45af68c
4436e69
bb6382d
30ca109
2559d11
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,15 +1,22 @@ | ||
| package processor | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "context" | ||
| "fmt" | ||
| "io" | ||
| "net/http" | ||
|
|
||
| "github.com/free5gc/nef/internal/logger" | ||
| "github.com/free5gc/openapi" | ||
| "github.com/free5gc/openapi/models" | ||
| "github.com/free5gc/util/metrics/sbi" | ||
| "github.com/gin-gonic/gin" | ||
| "golang.org/x/oauth2" | ||
| ) | ||
|
|
||
| var afCallbackHTTPClient = &http.Client{} | ||
|
||
|
|
||
| func (p *Processor) SmfNotification( | ||
| c *gin.Context, | ||
| eeNotif *models.NsmfEventExposureNotification, | ||
|
|
@@ -25,9 +32,100 @@ func (p *Processor) SmfNotification( | |
| } | ||
|
|
||
| af.Mu.RLock() | ||
| defer af.Mu.RUnlock() | ||
| notifDestination := "" | ||
| if sub.TiSub != nil { | ||
| notifDestination = sub.TiSub.NotificationDestination | ||
| } | ||
| af.Mu.RUnlock() | ||
|
|
||
| if notifDestination == "" { | ||
| pd := openapi.ProblemDetailsSystemFailure("AF notification destination is empty") | ||
| c.Set(sbi.IN_PB_DETAILS_CTX_STR, pd.Cause) | ||
| c.JSON(http.StatusInternalServerError, pd) | ||
| return | ||
| } | ||
|
|
||
| afCallbackTokenCtx, pd, err := p.Context().GetTokenCtx( | ||
| models.ServiceName("nnef-callback"), models.NrfNfManagementNfType_AF) | ||
| if err != nil { | ||
| logger.TrafInfluLog.Errorf("Get token for AF callback failed: %+v", pd) | ||
| failure := openapi.ProblemDetailsSystemFailure("get token for AF callback failed") | ||
| if pd != nil && pd.Cause != "" { | ||
| c.Set(sbi.IN_PB_DETAILS_CTX_STR, pd.Cause) | ||
| } else { | ||
| c.Set(sbi.IN_PB_DETAILS_CTX_STR, failure.Cause) | ||
| } | ||
| c.JSON(http.StatusBadGateway, failure) | ||
| return | ||
| } | ||
|
|
||
| if err := postSmfEventExposureNotificationToAf(notifDestination, eeNotif, afCallbackTokenCtx); err != nil { | ||
| logger.TrafInfluLog.Errorf("Forward SMF notification to AF failed: %v", err) | ||
| pd := openapi.ProblemDetailsSystemFailure(err.Error()) | ||
| c.Set(sbi.IN_PB_DETAILS_CTX_STR, pd.Cause) | ||
| c.JSON(http.StatusBadGateway, pd) | ||
| return | ||
| } | ||
|
|
||
| c.Status(http.StatusNoContent) | ||
| } | ||
|
|
||
| func postSmfEventExposureNotificationToAf( | ||
| notifDestination string, | ||
| eeNotif *models.NsmfEventExposureNotification, | ||
| requestCtx context.Context, | ||
| ) error { | ||
| reqBody, err := openapi.Serialize(eeNotif, "application/json") | ||
| if err != nil { | ||
| return fmt.Errorf("serialize SMF notification failed: %w", err) | ||
| } | ||
| if requestCtx == nil { | ||
| requestCtx = context.Background() | ||
| } | ||
|
|
||
| httpReq, err := http.NewRequestWithContext(requestCtx, http.MethodPost, notifDestination, bytes.NewReader(reqBody)) | ||
| if err != nil { | ||
| return fmt.Errorf("create AF callback request failed: %w", err) | ||
| } | ||
| httpReq.Header.Set("Content-Type", "application/json") | ||
| if err = bindOAuthTokenToRequest(httpReq, requestCtx); err != nil { | ||
| return fmt.Errorf("bind OAuth2 token for AF callback failed: %w", err) | ||
| } | ||
|
|
||
| httpRsp, err := afCallbackHTTPClient.Do(httpReq) | ||
| if err != nil { | ||
| return fmt.Errorf("send AF callback failed: %w", err) | ||
| } | ||
| defer func() { | ||
| if _, copyErr := io.Copy(io.Discard, httpRsp.Body); copyErr != nil { | ||
| logger.TrafInfluLog.Warnf("drain AF callback response body failed: %v", copyErr) | ||
| } | ||
| if closeErr := httpRsp.Body.Close(); closeErr != nil { | ||
| logger.TrafInfluLog.Warnf("close AF callback response body failed: %v", closeErr) | ||
| } | ||
| }() | ||
|
|
||
| // TODO: Notify AF | ||
| if httpRsp.StatusCode < http.StatusOK || httpRsp.StatusCode >= http.StatusMultipleChoices { | ||
| return fmt.Errorf("AF callback returned status code %d", httpRsp.StatusCode) | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| func bindOAuthTokenToRequest(req *http.Request, requestCtx context.Context) error { | ||
| if requestCtx == nil { | ||
| return nil | ||
| } | ||
|
|
||
| tok, ok := requestCtx.Value(openapi.ContextOAuth2).(oauth2.TokenSource) | ||
| if !ok { | ||
| return nil | ||
| } | ||
|
|
||
| c.JSON(http.StatusOK, nil) | ||
| latestToken, err := tok.Token() | ||
| if err != nil { | ||
| return err | ||
| } | ||
| latestToken.SetAuthHeader(req) | ||
| return nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,97 @@ | ||
| package processor | ||
|
|
||
| import ( | ||
| "context" | ||
| "io" | ||
| "net/http" | ||
| "strings" | ||
| "testing" | ||
|
|
||
| "github.com/free5gc/openapi" | ||
| "github.com/free5gc/openapi/models" | ||
| "golang.org/x/oauth2" | ||
| ) | ||
|
|
||
| type roundTripFunc func(*http.Request) (*http.Response, error) | ||
|
|
||
| func (f roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { | ||
| return f(req) | ||
| } | ||
|
|
||
| func TestBindOAuthTokenToRequest(t *testing.T) { | ||
| t.Run("context without token source", func(t *testing.T) { | ||
| req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, "http://example.com", nil) | ||
| if err != nil { | ||
| t.Fatalf("create request failed: %v", err) | ||
| } | ||
|
|
||
| err = bindOAuthTokenToRequest(req, context.TODO()) | ||
| if err != nil { | ||
| t.Fatalf("bind token failed: %v", err) | ||
| } | ||
| if got := req.Header.Get("Authorization"); got != "" { | ||
| t.Fatalf("unexpected authorization header: %q", got) | ||
| } | ||
| }) | ||
|
|
||
| t.Run("context with token source", func(t *testing.T) { | ||
| req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, "http://example.com", nil) | ||
| if err != nil { | ||
| t.Fatalf("create request failed: %v", err) | ||
| } | ||
|
|
||
| tok := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: "abc123", TokenType: "Bearer"}) | ||
| tokenCtx := context.WithValue(context.Background(), openapi.ContextOAuth2, tok) | ||
|
|
||
| err = bindOAuthTokenToRequest(req, tokenCtx) | ||
| if err != nil { | ||
| t.Fatalf("bind token failed: %v", err) | ||
| } | ||
| if got := req.Header.Get("Authorization"); got != "Bearer abc123" { | ||
| t.Fatalf("authorization header = %q, want %q", got, "Bearer abc123") | ||
| } | ||
| }) | ||
| } | ||
|
|
||
| func TestPostSmfEventExposureNotificationToAfWithToken(t *testing.T) { | ||
| originalClient := afCallbackHTTPClient | ||
| t.Cleanup(func() { afCallbackHTTPClient = originalClient }) | ||
|
|
||
| afCallbackHTTPClient = &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { | ||
| if got := req.Header.Get("Authorization"); got != "Bearer token-for-af" { | ||
| t.Fatalf("authorization header = %q, want %q", got, "Bearer token-for-af") | ||
| } | ||
| return &http.Response{ | ||
| StatusCode: http.StatusNoContent, | ||
| Body: io.NopCloser(strings.NewReader("")), | ||
| Header: make(http.Header), | ||
| }, nil | ||
| })} | ||
|
|
||
| tok := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: "token-for-af", TokenType: "Bearer"}) | ||
| tokenCtx := context.WithValue(context.Background(), openapi.ContextOAuth2, tok) | ||
|
|
||
| eeNotif := &models.NsmfEventExposureNotification{NotifId: "notif-1"} | ||
| if err := postSmfEventExposureNotificationToAf("http://af.example.com/notify", eeNotif, tokenCtx); err != nil { | ||
| t.Fatalf("post callback failed: %v", err) | ||
| } | ||
| } | ||
|
|
||
| func TestPostSmfEventExposureNotificationToAfNon2xx(t *testing.T) { | ||
| originalClient := afCallbackHTTPClient | ||
| t.Cleanup(func() { afCallbackHTTPClient = originalClient }) | ||
|
|
||
| afCallbackHTTPClient = &http.Client{Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) { | ||
| return &http.Response{ | ||
| StatusCode: http.StatusForbidden, | ||
| Body: io.NopCloser(strings.NewReader("forbidden")), | ||
| Header: make(http.Header), | ||
| }, nil | ||
| })} | ||
|
|
||
| eeNotif := &models.NsmfEventExposureNotification{NotifId: "notif-2"} | ||
| err := postSmfEventExposureNotificationToAf("http://af.example.com/notify", eeNotif, context.TODO()) | ||
| if err == nil { | ||
| t.Fatal("expected error when AF callback returns non-2xx") | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.