diff --git a/models/yang/annotations/openconfig-aaa-annot.yang b/models/yang/annotations/openconfig-aaa-annot.yang new file mode 100644 index 000000000..db2970257 --- /dev/null +++ b/models/yang/annotations/openconfig-aaa-annot.yang @@ -0,0 +1,125 @@ +module openconfig-aaa-annot { + + yang-version "1"; + + namespace "http://openconfig.net/yang/aaa-annot"; + prefix "oc-aaa-annot"; + + import sonic-extensions { prefix sonic-ext; } + import openconfig-aaa { prefix oc-aaa; } + import openconfig-aaa-sonic-ext { prefix oc-aaa-sonic-ext; } + + organization + "SONiC"; + + contact + "SONiC"; + + description + "OpenConfig AAA YANG annotations for SONiC transformer mapping"; + + revision 2024-01-20 { + description + "Initial revision."; + } + + deviation /oc-aaa:aaa/oc-aaa:authentication { + deviate add { + sonic-ext:table-name "AAA"; + sonic-ext:key-transformer "aaa_tbl_key_xfmr"; + } + } + + deviation /oc-aaa:aaa/oc-aaa:authentication/oc-aaa:config/oc-aaa:authentication-method { + deviate add { + sonic-ext:field-name "login"; + sonic-ext:field-transformer "aaa_auth_method_xfmr"; + } + } + + deviation /oc-aaa:aaa/oc-aaa:authentication/oc-aaa:state/oc-aaa:authentication-method { + deviate add { + sonic-ext:field-name "login"; + sonic-ext:field-transformer "aaa_auth_method_xfmr"; + } + } + + deviation /oc-aaa:aaa/oc-aaa:authentication/oc-aaa:config/oc-aaa-sonic-ext:failthrough { + deviate add { + sonic-ext:field-name "failthrough"; + } + } + + deviation /oc-aaa:aaa/oc-aaa:authentication/oc-aaa:config/oc-aaa-sonic-ext:fallback { + deviate add { + sonic-ext:field-name "fallback"; + } + } + + deviation /oc-aaa:aaa/oc-aaa:authentication/oc-aaa:config/oc-aaa-sonic-ext:debug { + deviate add { + sonic-ext:field-name "debug"; + } + } + + deviation /oc-aaa:aaa/oc-aaa:authentication/oc-aaa:state/oc-aaa-sonic-ext:failthrough { + deviate add { + sonic-ext:field-name "failthrough"; + } + } + + deviation /oc-aaa:aaa/oc-aaa:authentication/oc-aaa:state/oc-aaa-sonic-ext:fallback { + deviate add { + sonic-ext:field-name "fallback"; + } + } + + deviation /oc-aaa:aaa/oc-aaa:authentication/oc-aaa:state/oc-aaa-sonic-ext:debug { + deviate add { + sonic-ext:field-name "debug"; + } + } + + deviation /oc-aaa:aaa/oc-aaa:authorization { + deviate add { + sonic-ext:table-name "AAA"; + sonic-ext:key-transformer "aaa_tbl_key_xfmr"; + } + } + + deviation /oc-aaa:aaa/oc-aaa:authorization/oc-aaa:config/oc-aaa:authorization-method { + deviate add { + sonic-ext:field-name "login"; + sonic-ext:field-transformer "aaa_authz_method_xfmr"; + } + } + + deviation /oc-aaa:aaa/oc-aaa:authorization/oc-aaa:state/oc-aaa:authorization-method { + deviate add { + sonic-ext:field-name "login"; + sonic-ext:field-transformer "aaa_authz_method_xfmr"; + } + } + + deviation /oc-aaa:aaa/oc-aaa:accounting { + deviate add { + sonic-ext:table-name "AAA"; + sonic-ext:key-transformer "aaa_tbl_key_xfmr"; + } + } + + deviation /oc-aaa:aaa/oc-aaa:accounting/oc-aaa:config/oc-aaa:accounting-method { + deviate add { + sonic-ext:field-name "login"; + sonic-ext:field-transformer "aaa_acct_method_xfmr"; + } + } + + deviation /oc-aaa:aaa/oc-aaa:accounting/oc-aaa:state/oc-aaa:accounting-method { + deviate add { + sonic-ext:field-name "login"; + sonic-ext:field-transformer "aaa_acct_method_xfmr"; + } + } + +} diff --git a/models/yang/extensions/openconfig-aaa-sonic-ext.yang b/models/yang/extensions/openconfig-aaa-sonic-ext.yang new file mode 100644 index 000000000..1b24ff34e --- /dev/null +++ b/models/yang/extensions/openconfig-aaa-sonic-ext.yang @@ -0,0 +1,74 @@ +module openconfig-aaa-sonic-ext { + + yang-version "1"; + + namespace "http://openconfig.net/yang/aaa/sonic-ext"; + prefix "oc-aaa-sonic-ext"; + + import openconfig-aaa { prefix oc-aaa; } + + organization + "SONiC"; + + contact + "SONiC"; + + description + "SONiC-specific augmentations to the OpenConfig AAA model. + Adds failthrough, fallback, and debug leaves to the + authentication config container."; + + revision 2024-01-20 { + description + "Initial revision."; + } + + augment /oc-aaa:aaa/oc-aaa:authentication/oc-aaa:config { + leaf failthrough { + type boolean; + default false; + description + "When set to true, authentication is attempted on + the next configured server/local in the list upon + failure."; + } + + leaf fallback { + type boolean; + default false; + description + "Allow AAA fallback to local authentication when + remote authentication servers are unreachable."; + } + + leaf debug { + type boolean; + default false; + description + "Enable or disable AAA debugging."; + } + } + + augment /oc-aaa:aaa/oc-aaa:authentication/oc-aaa:state { + leaf failthrough { + type boolean; + description + "When set to true, authentication is attempted on + the next configured server/local in the list upon + failure."; + } + + leaf fallback { + type boolean; + description + "Allow AAA fallback to local authentication when + remote authentication servers are unreachable."; + } + + leaf debug { + type boolean; + description + "Enable or disable AAA debugging."; + } + } +} diff --git a/translib/transformer/xfmr_aaa.go b/translib/transformer/xfmr_aaa.go new file mode 100644 index 000000000..381410e6a --- /dev/null +++ b/translib/transformer/xfmr_aaa.go @@ -0,0 +1,203 @@ +//////////////////////////////////////////////////////////////////////////////// +// // +// Copyright 2024 Broadcom. The term Broadcom refers to Broadcom Inc. and/or // +// its subsidiaries. // +// // +// Licensed under the Apache License, Version 2.0 (the "License"); // +// you may not use this file except in compliance with the License. // +// You may obtain a copy of the License at // +// // +// http://www.apache.org/licenses/LICENSE-2.0 // +// // +// Unless required by applicable law or agreed to in writing, software // +// distributed under the License is distributed on an "AS IS" BASIS, // +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // +// See the License for the specific language governing permissions and // +// limitations under the License. // +// // +//////////////////////////////////////////////////////////////////////////////// + +package transformer + +import ( + "fmt" + "strings" + + log "github.com/golang/glog" +) + +func init() { + XlateFuncBind("aaa_tbl_key_xfmr", aaa_tbl_key_xfmr) + XlateFuncBind("YangToDb_aaa_auth_method_xfmr", YangToDb_aaa_auth_method_xfmr) + XlateFuncBind("DbToYang_aaa_auth_method_xfmr", DbToYang_aaa_auth_method_xfmr) + XlateFuncBind("YangToDb_aaa_authz_method_xfmr", YangToDb_aaa_authz_method_xfmr) + XlateFuncBind("DbToYang_aaa_authz_method_xfmr", DbToYang_aaa_authz_method_xfmr) + XlateFuncBind("YangToDb_aaa_acct_method_xfmr", YangToDb_aaa_acct_method_xfmr) + XlateFuncBind("DbToYang_aaa_acct_method_xfmr", DbToYang_aaa_acct_method_xfmr) +} + +var aaa_tbl_key_xfmr KeyXfmrYangToDb = func(inParams XfmrParams) (string, error) { + log.Info("aaa_tbl_key_xfmr: ", inParams.uri) + pathInfo := NewPathInfo(inParams.uri) + uriPath := pathInfo.Template + + if strings.Contains(uriPath, "authorization") { + return "authorization", nil + } else if strings.Contains(uriPath, "accounting") { + return "accounting", nil + } + return "authentication", nil +} + +func aaaMethodListToLoginString(methods []interface{}) string { + var parts []string + for _, m := range methods { + s := fmt.Sprintf("%v", m) + trimmed := strings.TrimSpace(s) + if trimmed != "" { + parts = append(parts, trimmed) + } + } + return strings.Join(parts, ",") +} + +func aaaLoginStringToMethodList(loginStr string) []string { + var methods []string + if loginStr == "" { + return methods + } + parts := strings.Split(loginStr, ",") + for _, p := range parts { + trimmed := strings.TrimSpace(p) + if trimmed != "" { + methods = append(methods, trimmed) + } + } + return methods +} + +var YangToDb_aaa_auth_method_xfmr FieldXfmrYangToDb = func(inParams XfmrParams) (map[string]string, error) { + res_map := make(map[string]string) + log.Info("YangToDb_aaa_auth_method_xfmr: ", inParams.param) + + if inParams.param == nil { + return res_map, nil + } + + methods, ok := inParams.param.([]interface{}) + if ok && len(methods) > 0 { + res_map["login"] = aaaMethodListToLoginString(methods) + } else { + s := fmt.Sprintf("%v", inParams.param) + if s != "" { + res_map["login"] = s + } + } + return res_map, nil +} + +var DbToYang_aaa_auth_method_xfmr FieldXfmrDbtoYang = func(inParams XfmrParams) (map[string]interface{}, error) { + result := make(map[string]interface{}) + log.Info("DbToYang_aaa_auth_method_xfmr: ", inParams.key) + + data := (*inParams.dbDataMap)[inParams.curDb] + aaaEntry, ok := data["AAA"] + if !ok { + return result, nil + } + entry, ok := aaaEntry["authentication"] + if !ok { + return result, nil + } + + loginStr := entry.Get("login") + methods := aaaLoginStringToMethodList(loginStr) + if len(methods) > 0 { + result["authentication-method"] = methods + } + return result, nil +} + +var YangToDb_aaa_authz_method_xfmr FieldXfmrYangToDb = func(inParams XfmrParams) (map[string]string, error) { + res_map := make(map[string]string) + log.Info("YangToDb_aaa_authz_method_xfmr: ", inParams.param) + + if inParams.param == nil { + return res_map, nil + } + + methods, ok := inParams.param.([]interface{}) + if ok && len(methods) > 0 { + res_map["login"] = aaaMethodListToLoginString(methods) + } else { + s := fmt.Sprintf("%v", inParams.param) + if s != "" { + res_map["login"] = s + } + } + return res_map, nil +} + +var DbToYang_aaa_authz_method_xfmr FieldXfmrDbtoYang = func(inParams XfmrParams) (map[string]interface{}, error) { + result := make(map[string]interface{}) + log.Info("DbToYang_aaa_authz_method_xfmr: ", inParams.key) + + data := (*inParams.dbDataMap)[inParams.curDb] + aaaEntry, ok := data["AAA"] + if !ok { + return result, nil + } + entry, ok := aaaEntry["authorization"] + if !ok { + return result, nil + } + + loginStr := entry.Get("login") + methods := aaaLoginStringToMethodList(loginStr) + if len(methods) > 0 { + result["authorization-method"] = methods + } + return result, nil +} + +var YangToDb_aaa_acct_method_xfmr FieldXfmrYangToDb = func(inParams XfmrParams) (map[string]string, error) { + res_map := make(map[string]string) + log.Info("YangToDb_aaa_acct_method_xfmr: ", inParams.param) + + if inParams.param == nil { + return res_map, nil + } + + methods, ok := inParams.param.([]interface{}) + if ok && len(methods) > 0 { + res_map["login"] = aaaMethodListToLoginString(methods) + } else { + s := fmt.Sprintf("%v", inParams.param) + if s != "" { + res_map["login"] = s + } + } + return res_map, nil +} + +var DbToYang_aaa_acct_method_xfmr FieldXfmrDbtoYang = func(inParams XfmrParams) (map[string]interface{}, error) { + result := make(map[string]interface{}) + log.Info("DbToYang_aaa_acct_method_xfmr: ", inParams.key) + + data := (*inParams.dbDataMap)[inParams.curDb] + aaaEntry, ok := data["AAA"] + if !ok { + return result, nil + } + entry, ok := aaaEntry["accounting"] + if !ok { + return result, nil + } + + loginStr := entry.Get("login") + methods := aaaLoginStringToMethodList(loginStr) + if len(methods) > 0 { + result["accounting-method"] = methods + } + return result, nil +} diff --git a/translib/transformer/xfmr_aaa_test.go b/translib/transformer/xfmr_aaa_test.go new file mode 100644 index 000000000..69f3ac4e5 --- /dev/null +++ b/translib/transformer/xfmr_aaa_test.go @@ -0,0 +1,166 @@ +package transformer + +import ( + "testing" +) + +func TestAaaMethodListToLoginString(t *testing.T) { + tests := []struct { + name string + input []interface{} + expected string + }{ + { + name: "single method", + input: []interface{}{"tacacs+"}, + expected: "tacacs+", + }, + { + name: "multiple methods", + input: []interface{}{"tacacs+", "local"}, + expected: "tacacs+,local", + }, + { + name: "three methods", + input: []interface{}{"tacacs+", "local", "radius"}, + expected: "tacacs+,local,radius", + }, + { + name: "empty list", + input: []interface{}{}, + expected: "", + }, + { + name: "methods with spaces", + input: []interface{}{" tacacs+ ", " local "}, + expected: "tacacs+,local", + }, + { + name: "filter empty strings", + input: []interface{}{"tacacs+", "", "local"}, + expected: "tacacs+,local", + }, + { + name: "single local", + input: []interface{}{"local"}, + expected: "local", + }, + { + name: "all supported methods", + input: []interface{}{"ldap", "tacacs+", "local", "radius", "default"}, + expected: "ldap,tacacs+,local,radius,default", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := aaaMethodListToLoginString(tt.input) + if result != tt.expected { + t.Errorf("aaaMethodListToLoginString(%v) = %q, want %q", tt.input, result, tt.expected) + } + }) + } +} + +func TestAaaLoginStringToMethodList(t *testing.T) { + tests := []struct { + name string + input string + expected []string + }{ + { + name: "single method", + input: "tacacs+", + expected: []string{"tacacs+"}, + }, + { + name: "multiple methods", + input: "tacacs+,local", + expected: []string{"tacacs+", "local"}, + }, + { + name: "three methods", + input: "tacacs+,local,radius", + expected: []string{"tacacs+", "local", "radius"}, + }, + { + name: "empty string", + input: "", + expected: nil, + }, + { + name: "methods with spaces", + input: " tacacs+ , local ", + expected: []string{"tacacs+", "local"}, + }, + { + name: "single local", + input: "local", + expected: []string{"local"}, + }, + { + name: "all methods", + input: "ldap,tacacs+,local,radius,default", + expected: []string{"ldap", "tacacs+", "local", "radius", "default"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := aaaLoginStringToMethodList(tt.input) + if tt.expected == nil { + if result != nil { + t.Errorf("aaaLoginStringToMethodList(%q) = %v, want nil", tt.input, result) + } + return + } + if len(result) != len(tt.expected) { + t.Errorf("aaaLoginStringToMethodList(%q) returned %d items, want %d", tt.input, len(result), len(tt.expected)) + return + } + for i, v := range result { + if v != tt.expected[i] { + t.Errorf("aaaLoginStringToMethodList(%q)[%d] = %q, want %q", tt.input, i, v, tt.expected[i]) + } + } + }) + } +} + +func TestAaaMethodListRoundTrip(t *testing.T) { + tests := []struct { + name string + methods []interface{} + }{ + { + name: "tacacs+ and local", + methods: []interface{}{"tacacs+", "local"}, + }, + { + name: "single method", + methods: []interface{}{"local"}, + }, + { + name: "all methods", + methods: []interface{}{"ldap", "tacacs+", "local", "radius"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + loginStr := aaaMethodListToLoginString(tt.methods) + result := aaaLoginStringToMethodList(loginStr) + + if len(result) != len(tt.methods) { + t.Errorf("Round trip failed: input %v -> %q -> %v", tt.methods, loginStr, result) + return + } + for i, v := range result { + expected := tt.methods[i].(string) + if v != expected { + t.Errorf("Round trip mismatch at index %d: got %q, want %q", i, v, expected) + } + } + }) + } +}