diff --git a/pkg/authz/feature/spec.go b/pkg/authz/feature/spec.go index 51ba2777..5ce1b5f7 100644 --- a/pkg/authz/feature/spec.go +++ b/pkg/authz/feature/spec.go @@ -8,7 +8,7 @@ import ( ) type FeatureSpec struct { - FeatureId string `json:"featureId" validate:"required"` + FeatureId string `json:"featureId" validate:"required,valid_object_id"` Name *string `json:"name"` Description *string `json:"description"` CreatedAt time.Time `json:"createdAt"` diff --git a/pkg/authz/permission/spec.go b/pkg/authz/permission/spec.go index 00fb382c..472ea88c 100644 --- a/pkg/authz/permission/spec.go +++ b/pkg/authz/permission/spec.go @@ -8,7 +8,7 @@ import ( ) type PermissionSpec struct { - PermissionId string `json:"permissionId" validate:"required"` + PermissionId string `json:"permissionId" validate:"required,valid_object_id"` Name *string `json:"name"` Description *string `json:"description"` CreatedAt time.Time `json:"createdAt"` diff --git a/pkg/authz/pricingtier/spec.go b/pkg/authz/pricingtier/spec.go index 05bd205e..a45eca35 100644 --- a/pkg/authz/pricingtier/spec.go +++ b/pkg/authz/pricingtier/spec.go @@ -8,7 +8,7 @@ import ( ) type PricingTierSpec struct { - PricingTierId string `json:"pricingTierId" validate:"required"` + PricingTierId string `json:"pricingTierId" validate:"required,valid_object_id"` Name *string `json:"name"` Description *string `json:"description"` CreatedAt time.Time `json:"createdAt"` diff --git a/pkg/authz/role/spec.go b/pkg/authz/role/spec.go index 6cb26bca..db62191d 100644 --- a/pkg/authz/role/spec.go +++ b/pkg/authz/role/spec.go @@ -8,7 +8,7 @@ import ( ) type RoleSpec struct { - RoleId string `json:"roleId" validate:"required"` + RoleId string `json:"roleId" validate:"required,valid_object_id"` Name *string `json:"name"` Description *string `json:"description"` CreatedAt time.Time `json:"createdAt"` diff --git a/pkg/authz/tenant/service.go b/pkg/authz/tenant/service.go index 111ca70c..3134fb39 100644 --- a/pkg/authz/tenant/service.go +++ b/pkg/authz/tenant/service.go @@ -2,9 +2,9 @@ package tenant import ( "context" - "regexp" "github.com/google/uuid" + "github.com/pkg/errors" object "github.com/warrant-dev/warrant/pkg/authz/object" objecttype "github.com/warrant-dev/warrant/pkg/authz/objecttype" "github.com/warrant-dev/warrant/pkg/event" @@ -31,13 +31,17 @@ func NewService(env service.Env, repository TenantRepository, eventSvc event.Eve } func (svc TenantService) Create(ctx context.Context, tenantSpec TenantSpec) (*TenantSpec, error) { - err := validateOrGenerateTenantIdInSpec(&tenantSpec) - if err != nil { - return nil, err + if tenantSpec.TenantId == "" { + // generate an id for the tenant if one isn't supplied + generatedUUID, err := uuid.NewRandom() + if err != nil { + return nil, errors.New("unable to generate random UUID for tenant") + } + tenantSpec.TenantId = generatedUUID.String() } var newTenant Model - err = svc.Env().DB().WithinTransaction(ctx, func(txCtx context.Context) error { + err := svc.Env().DB().WithinTransaction(ctx, func(txCtx context.Context) error { createdObject, err := svc.ObjectSvc.Create(txCtx, *tenantSpec.ToObjectSpec()) if err != nil { switch err.(type) { @@ -152,21 +156,3 @@ func (svc TenantService) DeleteByTenantId(ctx context.Context, tenantId string) return nil } - -func validateOrGenerateTenantIdInSpec(tenantSpec *TenantSpec) error { - tenantIdRegExp := regexp.MustCompile(`^[a-zA-Z0-9_\-\.@\|]+$`) - if tenantSpec.TenantId != "" { - // Validate tenantId if provided - if !tenantIdRegExp.Match([]byte(tenantSpec.TenantId)) { - return service.NewInvalidParameterError("tenantId", "must be provided and can only contain alphanumeric characters and/or '-', '_', '@', and '|'") - } - } else { - // Generate a TenantId for the tenant if one isn't supplied - generatedUUID, err := uuid.NewRandom() - if err != nil { - return service.NewInternalError("unable to generate random UUID for tenant") - } - tenantSpec.TenantId = generatedUUID.String() - } - return nil -} diff --git a/pkg/authz/tenant/spec.go b/pkg/authz/tenant/spec.go index 604250bb..f17401fc 100644 --- a/pkg/authz/tenant/spec.go +++ b/pkg/authz/tenant/spec.go @@ -8,7 +8,7 @@ import ( ) type TenantSpec struct { - TenantId string `json:"tenantId"` + TenantId string `json:"tenantId" validate:"omitempty,valid_object_id"` Name *string `json:"name"` CreatedAt time.Time `json:"createdAt"` } diff --git a/pkg/authz/user/service.go b/pkg/authz/user/service.go index 826655e4..aadad81d 100644 --- a/pkg/authz/user/service.go +++ b/pkg/authz/user/service.go @@ -2,9 +2,9 @@ package authz import ( "context" - "regexp" "github.com/google/uuid" + "github.com/pkg/errors" object "github.com/warrant-dev/warrant/pkg/authz/object" objecttype "github.com/warrant-dev/warrant/pkg/authz/objecttype" "github.com/warrant-dev/warrant/pkg/event" @@ -31,13 +31,17 @@ func NewService(env service.Env, repository UserRepository, eventSvc event.Event } func (svc UserService) Create(ctx context.Context, userSpec UserSpec) (*UserSpec, error) { - err := validateOrGenerateUserIdInSpec(&userSpec) - if err != nil { - return nil, err + if userSpec.UserId == "" { + // generate an id for the user if one isn't provided + generatedUUID, err := uuid.NewRandom() + if err != nil { + return nil, errors.New("unable to generate random UUID for user") + } + userSpec.UserId = generatedUUID.String() } var newUser Model - err = svc.Env().DB().WithinTransaction(ctx, func(txCtx context.Context) error { + err := svc.Env().DB().WithinTransaction(ctx, func(txCtx context.Context) error { createdObject, err := svc.ObjectSvc.Create(txCtx, *userSpec.ToObjectSpec()) if err != nil { switch err.(type) { @@ -149,21 +153,3 @@ func (svc UserService) DeleteByUserId(ctx context.Context, userId string) error return err } - -func validateOrGenerateUserIdInSpec(userSpec *UserSpec) error { - userIdRegExp := regexp.MustCompile(`^[a-zA-Z0-9_\-\.@\|]+$`) - if userSpec.UserId != "" { - // Validate userId if provided - if !userIdRegExp.Match([]byte(userSpec.UserId)) { - return service.NewInvalidParameterError("userId", "must be provided and can only contain alphanumeric characters and/or '-', '_', '@', and '|'") - } - } else { - // Generate a UserID for the user if one isn't supplied - generatedUUID, err := uuid.NewRandom() - if err != nil { - return service.NewInternalError("unable to generate random UUID for user") - } - userSpec.UserId = generatedUUID.String() - } - return nil -} diff --git a/pkg/authz/user/spec.go b/pkg/authz/user/spec.go index 5bdff5cb..06d2d9ec 100644 --- a/pkg/authz/user/spec.go +++ b/pkg/authz/user/spec.go @@ -8,7 +8,7 @@ import ( ) type UserSpec struct { - UserId string `json:"userId"` + UserId string `json:"userId" validate:"omitempty,valid_object_id"` Email *string `json:"email" validate:"omitempty,email"` CreatedAt time.Time `json:"createdAt"` } diff --git a/pkg/authz/warrant/spec.go b/pkg/authz/warrant/spec.go index 33e1322f..e3bf86a4 100644 --- a/pkg/authz/warrant/spec.go +++ b/pkg/authz/warrant/spec.go @@ -25,24 +25,6 @@ type SortOptions struct { IsAscending bool } -type ObjectSpec struct { - ObjectType string `json:"objectType" validate:"required,valid_object_type"` - ObjectId string `json:"objectId" validate:"required,valid_object_id"` -} - -func StringToObjectSpec(str string) (*ObjectSpec, error) { - objectTypeId := strings.Split(str, ":") - - if len(objectTypeId) != 2 { - return nil, fmt.Errorf("invalid object") - } - - return &ObjectSpec{ - ObjectType: objectTypeId[0], - ObjectId: objectTypeId[1], - }, nil -} - type SubjectSpec struct { ObjectType string `json:"objectType,omitempty" validate:"required_with=ObjectId,valid_object_type"` ObjectId string `json:"objectId,omitempty" validate:"required_with=ObjectType,valid_object_id"` @@ -60,23 +42,25 @@ func (spec *SubjectSpec) String() string { func StringToSubjectSpec(str string) (*SubjectSpec, error) { objectRelation := strings.Split(str, "#") if len(objectRelation) < 2 { - objectTypeId := strings.Split(str, ":") + objectType, objectId, colonFound := strings.Cut(str, ":") - if len(objectTypeId) != 2 { + if !colonFound { return nil, fmt.Errorf("invalid subject") } return &SubjectSpec{ - ObjectType: objectTypeId[0], - ObjectId: objectTypeId[1], + ObjectType: objectType, + ObjectId: objectId, }, nil } object := objectRelation[0] relation := objectRelation[1] - objectTypeId := strings.Split(object, ":") - objectType := objectTypeId[0] - objectId := objectTypeId[1] + + objectType, objectId, colonFound := strings.Cut(object, ":") + if !colonFound { + return nil, fmt.Errorf("invalid subject") + } subjectSpec := &SubjectSpec{ ObjectType: objectType, @@ -147,8 +131,8 @@ func StringToWarrantSpec(warrantString string) (*WarrantSpec, error) { return nil, fmt.Errorf("invalid warrant") } - objectTypeAndObjectId := strings.Split(objectAndRelation[0], ":") - if len(objectTypeAndObjectId) != 2 { + objectType, objectId, colonFound := strings.Cut(objectAndRelation[0], ":") + if !colonFound { return nil, fmt.Errorf("invalid warrant") } @@ -176,8 +160,8 @@ func StringToWarrantSpec(warrantString string) (*WarrantSpec, error) { } return &WarrantSpec{ - ObjectType: objectTypeAndObjectId[0], - ObjectId: objectTypeAndObjectId[1], + ObjectType: objectType, + ObjectId: objectId, Relation: objectAndRelation[1], Subject: subjectSpec, Context: contextSetSpec, diff --git a/pkg/service/json.go b/pkg/service/json.go index e0976c7e..6e772c06 100644 --- a/pkg/service/json.go +++ b/pkg/service/json.go @@ -58,7 +58,7 @@ func validObjectId(fl validator.FieldLevel) bool { return true } - regExp := regexp.MustCompile(`^[a-zA-Z0-9_\-\.@\|]+$`) + regExp := regexp.MustCompile(`^[a-zA-Z0-9_\-\.@\|:]+$`) return regExp.Match([]byte(value)) } @@ -183,11 +183,11 @@ func ValidateStruct(obj interface{}) error { validValues := strings.Join(strings.Split(err.Param(), " "), ", ") return NewInvalidParameterError(fieldName, fmt.Sprintf("must be one of %s", validValues)) case "valid_object_type", "valid_relation": - return NewInvalidParameterError(fieldName, "must be provided and can only contain lower-case alphanumeric characters and/or '-' and '_'") + return NewInvalidParameterError(fieldName, "can only contain lower-case alphanumeric characters and/or '-' and '_'") case "valid_object_id": - return NewInvalidParameterError(fieldName, "must be provided and can only contain alphanumeric characters and/or '-', '_', '@', and '|'") + return NewInvalidParameterError(fieldName, "can only contain alphanumeric characters and/or '-', '_', '@', ':', and '|'") case "valid_inheritif": - return NewInvalidParameterError(fieldName, "must be provided and can only be 'anyOf', 'allOf', 'noneOf', or a valid relation name") + return NewInvalidParameterError(fieldName, "can only be 'anyOf', 'allOf', 'noneOf', or a valid relation name") default: return NewInvalidRequestError("Invalid request body") } diff --git a/tests/warrants.json b/tests/warrants.json index f975c47b..91b210c6 100644 --- a/tests/warrants.json +++ b/tests/warrants.json @@ -67,7 +67,7 @@ "method": "POST", "url": "/v1/permissions", "body": { - "permissionId": "edit-balance-sheet", + "permissionId": "balance-sheet:edit", "name": "Edit Balance Sheet", "description": "Grants access to edit the balance sheet." } @@ -75,7 +75,7 @@ "expectedResponse": { "statusCode": 200, "body": { - "permissionId": "edit-balance-sheet", + "permissionId": "balance-sheet:edit", "name": "Edit Balance Sheet", "description": "Grants access to edit the balance sheet." } @@ -116,7 +116,7 @@ "url": "/v1/warrants", "body": { "objectType": "permission", - "objectId": "edit-balance-sheet", + "objectId": "balance-sheet:edit", "relation": "member", "subject": { "objectType": "user", @@ -128,7 +128,7 @@ "statusCode": 200, "body": { "objectType": "permission", - "objectId": "edit-balance-sheet", + "objectId": "balance-sheet:edit", "relation": "member", "subject": { "objectType": "user", @@ -203,7 +203,7 @@ "url": "/v1/warrants", "body": { "objectType": "permission", - "objectId": "edit-balance-sheet", + "objectId": "balance-sheet:edit", "relation": "member", "subject": { "objectType": "user", @@ -238,9 +238,9 @@ "name": "deletePermissionEditBalanceSheet", "request": { "method": "DELETE", - "url": "/v1/permissions/edit-balance-sheet", + "url": "/v1/permissions/balance-sheet:edit", "body": { - "permissionId": "edit-balance-sheet" + "permissionId": "balance-sheet:edit" } }, "expectedResponse": { diff --git a/tests/zz-events.json b/tests/zz-events.json index 04c71916..7c0e5414 100644 --- a/tests/zz-events.json +++ b/tests/zz-events.json @@ -51,17 +51,17 @@ "type": "deleted", "source": "api", "resourceType": "permission", - "resourceId": "edit-balance-sheet" + "resourceId": "balance-sheet:edit" }, { "type": "created", "source": "api", "resourceType": "permission", - "resourceId": "edit-balance-sheet", + "resourceId": "balance-sheet:edit", "meta": { "description": "Grants access to edit the balance sheet.", "name": "Edit Balance Sheet", - "permissionId": "edit-balance-sheet" + "permissionId": "balance-sheet:edit" } } ], @@ -114,7 +114,7 @@ "type": "deleted", "source": "api", "resourceType": "permission", - "resourceId": "edit-balance-sheet" + "resourceId": "balance-sheet:edit" }, { "type": "deleted", @@ -252,7 +252,7 @@ "type": "access_revoked", "source": "api", "objectType": "permission", - "objectId": "edit-balance-sheet", + "objectId": "balance-sheet:edit", "relation": "member", "subjectType": "user", "subjectId": "user-a" @@ -294,7 +294,7 @@ "type": "access_granted", "source": "api", "objectType": "permission", - "objectId": "edit-balance-sheet", + "objectId": "balance-sheet:edit", "relation": "member", "subjectType": "user", "subjectId": "user-a" @@ -340,7 +340,7 @@ "type": "access_granted", "source": "api", "objectType": "permission", - "objectId": "edit-balance-sheet", + "objectId": "balance-sheet:edit", "relation": "member", "subjectType": "user", "subjectId": "user-a" @@ -562,7 +562,7 @@ "type": "access_revoked", "source": "api", "objectType": "permission", - "objectId": "edit-balance-sheet", + "objectId": "balance-sheet:edit", "relation": "member", "subjectType": "user", "subjectId": "user-a" @@ -589,7 +589,7 @@ "type": "access_granted", "source": "api", "objectType": "permission", - "objectId": "edit-balance-sheet", + "objectId": "balance-sheet:edit", "relation": "member", "subjectType": "user", "subjectId": "user-a"