From 78718773ec4a77c530b238a2159825a501292d97 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 23 Dec 2025 18:28:37 +0000 Subject: [PATCH] Add CREATE/ALTER SERVER ROLE and GRANT ON object_kind support - Add CreateServerRoleStatement and AlterServerRoleStatement AST types - Add SecurityTargetObject and SecurityTargetObjectName AST types - Update GrantStatement to support ON :: syntax - Parse CREATE SERVER ROLE with optional AUTHORIZATION clause - Parse ALTER SERVER ROLE with ADD/DROP MEMBER and WITH NAME actions - Parse many object kinds in GRANT (ServerRole, ApplicationRole, etc.) - Enable Baselines110_ServerRoleStatementTests and ServerRoleStatementTests --- ast/alter_server_role_statement.go | 10 + ast/create_server_role_statement.go | 10 + ast/grant_statement.go | 7 +- ast/security_target_object.go | 16 ++ parser/marshal.go | 204 +++++++++++++++++- parser/parse_ddl.go | 66 +++++- parser/parse_statements.go | 31 +++ .../metadata.json | 2 +- .../ServerRoleStatementTests/metadata.json | 2 +- 9 files changed, 340 insertions(+), 8 deletions(-) create mode 100644 ast/alter_server_role_statement.go create mode 100644 ast/create_server_role_statement.go create mode 100644 ast/security_target_object.go diff --git a/ast/alter_server_role_statement.go b/ast/alter_server_role_statement.go new file mode 100644 index 00000000..347fa12e --- /dev/null +++ b/ast/alter_server_role_statement.go @@ -0,0 +1,10 @@ +package ast + +// AlterServerRoleStatement represents an ALTER SERVER ROLE statement +type AlterServerRoleStatement struct { + Name *Identifier + Action AlterRoleAction // Reuses the same action types as AlterRoleStatement +} + +func (a *AlterServerRoleStatement) node() {} +func (a *AlterServerRoleStatement) statement() {} diff --git a/ast/create_server_role_statement.go b/ast/create_server_role_statement.go new file mode 100644 index 00000000..c3091add --- /dev/null +++ b/ast/create_server_role_statement.go @@ -0,0 +1,10 @@ +package ast + +// CreateServerRoleStatement represents a CREATE SERVER ROLE statement. +type CreateServerRoleStatement struct { + Name *Identifier + Owner *Identifier // via AUTHORIZATION +} + +func (c *CreateServerRoleStatement) node() {} +func (c *CreateServerRoleStatement) statement() {} diff --git a/ast/grant_statement.go b/ast/grant_statement.go index 9c6f48a2..cb41a140 100644 --- a/ast/grant_statement.go +++ b/ast/grant_statement.go @@ -2,9 +2,10 @@ package ast // GrantStatement represents a GRANT statement type GrantStatement struct { - Permissions []*Permission - Principals []*SecurityPrincipal - WithGrantOption bool + Permissions []*Permission + Principals []*SecurityPrincipal + WithGrantOption bool + SecurityTargetObject *SecurityTargetObject } func (s *GrantStatement) node() {} diff --git a/ast/security_target_object.go b/ast/security_target_object.go new file mode 100644 index 00000000..bb81f587 --- /dev/null +++ b/ast/security_target_object.go @@ -0,0 +1,16 @@ +package ast + +// SecurityTargetObject represents the target object in security statements (GRANT, REVOKE, DENY) +type SecurityTargetObject struct { + ObjectKind string // e.g., "ServerRole", "NotSpecified", "Type", etc. + ObjectName *SecurityTargetObjectName +} + +func (s *SecurityTargetObject) node() {} + +// SecurityTargetObjectName represents the name of a security target object +type SecurityTargetObjectName struct { + MultiPartIdentifier *MultiPartIdentifier +} + +func (s *SecurityTargetObjectName) node() {} diff --git a/parser/marshal.go b/parser/marshal.go index 9385accc..a9e9b741 100644 --- a/parser/marshal.go +++ b/parser/marshal.go @@ -282,6 +282,10 @@ func statementToJSON(stmt ast.Statement) jsonNode { return alterSchemaStatementToJSON(s) case *ast.AlterRoleStatement: return alterRoleStatementToJSON(s) + case *ast.CreateServerRoleStatement: + return createServerRoleStatementToJSON(s) + case *ast.AlterServerRoleStatement: + return alterServerRoleStatementToJSON(s) case *ast.AlterRemoteServiceBindingStatement: return alterRemoteServiceBindingStatementToJSON(s) case *ast.AlterXmlSchemaCollectionStatement: @@ -2655,11 +2659,13 @@ func (p *Parser) parseGrantStatement() (*ast.GrantStatement, error) { // Parse permission(s) perm := &ast.Permission{} - for p.curTok.Type != TokenTo && p.curTok.Type != TokenEOF { + for p.curTok.Type != TokenTo && p.curTok.Type != TokenOn && p.curTok.Type != TokenEOF { if p.curTok.Type == TokenIdent || p.curTok.Type == TokenCreate || p.curTok.Type == TokenProcedure || p.curTok.Type == TokenView || p.curTok.Type == TokenSelect || p.curTok.Type == TokenInsert || - p.curTok.Type == TokenUpdate || p.curTok.Type == TokenDelete { + p.curTok.Type == TokenUpdate || p.curTok.Type == TokenDelete || + p.curTok.Type == TokenAlter || p.curTok.Type == TokenExecute || + p.curTok.Type == TokenDrop { perm.Identifiers = append(perm.Identifiers, &ast.Identifier{ Value: p.curTok.Literal, QuoteType: "NotQuoted", @@ -2677,6 +2683,150 @@ func (p *Parser) parseGrantStatement() (*ast.GrantStatement, error) { stmt.Permissions = append(stmt.Permissions, perm) } + // Check for ON clause (SecurityTargetObject) + if p.curTok.Type == TokenOn { + p.nextToken() // consume ON + + stmt.SecurityTargetObject = &ast.SecurityTargetObject{} + stmt.SecurityTargetObject.ObjectKind = "NotSpecified" + + // Parse object kind and :: + // Object kinds can be: SERVER ROLE, APPLICATION ROLE, ASYMMETRIC KEY, SYMMETRIC KEY, etc. + objectKind := strings.ToUpper(p.curTok.Literal) + switch objectKind { + case "SERVER": + p.nextToken() // consume SERVER + if strings.ToUpper(p.curTok.Literal) == "ROLE" { + p.nextToken() // consume ROLE + stmt.SecurityTargetObject.ObjectKind = "ServerRole" + } else { + stmt.SecurityTargetObject.ObjectKind = "Server" + } + case "APPLICATION": + p.nextToken() // consume APPLICATION + if strings.ToUpper(p.curTok.Literal) == "ROLE" { + p.nextToken() // consume ROLE + } + stmt.SecurityTargetObject.ObjectKind = "ApplicationRole" + case "ASYMMETRIC": + p.nextToken() // consume ASYMMETRIC + if strings.ToUpper(p.curTok.Literal) == "KEY" { + p.nextToken() // consume KEY + } + stmt.SecurityTargetObject.ObjectKind = "AsymmetricKey" + case "SYMMETRIC": + p.nextToken() // consume SYMMETRIC + if strings.ToUpper(p.curTok.Literal) == "KEY" { + p.nextToken() // consume KEY + } + stmt.SecurityTargetObject.ObjectKind = "SymmetricKey" + case "REMOTE": + p.nextToken() // consume REMOTE + if strings.ToUpper(p.curTok.Literal) == "SERVICE" { + p.nextToken() // consume SERVICE + if strings.ToUpper(p.curTok.Literal) == "BINDING" { + p.nextToken() // consume BINDING + } + } + stmt.SecurityTargetObject.ObjectKind = "RemoteServiceBinding" + case "FULLTEXT": + p.nextToken() // consume FULLTEXT + if strings.ToUpper(p.curTok.Literal) == "CATALOG" { + p.nextToken() // consume CATALOG + } + stmt.SecurityTargetObject.ObjectKind = "FullTextCatalog" + case "MESSAGE": + p.nextToken() // consume MESSAGE + if strings.ToUpper(p.curTok.Literal) == "TYPE" { + p.nextToken() // consume TYPE + } + stmt.SecurityTargetObject.ObjectKind = "MessageType" + case "XML": + p.nextToken() // consume XML + if strings.ToUpper(p.curTok.Literal) == "SCHEMA" { + p.nextToken() // consume SCHEMA + if strings.ToUpper(p.curTok.Literal) == "COLLECTION" { + p.nextToken() // consume COLLECTION + } + } + stmt.SecurityTargetObject.ObjectKind = "XmlSchemaCollection" + case "SEARCH": + p.nextToken() // consume SEARCH + if strings.ToUpper(p.curTok.Literal) == "PROPERTY" { + p.nextToken() // consume PROPERTY + if strings.ToUpper(p.curTok.Literal) == "LIST" { + p.nextToken() // consume LIST + } + } + stmt.SecurityTargetObject.ObjectKind = "SearchPropertyList" + case "AVAILABILITY": + p.nextToken() // consume AVAILABILITY + if strings.ToUpper(p.curTok.Literal) == "GROUP" { + p.nextToken() // consume GROUP + } + stmt.SecurityTargetObject.ObjectKind = "AvailabilityGroup" + case "TYPE": + p.nextToken() + stmt.SecurityTargetObject.ObjectKind = "Type" + case "OBJECT": + p.nextToken() + stmt.SecurityTargetObject.ObjectKind = "Object" + case "ASSEMBLY": + p.nextToken() + stmt.SecurityTargetObject.ObjectKind = "Assembly" + case "CERTIFICATE": + p.nextToken() + stmt.SecurityTargetObject.ObjectKind = "Certificate" + case "CONTRACT": + p.nextToken() + stmt.SecurityTargetObject.ObjectKind = "Contract" + case "DATABASE": + p.nextToken() + stmt.SecurityTargetObject.ObjectKind = "Database" + case "ENDPOINT": + p.nextToken() + stmt.SecurityTargetObject.ObjectKind = "Endpoint" + case "LOGIN": + p.nextToken() + stmt.SecurityTargetObject.ObjectKind = "Login" + case "ROLE": + p.nextToken() + stmt.SecurityTargetObject.ObjectKind = "Role" + case "ROUTE": + p.nextToken() + stmt.SecurityTargetObject.ObjectKind = "Route" + case "SCHEMA": + p.nextToken() + stmt.SecurityTargetObject.ObjectKind = "Schema" + case "SERVICE": + p.nextToken() + stmt.SecurityTargetObject.ObjectKind = "Service" + case "USER": + p.nextToken() + stmt.SecurityTargetObject.ObjectKind = "User" + } + + // Expect :: + if p.curTok.Type == TokenColonColon { + p.nextToken() // consume :: + + // Parse object name as multi-part identifier + stmt.SecurityTargetObject.ObjectName = &ast.SecurityTargetObjectName{} + multiPart := &ast.MultiPartIdentifier{} + for { + id := p.parseIdentifier() + multiPart.Identifiers = append(multiPart.Identifiers, id) + if p.curTok.Type == TokenDot { + p.nextToken() // consume . + } else { + break + } + } + multiPart.Count = len(multiPart.Identifiers) + stmt.SecurityTargetObject.ObjectName.MultiPartIdentifier = multiPart + } + } + // Expect TO if p.curTok.Type == TokenTo { p.nextToken() @@ -2965,6 +3115,9 @@ func grantStatementToJSON(s *ast.GrantStatement) jsonNode { } node["Permissions"] = perms } + if s.SecurityTargetObject != nil { + node["SecurityTargetObject"] = securityTargetObjectToJSON(s.SecurityTargetObject) + } if len(s.Principals) > 0 { principals := make([]jsonNode, len(s.Principals)) for i, p := range s.Principals { @@ -2975,6 +3128,27 @@ func grantStatementToJSON(s *ast.GrantStatement) jsonNode { return node } +func securityTargetObjectToJSON(s *ast.SecurityTargetObject) jsonNode { + node := jsonNode{ + "$type": "SecurityTargetObject", + "ObjectKind": s.ObjectKind, + } + if s.ObjectName != nil { + node["ObjectName"] = securityTargetObjectNameToJSON(s.ObjectName) + } + return node +} + +func securityTargetObjectNameToJSON(s *ast.SecurityTargetObjectName) jsonNode { + node := jsonNode{ + "$type": "SecurityTargetObjectName", + } + if s.MultiPartIdentifier != nil { + node["MultiPartIdentifier"] = multiPartIdentifierToJSON(s.MultiPartIdentifier) + } + return node +} + func permissionToJSON(p *ast.Permission) jsonNode { node := jsonNode{ "$type": "Permission", @@ -3578,6 +3752,32 @@ func alterRoleActionToJSON(a ast.AlterRoleAction) jsonNode { } } +func createServerRoleStatementToJSON(s *ast.CreateServerRoleStatement) jsonNode { + node := jsonNode{ + "$type": "CreateServerRoleStatement", + } + if s.Owner != nil { + node["Owner"] = identifierToJSON(s.Owner) + } + if s.Name != nil { + node["Name"] = identifierToJSON(s.Name) + } + return node +} + +func alterServerRoleStatementToJSON(s *ast.AlterServerRoleStatement) jsonNode { + node := jsonNode{ + "$type": "AlterServerRoleStatement", + } + if s.Action != nil { + node["Action"] = alterRoleActionToJSON(s.Action) + } + if s.Name != nil { + node["Name"] = identifierToJSON(s.Name) + } + return node +} + func alterRemoteServiceBindingStatementToJSON(s *ast.AlterRemoteServiceBindingStatement) jsonNode { node := jsonNode{ "$type": "AlterRemoteServiceBindingStatement", diff --git a/parser/parse_ddl.go b/parser/parse_ddl.go index cfa31554..273cf36d 100644 --- a/parser/parse_ddl.go +++ b/parser/parse_ddl.go @@ -1776,9 +1776,14 @@ func (p *Parser) parseAlterServerConfigurationStatement() (ast.Statement, error) // Consume SERVER p.nextToken() + // Check if it's ALTER SERVER ROLE or ALTER SERVER CONFIGURATION + if strings.ToUpper(p.curTok.Literal) == "ROLE" { + return p.parseAlterServerRoleStatement() + } + // Expect CONFIGURATION if strings.ToUpper(p.curTok.Literal) != "CONFIGURATION" { - return nil, fmt.Errorf("expected CONFIGURATION after SERVER, got %s", p.curTok.Literal) + return nil, fmt.Errorf("expected CONFIGURATION or ROLE after SERVER, got %s", p.curTok.Literal) } p.nextToken() @@ -3045,6 +3050,65 @@ func (p *Parser) parseAlterRoleStatement() (*ast.AlterRoleStatement, error) { return stmt, nil } +func (p *Parser) parseAlterServerRoleStatement() (*ast.AlterServerRoleStatement, error) { + // Consume ROLE + p.nextToken() + + stmt := &ast.AlterServerRoleStatement{} + + // Parse role name + stmt.Name = p.parseIdentifier() + + // Parse action: ADD MEMBER, DROP MEMBER, or WITH NAME = + switch strings.ToUpper(p.curTok.Literal) { + case "ADD": + p.nextToken() // consume ADD + if strings.ToUpper(p.curTok.Literal) != "MEMBER" { + return nil, fmt.Errorf("expected MEMBER after ADD, got %s", p.curTok.Literal) + } + p.nextToken() // consume MEMBER + action := &ast.AddMemberAlterRoleAction{} + action.Member = p.parseIdentifier() + stmt.Action = action + + case "DROP": + p.nextToken() // consume DROP + if strings.ToUpper(p.curTok.Literal) != "MEMBER" { + return nil, fmt.Errorf("expected MEMBER after DROP, got %s", p.curTok.Literal) + } + p.nextToken() // consume MEMBER + action := &ast.DropMemberAlterRoleAction{} + action.Member = p.parseIdentifier() + stmt.Action = action + + case "WITH": + p.nextToken() // consume WITH + if strings.ToUpper(p.curTok.Literal) != "NAME" { + return nil, fmt.Errorf("expected NAME after WITH, got %s", p.curTok.Literal) + } + p.nextToken() // consume NAME + if p.curTok.Type != TokenEquals { + return nil, fmt.Errorf("expected = after NAME, got %s", p.curTok.Literal) + } + p.nextToken() // consume = + action := &ast.RenameAlterRoleAction{} + action.NewName = p.parseIdentifier() + stmt.Action = action + + default: + // Handle incomplete statement + p.skipToEndOfStatement() + return stmt, nil + } + + // Skip optional semicolon + if p.curTok.Type == TokenSemicolon { + p.nextToken() + } + + return stmt, nil +} + func (p *Parser) parseAlterRemoteServiceBindingStatement() (*ast.AlterRemoteServiceBindingStatement, error) { // Consume REMOTE p.nextToken() diff --git a/parser/parse_statements.go b/parser/parse_statements.go index 1aae3eee..22203fb8 100644 --- a/parser/parse_statements.go +++ b/parser/parse_statements.go @@ -1246,6 +1246,8 @@ func (p *Parser) parseCreateStatement() (ast.Statement, error) { return p.parseCreateSequenceStatement() case "SPATIAL": return p.parseCreateSpatialIndexStatement() + case "SERVER": + return p.parseCreateServerRoleStatement() } // Lenient: skip unknown CREATE statements p.skipToEndOfStatement() @@ -1382,6 +1384,35 @@ func (p *Parser) parseCreateRoleStatement() (*ast.CreateRoleStatement, error) { return stmt, nil } +func (p *Parser) parseCreateServerRoleStatement() (*ast.CreateServerRoleStatement, error) { + // Consume SERVER + p.nextToken() + + // Expect ROLE + if strings.ToUpper(p.curTok.Literal) != "ROLE" { + return nil, fmt.Errorf("expected ROLE after SERVER, got %s", p.curTok.Literal) + } + p.nextToken() // consume ROLE + + stmt := &ast.CreateServerRoleStatement{} + + // Parse role name + stmt.Name = p.parseIdentifier() + + // Check for optional AUTHORIZATION + if p.curTok.Type == TokenAuthorization { + p.nextToken() // consume AUTHORIZATION + stmt.Owner = p.parseIdentifier() + } + + // Skip optional semicolon + if p.curTok.Type == TokenSemicolon { + p.nextToken() + } + + return stmt, nil +} + func (p *Parser) parseCreateContractStatement() (*ast.CreateContractStatement, error) { // Consume CONTRACT p.nextToken() diff --git a/parser/testdata/Baselines110_ServerRoleStatementTests/metadata.json b/parser/testdata/Baselines110_ServerRoleStatementTests/metadata.json index ccffb5b9..9e26dfee 100644 --- a/parser/testdata/Baselines110_ServerRoleStatementTests/metadata.json +++ b/parser/testdata/Baselines110_ServerRoleStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} \ No newline at end of file diff --git a/parser/testdata/ServerRoleStatementTests/metadata.json b/parser/testdata/ServerRoleStatementTests/metadata.json index ccffb5b9..9e26dfee 100644 --- a/parser/testdata/ServerRoleStatementTests/metadata.json +++ b/parser/testdata/ServerRoleStatementTests/metadata.json @@ -1 +1 @@ -{"todo": true} \ No newline at end of file +{} \ No newline at end of file