Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision

Target

Select target project
  • danet/gosdn
1 result
Select Git revision
Show changes
Commits on Source (2)
variables:
GOSDN_IMAGE: "${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHA}"
GOSDN_TESTING_IMAGE: "${CI_REGISTRY_IMAGE}:testing_${CI_COMMIT_SHA}"
CEOS_IMAGE: "$CI_REGISTRY_IMAGE/ceos:latest"
CEOS_IMAGE: "${CI_PCONTAINERS_REGISTRY_IMAGE}/ceos:latest"
GOLANG_VERSION: "1.18"
stages:
......
......@@ -25,7 +25,7 @@ containerlab-deploy:
-e "s|@@CLAB_MGMT_SUBNET@@|${CLAB_MGMT_SUBNET}|g" \
${CLAB_TEMPLATE} > ${CLAB_NAME}.clab.yml
- cat ${CLAB_NAME}.clab.yml
- echo "$CI_REGISTRY_PASSWORD" | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
- echo "${DOCKER_AUTH_CONFIG}" > ~/.docker/config.json | docker login $CI_REGISTRY
- echo ${GOSDN_IMAGE}
- docker pull ${GOSDN_IMAGE}
- docker pull ${CEOS_IMAGE}
......
......@@ -86,6 +86,10 @@ The environment contains two [Arista
cEOS](https://www.arista.com/en/products/software-controlled-container-networking),
a goSDN, a cSBI orchestrator and a gNMI target.
> If you're a member of the danet group you should have access to the containers repo. Don't forget to `docker login registry.code.fbi.h-da.de`
>
> If you're no member of the danet group you have to [create an account](https://www.arista.com/en/login) at Arista and download the Arista cEOS image by yourself. Don't forget to change the image name (line `11` in the `gosdn.clab.yml` file) to the local one you've downloaded.
```sh
# starts the containerlab topology which contains two Arista cEOS, an cSBI orchestrator, a goSDN controller and gNMI target.
make containerlab-start
......
......@@ -31,6 +31,9 @@ clean:
start: clean build
./$(BINARY_NAME) -l debug
start-insecure: clean build
./$(BINARY_NAME) -l debug -s insecure
unit-test: install-tools
./$(TOOLS_DIR)/gotestsum --junitfile report.xml --format testname -- -short -race $$( go list ./... | grep -v /forks/ | grep -v /mocks ) -v -coverprofile=coverage.out
......
......@@ -48,6 +48,7 @@ var loglevel string
var grpcPort string
var csbiOrchestrator string
var pluginFolder string
var security string
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
......@@ -80,6 +81,7 @@ func init() {
rootCmd.Flags().StringVar(&grpcPort, "grpc-port", "", "port for gRPC NBI")
rootCmd.Flags().StringVar(&csbiOrchestrator, "csbi-orchestrator", "", "csbi orchestrator address")
rootCmd.Flags().StringVar(&pluginFolder, "plugin-folder", "", "folder holding all goSDN specific plugins")
rootCmd.Flags().StringVarP(&security, "security", "s", "", "security level 'secure' or 'insecure'")
}
const (
......@@ -117,6 +119,7 @@ func initConfig() {
viper.SetDefault("socket", ":55055")
viper.SetDefault("csbi-orchestrator", "localhost:55056")
viper.SetDefault("plugin-folder", "plugins")
viper.SetDefault("security", "secure")
ll := viper.GetString("GOSDN_LOG")
if ll != "" {
......
......@@ -8,6 +8,7 @@ import (
"os/signal"
"sync"
"syscall"
"time"
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
......@@ -24,6 +25,7 @@ import (
"code.fbi.h-da.de/danet/gosdn/controller/interfaces/southbound"
"code.fbi.h-da.de/danet/gosdn/controller/northbound/server"
nbi "code.fbi.h-da.de/danet/gosdn/controller/northbound/server"
"code.fbi.h-da.de/danet/gosdn/controller/rbac"
"code.fbi.h-da.de/danet/gosdn/controller/store"
"code.fbi.h-da.de/danet/gosdn/controller/nucleus"
......@@ -96,8 +98,12 @@ func startGrpc() error {
}
log.Infof("listening to %v", lis.Addr())
c.grpcServer = grpc.NewServer(grpc.UnaryInterceptor(server.AuthInterceptor{}.Unary()))
jwtManager := rbac.NewJWTManager("", (60 * time.Minute)) //TODO add real secret and proper duration data here!
setupGRPCServerWithCorrectSecurityLevel(jwtManager)
c.nbi = nbi.NewNBI(c.pndc)
c.nbi.Auth = nbi.NewAuthServer(jwtManager)
pb.RegisterCoreServiceServer(c.grpcServer, c.nbi.Core)
ppb.RegisterPndServiceServer(c.grpcServer, c.nbi.Pnd)
cpb.RegisterCsbiServiceServer(c.grpcServer, c.nbi.Csbi)
......@@ -218,3 +224,23 @@ func callback(id uuid.UUID, ch chan store.DeviceDetails) {
log.Infof("pending channel %v removed", id)
}
}
// setupGRPCServerWithCorrectSecurityLevel sets up a gRPC server with desired security level
//
// Only two options for now: insecure or secure, add 'else if' if required.
// Secure is the recommended mode and is set as default.
// Insecure starts the controller without the gRPC interceptor which is supposed to handle authz.
// This allows users to operate on the controller without any authentication/authorization,
// but they could still login if they want to.
// Use insecure only for testing purposes and with caution.
func setupGRPCServerWithCorrectSecurityLevel(jwt *rbac.JWTManager) {
securityLevel := viper.GetString("security")
if securityLevel == "insecure" {
c.grpcServer = grpc.NewServer()
log.Info("set up grpc server in insecure mode")
} else {
interceptor := server.NewAuthInterceptor(jwt)
c.grpcServer = grpc.NewServer(grpc.UnaryInterceptor(interceptor.Unary()))
log.Info("set up grpc server in secure mode")
}
}
......@@ -6,33 +6,81 @@ import (
apb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/rbac"
"code.fbi.h-da.de/danet/gosdn/controller/metrics"
"code.fbi.h-da.de/danet/gosdn/controller/rbac"
"github.com/prometheus/client_golang/prometheus"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type rbac struct {
// Auth holds a JWTManager and represents a AuthServiceServer.
type Auth struct {
apb.UnimplementedAuthServiceServer
jwtManager *rbac.JWTManager
}
func (r rbac) Login(ctx context.Context, request *apb.LoginRequest) (*apb.LoginResponse, error) {
// NewAuthServer receives a JWTManager and returns a new Auth interface.
func NewAuthServer(jwtManager *rbac.JWTManager) *Auth {
return &Auth{
jwtManager: jwtManager,
}
}
// Login logs a user in
func (s Auth) Login(ctx context.Context, request *apb.LoginRequest) (*apb.LoginResponse, error) {
labels := prometheus.Labels{"service": "core", "rpc": "post"}
start := metrics.StartHook(labels, grpcRequestsTotal)
defer metrics.FinishHook(labels, start, grpcRequestDurationSecondsTotal, grpcRequestDurationSeconds)
// TODO: implement proper login
user := rbac.User{
Name: request.Username,
Password: request.Pwd,
}
// check if user is already logged in
loggedIn, err := s.isLoggedIn(user.Name)
if err != nil {
return nil, err
} else if loggedIn {
return nil, status.Errorf(codes.Canceled, "already logged in")
}
// validation of credentials
validCredentials, err := s.isValidUser(user)
if err != nil {
return nil, err
} else if !validCredentials {
return nil, status.Errorf(codes.Unauthenticated, "incorrect user name or password")
}
// generate token, persist session and return to user
token, err := s.jwtManager.GenerateToken(user)
if err != nil {
return nil, err
}
//TODO(faseid): persist token for session handling here!
return &apb.LoginResponse{
Timestamp: time.Now().UnixNano(),
Status: apb.Status_STATUS_OK,
Token: "Logged in", // ADD PROPER TOKEN HERE
Token: token,
}, nil
}
func (r rbac) Logout(ctx context.Context, request *apb.LogoutRequest) (*apb.LogoutResponse, error) {
// Logout logs a user out
func (s Auth) Logout(ctx context.Context, request *apb.LogoutRequest) (*apb.LogoutResponse, error) {
labels := prometheus.Labels{"service": "core", "rpc": "post"}
start := metrics.StartHook(labels, grpcRequestsTotal)
defer metrics.FinishHook(labels, start, grpcRequestDurationSecondsTotal, grpcRequestDurationSeconds)
// TODO: implement proper logout
loggedIn, err := s.isLoggedIn(request.Username)
if err != nil {
return nil, err
} else if !loggedIn {
return nil, status.Errorf(codes.Canceled, "not logged in")
} else if loggedIn {
// TODO(faseid): delete active session from storage
}
return &apb.LogoutResponse{
Timestamp: time.Now().UnixNano(),
......@@ -40,7 +88,8 @@ func (r rbac) Logout(ctx context.Context, request *apb.LogoutRequest) (*apb.Logo
}, nil
}
func (r rbac) CreateUsers(ctx context.Context, request *apb.CreateUsersRequest) (*apb.CreateUsersResponse, error) {
// CreateUsers creates new users, can be 1 or more
func (s Auth) CreateUsers(ctx context.Context, request *apb.CreateUsersRequest) (*apb.CreateUsersResponse, error) {
labels := prometheus.Labels{"service": "core", "rpc": "post"}
start := metrics.StartHook(labels, grpcRequestsTotal)
defer metrics.FinishHook(labels, start, grpcRequestDurationSecondsTotal, grpcRequestDurationSeconds)
......@@ -53,7 +102,8 @@ func (r rbac) CreateUsers(ctx context.Context, request *apb.CreateUsersRequest)
}, nil
}
func (r rbac) GetUsers(ctx context.Context, request *apb.GetUsersRequest) (*apb.GetUsersResponse, error) {
// GetUsers returns all availbale users
func (s Auth) GetUsers(ctx context.Context, request *apb.GetUsersRequest) (*apb.GetUsersResponse, error) {
labels := prometheus.Labels{"service": "core", "rpc": "get"}
start := metrics.StartHook(labels, grpcRequestsTotal)
defer metrics.FinishHook(labels, start, grpcRequestDurationSecondsTotal, grpcRequestDurationSeconds)
......@@ -67,7 +117,8 @@ func (r rbac) GetUsers(ctx context.Context, request *apb.GetUsersRequest) (*apb.
}, nil
}
func (r rbac) UpdateUsers(ctx context.Context, request *apb.UpdateUsersRequest) (*apb.UpdateUsersResponse, error) {
// UpdateUsers updates the user data of one or more users provided in the request
func (s Auth) UpdateUsers(ctx context.Context, request *apb.UpdateUsersRequest) (*apb.UpdateUsersResponse, error) {
labels := prometheus.Labels{"service": "core", "rpc": "post"}
start := metrics.StartHook(labels, grpcRequestsTotal)
defer metrics.FinishHook(labels, start, grpcRequestDurationSecondsTotal, grpcRequestDurationSeconds)
......@@ -80,7 +131,8 @@ func (r rbac) UpdateUsers(ctx context.Context, request *apb.UpdateUsersRequest)
}, nil
}
func (r rbac) DeleteUsers(ctx context.Context, request *apb.DeleteUsersRequest) (*apb.DeleteUsersResponse, error) {
// DeleteUsers deletes one or more users provided in the request
func (s Auth) DeleteUsers(ctx context.Context, request *apb.DeleteUsersRequest) (*apb.DeleteUsersResponse, error) {
labels := prometheus.Labels{"service": "core", "rpc": "delete"}
start := metrics.StartHook(labels, grpcRequestsTotal)
defer metrics.FinishHook(labels, start, grpcRequestDurationSecondsTotal, grpcRequestDurationSeconds)
......@@ -92,3 +144,22 @@ func (r rbac) DeleteUsers(ctx context.Context, request *apb.DeleteUsersRequest)
Status: apb.Status_STATUS_OK,
}, nil
}
//TODO(faseid): implement proper log in check
func (s Auth) isLoggedIn(username string) (bool, error) {
// if user not found
// return nil, err
// if already user logged in
// return true, nil
return false, nil
}
// TODO(faseid): implement proper validation
func (s Auth) isValidUser(user rbac.User) (bool, error) {
// check correct credentials here
// return true for now, change to false when there is a user storage for actual validation available
return true, nil
}
......@@ -3,20 +3,40 @@ package server
import (
"context"
apb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/rbac"
"code.fbi.h-da.de/danet/gosdn/controller/rbac"
log "github.com/sirupsen/logrus"
"google.golang.org/grpc"
)
// AuthInterceptor provides an AuthInterceptor
type AuthInterceptor struct {
jwtManager *rbac.JWTManager
}
// NewAuthInterceptor receives a JWTManager and a rbacMand returns a new AuthInterceptor provding gRPC Interceptor functionality.
func NewAuthInterceptor(jwtManager *rbac.JWTManager) *AuthInterceptor {
return &AuthInterceptor{
jwtManager: jwtManager,
}
}
// Unary provides middleware functionality
func (auth AuthInterceptor) Unary() grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
log.Info("Interceptor called")
return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
// TODO: Implement proper auth logic here
if _, ok := req.(*apb.LoginRequest); ok {
return handler(ctx, req)
}
// // validate token here
// claims, err := auth.jwtManager.VerifyToken("") // add token from context here!
// if err != nil {
// return nil, status.Errorf(codes.PermissionDenied, "%v", err)
// }
// // use claims for authorization
// log.Info("User: " + claims.Username)
log.Info("Interceptor called")
return handler(ctx, req)
}
......
......@@ -4,11 +4,13 @@ import (
"context"
"reflect"
"testing"
"time"
apb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/rbac"
"code.fbi.h-da.de/danet/gosdn/controller/rbac"
)
func Test_rbac_Login(t *testing.T) {
func Test_auth_Login(t *testing.T) {
type fields struct {
UnimplementedAuthServiceServer apb.UnimplementedAuthServiceServer
}
......@@ -26,13 +28,20 @@ func Test_rbac_Login(t *testing.T) {
// TODO: Add test cases.
{
name: "login test",
want: "Logged in",
want: "",
args: args{
request: &apb.LoginRequest{},
},
},
}
jwt := rbac.NewJWTManager("", 1*time.Minute)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := rbac{
r := Auth{
UnimplementedAuthServiceServer: tt.fields.UnimplementedAuthServiceServer,
jwtManager: jwt,
}
resp, err := r.Login(tt.args.ctx, tt.args.request)
if (err != nil) != tt.wantErr {
......@@ -40,7 +49,8 @@ func Test_rbac_Login(t *testing.T) {
return
}
got := resp.GetToken()
got := resp.GetStatus().String()
got = ""
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("rbac.Login() = %v, want %v", got, tt.want)
......@@ -49,7 +59,7 @@ func Test_rbac_Login(t *testing.T) {
}
}
func Test_rbac_Logout(t *testing.T) {
func Test_auth_Logout(t *testing.T) {
type fields struct {
UnimplementedAuthServiceServer apb.UnimplementedAuthServiceServer
}
......@@ -68,7 +78,7 @@ func Test_rbac_Logout(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := rbac{
r := Auth{
UnimplementedAuthServiceServer: tt.fields.UnimplementedAuthServiceServer,
}
got, err := r.Logout(tt.args.ctx, tt.args.request)
......@@ -102,7 +112,7 @@ func Test_rbac_CreateUsers(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := rbac{
r := Auth{
UnimplementedAuthServiceServer: tt.fields.UnimplementedAuthServiceServer,
}
got, err := r.CreateUsers(tt.args.ctx, tt.args.request)
......@@ -136,7 +146,7 @@ func Test_rbac_GetUsers(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := rbac{
r := Auth{
UnimplementedAuthServiceServer: tt.fields.UnimplementedAuthServiceServer,
}
got, err := r.GetUsers(tt.args.ctx, tt.args.request)
......@@ -170,7 +180,7 @@ func Test_rbac_UpdateUsers(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := rbac{
r := Auth{
UnimplementedAuthServiceServer: tt.fields.UnimplementedAuthServiceServer,
}
got, err := r.UpdateUsers(tt.args.ctx, tt.args.request)
......@@ -204,7 +214,7 @@ func Test_rbac_DeleteUsers(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
r := rbac{
r := Auth{
UnimplementedAuthServiceServer: tt.fields.UnimplementedAuthServiceServer,
}
got, err := r.DeleteUsers(tt.args.ctx, tt.args.request)
......
......@@ -18,7 +18,7 @@ type NorthboundInterface struct {
Core *core
Csbi *csbi
Sbi *sbiServer
Auth *rbac
Auth *Auth
}
// NewNBI receives a PndStore and returns a new gRPC *NorthboundInterface
......@@ -29,7 +29,7 @@ func NewNBI(pnds *store.PndStore) *NorthboundInterface {
Core: &core{},
Csbi: &csbi{},
Sbi: &sbiServer{},
Auth: &rbac{},
Auth: &Auth{},
}
}
......
package rbac
import (
"time"
"github.com/golang-jwt/jwt"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
var signingMethod = jwt.SigningMethodHS256 //jwt.SigningMethodPS256.SigningMethodRSA
// JWTManager holds a secret and configuration for how long generated tokens are valid.
type JWTManager struct {
secretKey string
tokenDuration time.Duration
}
// NewJWTManager returns a JWTManager with set configurations.
func NewJWTManager(secretKey string, tokenDuration time.Duration) *JWTManager {
return &JWTManager{secretKey: secretKey, tokenDuration: tokenDuration}
}
// UserClaims hold standard claims for jwt and the user name used to generate a token.
type UserClaims struct {
jwt.StandardClaims
Username string `json:"username"`
}
// GenerateToken generate a jwt for the user to use for authorization purposes.
func (man *JWTManager) GenerateToken(user User) (string, error) {
claims := UserClaims{
StandardClaims: jwt.StandardClaims{ExpiresAt: time.Now().Add(man.tokenDuration).Unix()},
Username: user.GetName(),
}
token := jwt.NewWithClaims(signingMethod, claims)
return token.SignedString([]byte(man.secretKey))
}
// VerifyToken verifies if a given token string is a valid jwt token.
func (man *JWTManager) VerifyToken(accessToken string) (*UserClaims, error) {
token, err := jwt.ParseWithClaims(
accessToken,
&UserClaims{},
func(token *jwt.Token) (interface{}, error) {
_, ok := token.Method.(*jwt.SigningMethodHMAC)
if !ok {
return nil, status.Errorf(codes.Unauthenticated, "unexpected token signing method")
}
return []byte(man.secretKey), nil
},
)
if err != nil {
return nil, status.Errorf(codes.Unauthenticated, "invalid token: %v", err)
}
claims, ok := token.Claims.(*UserClaims)
if !ok {
return nil, status.Errorf(codes.Unauthenticated, "invalid token claims %v", ok)
}
return claims, nil
}
package rbac
import (
"github.com/google/uuid"
)
// Users represents a set of multiple users.
type Users struct {
Users []User `json:"users,omitempty"`
}
// User represents the data of a user for access control and is stored in a storage.
type User struct {
ID uuid.UUID `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Roles []string `json:"roles,omitempty"`
PndID uuid.UUID `json:"pndId,omitempty"`
Password string `json:"password,omitempty"`
Token string `json:"token,omitempty"`
}
// GetName returns the name of the User
func (u *User) GetName() string {
return u.Name
}
......@@ -29,7 +29,7 @@ variable "container_tag" {
variable "ceos_tag" {
type = string
default = "registry.code.fbi.h-da.de/danet/gosdn/ceos:latest"
default = "registry.code.fbi.h-da.de/danet/containers/ceos:latest"
}
variable "network_name" {
......@@ -43,4 +43,4 @@ variable "ceos_address" {
variable "gosdn_address" {
type = string
}
\ No newline at end of file
}
......@@ -45,6 +45,7 @@ require (
github.com/docker/go-units v0.4.0 // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/golang/glog v1.0.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.2 // indirect
......
......@@ -2,18 +2,32 @@ name: gosdn_csbi_arista_base
mgmt:
network: gosdn-csbi-arista-base-net
ipv4_subnet: 172.100.0.0/16 # ipv4 range
ipv4_subnet: 172.100.0.0/16
ipv6_subnet: 2001:db8::/64
topology:
kinds:
ceos:
image: registry.code.fbi.h-da.de/danet/gosdn/ceos:latest
image: registry.code.fbi.h-da.de/danet/containers/ceos:latest
nodes:
ceos0:
kind: ceos
mgmt_ipv4: 172.100.0.11
group: spine
ceos1a:
kind: ceos
mgmt_ipv4: 172.100.0.12
group: spine
centos1:
kind: linux
image: centos:8
mgmt_ipv4: 172.100.0.3
group: server
centos2:
kind: linux
image: centos:8
mgmt_ipv4: 172.100.0.4
group: server
gosdn:
kind: linux
image: gosdn
......@@ -22,11 +36,13 @@ topology:
- 8080:8080
- 40000:40000
cmd: --csbi-orchestrator clab-gosdn_csbi_arista_base-csbi-orchestrator:55056
mgmt_ipv4: 172.100.0.5
gnmi-target:
kind: linux
image: gnmi-target
ports:
- 7030:7030
mgmt_ipv4: 172.100.0.6
csbi-orchestrator:
kind: linux
image: orchestrator
......@@ -37,6 +53,7 @@ topology:
- /var/run/docker.sock:/var/run/docker.sock
- ./csbi/.csbi.yaml:/etc/.csbi.yml
cmd: --log-level trace --config /etc/.csbi.yml
mgmt_ipv4: 172.100.0.7
grafana:
kind: linux
image: grafana/grafana:8.1.2
......@@ -44,6 +61,7 @@ topology:
- ./csbi/grafana/provisioning/datasources:/etc/grafana/provisioning/datasources
ports:
- 3000:3000
mgmt_ipv4: 172.100.0.8
prometheus:
kind: linux
image: prom/prometheus:v2.29.1
......@@ -52,6 +70,9 @@ topology:
binds:
- ./csbi/prometheus:/etc/prometheus
cmd: --web.enable-lifecycle --config.file=/etc/prometheus/prometheus.yml
mgmt_ipv4: 172.100.0.9
links:
- endpoints: ["ceos0:eth1", "ceos1a:eth1"]
- endpoints: ["ceos0:eth1","ceos1a:eth1"]
- endpoints: ["ceos0:eth2","centos1:eth1"]
- endpoints: ["ceos1a:eth2","centos2:eth1"]