feat: user roles

This commit is contained in:
Johan Siebens
2022-06-22 07:47:09 +02:00
parent 32c396a972
commit 12eb258e1e
11 changed files with 109 additions and 44 deletions
+8
View File
@@ -22,6 +22,14 @@ type IAMPolicy struct {
Subs []string `json:"subs,omitempty"`
Emails []string `json:"emails,omitempty"`
Filters []string `json:"filters,omitempty"`
Roles map[string]UserRole `json:"roles,omitempty"`
}
func (i *IAMPolicy) GetRole(user User) UserRole {
if val, ok := i.Roles[user.Name]; ok {
return val
}
return UserRoleMember
}
func (i *IAMPolicy) EvaluatePolicy(identity *Identity) (bool, error) {
+12
View File
@@ -23,6 +23,18 @@ const (
UserTypePerson UserType = "person"
)
type UserRole string
const (
UserRoleNone UserRole = ""
UserRoleMember UserRole = "member"
UserRoleAdmin UserRole = "admin"
)
func (s UserRole) IsAdmin() bool {
return s == UserRoleAdmin
}
type User struct {
ID uint64 `gorm:"primary_key;autoIncrement:false"`
Name string
+2 -2
View File
@@ -12,7 +12,7 @@ import (
func (s *Service) GetACLPolicy(ctx context.Context, req *connect.Request[api.GetACLPolicyRequest]) (*connect.Response[api.GetACLPolicyResponse], error) {
principal := CurrentPrincipal(ctx)
if !principal.IsSystemAdmin() && !principal.TailnetMatches(req.Msg.TailnetId) {
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(req.Msg.TailnetId) {
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("permission denied"))
}
@@ -34,7 +34,7 @@ func (s *Service) GetACLPolicy(ctx context.Context, req *connect.Request[api.Get
func (s *Service) SetACLPolicy(ctx context.Context, req *connect.Request[api.SetACLPolicyRequest]) (*connect.Response[api.SetACLPolicyResponse], error) {
principal := CurrentPrincipal(ctx)
if !principal.IsSystemAdmin() {
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(req.Msg.TailnetId) {
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("permission denied"))
}
+4 -4
View File
@@ -22,7 +22,7 @@ func (s *Service) GetAuthKey(ctx context.Context, req *connect.Request[api.GetAu
return nil, connect.NewError(connect.CodeNotFound, errors.New("auth key not found"))
}
if !principal.IsSystemAdmin() && !principal.UserMatches(key.UserID) {
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(key.TailnetID) {
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("permission denied"))
}
@@ -73,7 +73,7 @@ func mapAuthKeysToApi(authKeys []domain.AuthKey) []*api.AuthKey {
func (s *Service) ListAuthKeys(ctx context.Context, req *connect.Request[api.ListAuthKeysRequest]) (*connect.Response[api.ListAuthKeysResponse], error) {
principal := CurrentPrincipal(ctx)
if !principal.IsSystemAdmin() && !principal.TailnetMatches(req.Msg.TailnetId) {
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(req.Msg.TailnetId) {
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("permission denied"))
}
@@ -113,7 +113,7 @@ func (s *Service) ListAuthKeys(ctx context.Context, req *connect.Request[api.Lis
func (s *Service) CreateAuthKey(ctx context.Context, req *connect.Request[api.CreateAuthKeyRequest]) (*connect.Response[api.CreateAuthKeyResponse], error) {
principal := CurrentPrincipal(ctx)
if !principal.IsSystemAdmin() && !principal.TailnetMatches(req.Msg.TailnetId) {
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(req.Msg.TailnetId) {
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("permission denied"))
}
@@ -187,7 +187,7 @@ func (s *Service) DeleteAuthKey(ctx context.Context, req *connect.Request[api.De
return nil, connect.NewError(connect.CodeNotFound, errors.New("auth key not found"))
}
if !principal.IsSystemAdmin() && !principal.UserMatches(key.UserID) {
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(key.UserID) {
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("permission denied"))
}
+2 -2
View File
@@ -10,7 +10,7 @@ import (
func (s *Service) GetDNSConfig(ctx context.Context, req *connect.Request[api.GetDNSConfigRequest]) (*connect.Response[api.GetDNSConfigResponse], error) {
principal := CurrentPrincipal(ctx)
if !principal.IsSystemAdmin() && !principal.TailnetMatches(req.Msg.TailnetId) {
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(req.Msg.TailnetId) {
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("permission denied"))
}
@@ -38,7 +38,7 @@ func (s *Service) GetDNSConfig(ctx context.Context, req *connect.Request[api.Get
func (s *Service) SetDNSConfig(ctx context.Context, req *connect.Request[api.SetDNSConfigRequest]) (*connect.Response[api.SetDNSConfigResponse], error) {
principal := CurrentPrincipal(ctx)
if !principal.IsSystemAdmin() {
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(req.Msg.TailnetId) {
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("permission denied"))
}
+20 -2
View File
@@ -11,7 +11,7 @@ import (
func (s *Service) GetIAMPolicy(ctx context.Context, req *connect.Request[api.GetIAMPolicyRequest]) (*connect.Response[api.GetIAMPolicyResponse], error) {
principal := CurrentPrincipal(ctx)
if !principal.IsSystemAdmin() && !principal.TailnetMatches(req.Msg.TailnetId) {
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(req.Msg.TailnetId) {
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("permission denied"))
}
@@ -27,6 +27,7 @@ func (s *Service) GetIAMPolicy(ctx context.Context, req *connect.Request[api.Get
Subs: tailnet.IAMPolicy.Subs,
Emails: tailnet.IAMPolicy.Emails,
Filters: tailnet.IAMPolicy.Filters,
Roles: domainRolesMapToApiRolesMap(tailnet.IAMPolicy.Roles),
}
return connect.NewResponse(&api.GetIAMPolicyResponse{Policy: policy}), nil
@@ -34,7 +35,7 @@ func (s *Service) GetIAMPolicy(ctx context.Context, req *connect.Request[api.Get
func (s *Service) SetIAMPolicy(ctx context.Context, req *connect.Request[api.SetIAMPolicyRequest]) (*connect.Response[api.SetIAMPolicyResponse], error) {
principal := CurrentPrincipal(ctx)
if !principal.IsSystemAdmin() {
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(req.Msg.TailnetId) {
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("permission denied"))
}
@@ -50,6 +51,7 @@ func (s *Service) SetIAMPolicy(ctx context.Context, req *connect.Request[api.Set
Subs: req.Msg.Policy.Subs,
Emails: req.Msg.Policy.Emails,
Filters: req.Msg.Policy.Filters,
Roles: apiRolesMapToDomainRolesMap(req.Msg.Policy.Roles),
}
if err := s.repository.SaveTailnet(ctx, tailnet); err != nil {
@@ -58,3 +60,19 @@ func (s *Service) SetIAMPolicy(ctx context.Context, req *connect.Request[api.Set
return connect.NewResponse(&api.SetIAMPolicyResponse{}), nil
}
func apiRolesMapToDomainRolesMap(values map[string]string) map[string]domain.UserRole {
var result = map[string]domain.UserRole{}
for k, v := range values {
result[k] = domain.UserRole(v)
}
return result
}
func domainRolesMapToApiRolesMap(values map[string]domain.UserRole) map[string]string {
var result = map[string]string{}
for k, v := range values {
result[k] = string(v)
}
return result
}
+12 -3
View File
@@ -21,13 +21,18 @@ const (
type Principal struct {
SystemRole domain.SystemRole
User *domain.User
UserRole domain.UserRole
}
func (p Principal) IsSystemAdmin() bool {
return p.SystemRole.IsAdmin()
}
func (p Principal) TailnetMatches(tailnetID uint64) bool {
func (p Principal) IsTailnetAdmin(tailnetID uint64) bool {
return p.User.TailnetID == tailnetID && p.UserRole.IsAdmin()
}
func (p Principal) IsTailnetMember(tailnetID uint64) bool {
return p.User.TailnetID == tailnetID
}
@@ -38,7 +43,7 @@ func (p Principal) UserMatches(userID uint64) bool {
func CurrentPrincipal(ctx context.Context) Principal {
p := ctx.Value(principalKey)
if p == nil {
return Principal{SystemRole: domain.SystemRoleNone}
return Principal{SystemRole: domain.SystemRoleNone, UserRole: domain.UserRoleNone}
}
return p.(Principal)
}
@@ -81,5 +86,9 @@ func exchangeToken(ctx context.Context, systemAdminKey key.ServerPrivate, reposi
return nil
}
return &Principal{User: &apiKey.User, SystemRole: domain.SystemRoleNone}
user := apiKey.User
tailnet := apiKey.Tailnet
role := tailnet.IAMPolicy.GetRole(user)
return &Principal{User: &apiKey.User, SystemRole: domain.SystemRoleNone, UserRole: role}
}
+6 -6
View File
@@ -13,7 +13,7 @@ import (
func (s *Service) ListMachines(ctx context.Context, req *connect.Request[api.ListMachinesRequest]) (*connect.Response[api.ListMachinesResponse], error) {
principal := CurrentPrincipal(ctx)
if !principal.IsSystemAdmin() && !principal.TailnetMatches(req.Msg.TailnetId) {
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(req.Msg.TailnetId) {
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("permission denied"))
}
@@ -76,7 +76,7 @@ func (s *Service) DeleteMachine(ctx context.Context, req *connect.Request[api.De
return nil, connect.NewError(connect.CodeNotFound, errors.New("machine not found"))
}
if !principal.IsSystemAdmin() && !principal.UserMatches(m.UserID) {
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(m.TailnetID) {
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("permission denied"))
}
@@ -101,7 +101,7 @@ func (s *Service) ExpireMachine(ctx context.Context, req *connect.Request[api.Ex
return nil, connect.NewError(connect.CodeNotFound, errors.New("machine not found"))
}
if !principal.IsSystemAdmin() && !principal.UserMatches(m.UserID) {
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(m.TailnetID) {
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("permission denied"))
}
@@ -130,7 +130,7 @@ func (s *Service) GetMachineRoutes(ctx context.Context, req *connect.Request[api
return nil, connect.NewError(connect.CodeNotFound, errors.New("machine not found"))
}
if !principal.IsSystemAdmin() && !principal.TailnetMatches(m.TailnetID) {
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(m.TailnetID) {
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("permission denied"))
}
@@ -161,7 +161,7 @@ func (s *Service) SetMachineRoutes(ctx context.Context, req *connect.Request[api
return nil, connect.NewError(connect.CodeNotFound, errors.New("machine not found"))
}
if !principal.IsSystemAdmin() {
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(m.TailnetID) {
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("permission denied"))
}
@@ -208,7 +208,7 @@ func (s *Service) SetMachineKeyExpiry(ctx context.Context, req *connect.Request[
return nil, connect.NewError(connect.CodeNotFound, errors.New("machine not found"))
}
if !principal.IsSystemAdmin() {
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(m.TailnetID) {
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("permission denied"))
}
+2 -2
View File
@@ -34,7 +34,7 @@ func (s *Service) CreateTailnet(ctx context.Context, req *connect.Request[api.Cr
func (s *Service) GetTailnet(ctx context.Context, req *connect.Request[api.GetTailnetRequest]) (*connect.Response[api.GetTailnetResponse], error) {
principal := CurrentPrincipal(ctx)
if !principal.IsSystemAdmin() && !principal.TailnetMatches(req.Msg.Id) {
if !principal.IsSystemAdmin() && !principal.IsTailnetAdmin(req.Msg.Id) {
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("permission denied"))
}
@@ -83,7 +83,7 @@ func (s *Service) ListTailnets(ctx context.Context, req *connect.Request[api.Lis
func (s *Service) DeleteTailnet(ctx context.Context, req *connect.Request[api.DeleteTailnetRequest]) (*connect.Response[api.DeleteTailnetResponse], error) {
principal := CurrentPrincipal(ctx)
if !principal.IsSystemAdmin() && !principal.TailnetMatches(req.Msg.TailnetId) {
if !principal.IsSystemAdmin() {
return nil, connect.NewError(connect.CodePermissionDenied, errors.New("permission denied"))
}
+34 -17
View File
@@ -215,6 +215,7 @@ type IAMPolicy struct {
Subs []string `protobuf:"bytes,1,rep,name=subs,proto3" json:"subs,omitempty"`
Emails []string `protobuf:"bytes,2,rep,name=emails,proto3" json:"emails,omitempty"`
Filters []string `protobuf:"bytes,3,rep,name=filters,proto3" json:"filters,omitempty"`
Roles map[string]string `protobuf:"bytes,4,rep,name=roles,proto3" json:"roles,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
}
func (x *IAMPolicy) Reset() {
@@ -270,6 +271,13 @@ func (x *IAMPolicy) GetFilters() []string {
return nil
}
func (x *IAMPolicy) GetRoles() map[string]string {
if x != nil {
return x.Roles
}
return nil
}
var File_ionscale_v1_iam_proto protoreflect.FileDescriptor
var file_ionscale_v1_iam_proto_rawDesc = []byte{
@@ -291,16 +299,23 @@ var file_ionscale_v1_iam_proto_rawDesc = []byte{
0x61, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x41, 0x4d, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79,
0x52, 0x06, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x22, 0x16, 0x0a, 0x14, 0x53, 0x65, 0x74, 0x49,
0x41, 0x4d, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
0x22, 0x51, 0x0a, 0x09, 0x49, 0x41, 0x4d, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x12, 0x0a,
0x04, 0x73, 0x75, 0x62, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x73, 0x75, 0x62,
0x73, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28,
0x09, 0x52, 0x06, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x66, 0x69, 0x6c,
0x74, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x66, 0x69, 0x6c, 0x74,
0x65, 0x72, 0x73, 0x42, 0x3d, 0x5a, 0x3b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f,
0x6d, 0x2f, 0x6a, 0x73, 0x69, 0x65, 0x62, 0x65, 0x6e, 0x73, 0x2f, 0x69, 0x6f, 0x6e, 0x73, 0x63,
0x61, 0x6c, 0x65, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x69, 0x6f, 0x6e, 0x73,
0x63, 0x61, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x3b, 0x69, 0x6f, 0x6e, 0x73, 0x63, 0x61, 0x6c, 0x65,
0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x22, 0xc4, 0x01, 0x0a, 0x09, 0x49, 0x41, 0x4d, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x12,
0x0a, 0x04, 0x73, 0x75, 0x62, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x73, 0x75,
0x62, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x73, 0x18, 0x02, 0x20, 0x03,
0x28, 0x09, 0x52, 0x06, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x66, 0x69,
0x6c, 0x74, 0x65, 0x72, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x66, 0x69, 0x6c,
0x74, 0x65, 0x72, 0x73, 0x12, 0x37, 0x0a, 0x05, 0x72, 0x6f, 0x6c, 0x65, 0x73, 0x18, 0x04, 0x20,
0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x69, 0x6f, 0x6e, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2e, 0x76,
0x31, 0x2e, 0x49, 0x41, 0x4d, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x2e, 0x52, 0x6f, 0x6c, 0x65,
0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x05, 0x72, 0x6f, 0x6c, 0x65, 0x73, 0x1a, 0x38, 0x0a,
0x0a, 0x52, 0x6f, 0x6c, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b,
0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a,
0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61,
0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x3d, 0x5a, 0x3b, 0x67, 0x69, 0x74, 0x68, 0x75,
0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6a, 0x73, 0x69, 0x65, 0x62, 0x65, 0x6e, 0x73, 0x2f, 0x69,
0x6f, 0x6e, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x67, 0x65, 0x6e, 0x2f,
0x69, 0x6f, 0x6e, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x3b, 0x69, 0x6f, 0x6e, 0x73,
0x63, 0x61, 0x6c, 0x65, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -315,22 +330,24 @@ func file_ionscale_v1_iam_proto_rawDescGZIP() []byte {
return file_ionscale_v1_iam_proto_rawDescData
}
var file_ionscale_v1_iam_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
var file_ionscale_v1_iam_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
var file_ionscale_v1_iam_proto_goTypes = []interface{}{
(*GetIAMPolicyRequest)(nil), // 0: ionscale.v1.GetIAMPolicyRequest
(*GetIAMPolicyResponse)(nil), // 1: ionscale.v1.GetIAMPolicyResponse
(*SetIAMPolicyRequest)(nil), // 2: ionscale.v1.SetIAMPolicyRequest
(*SetIAMPolicyResponse)(nil), // 3: ionscale.v1.SetIAMPolicyResponse
(*IAMPolicy)(nil), // 4: ionscale.v1.IAMPolicy
nil, // 5: ionscale.v1.IAMPolicy.RolesEntry
}
var file_ionscale_v1_iam_proto_depIdxs = []int32{
4, // 0: ionscale.v1.GetIAMPolicyResponse.policy:type_name -> ionscale.v1.IAMPolicy
4, // 1: ionscale.v1.SetIAMPolicyRequest.policy:type_name -> ionscale.v1.IAMPolicy
2, // [2:2] is the sub-list for method output_type
2, // [2:2] is the sub-list for method input_type
2, // [2:2] is the sub-list for extension type_name
2, // [2:2] is the sub-list for extension extendee
0, // [0:2] is the sub-list for field type_name
5, // 2: ionscale.v1.IAMPolicy.roles:type_name -> ionscale.v1.IAMPolicy.RolesEntry
3, // [3:3] is the sub-list for method output_type
3, // [3:3] is the sub-list for method input_type
3, // [3:3] is the sub-list for extension type_name
3, // [3:3] is the sub-list for extension extendee
0, // [0:3] is the sub-list for field type_name
}
func init() { file_ionscale_v1_iam_proto_init() }
@@ -406,7 +423,7 @@ func file_ionscale_v1_iam_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_ionscale_v1_iam_proto_rawDesc,
NumEnums: 0,
NumMessages: 5,
NumMessages: 6,
NumExtensions: 0,
NumServices: 0,
},
+1
View File
@@ -23,4 +23,5 @@ message IAMPolicy {
repeated string subs = 1;
repeated string emails = 2;
repeated string filters = 3;
map<string, string> roles = 4;
}